mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
253 lines
8.1 KiB
JavaScript
253 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Scope-emission bench (#2699).
|
|
*
|
|
* JavaScript/TypeScript gained block scopes so that `let`/`const` in sibling
|
|
* blocks are distinct bindings. Emitted naively — one scope per
|
|
* `statement_block` — that TRIPLED the block-scope count and cost ~10% of
|
|
* analyze wall time, because every scope-chain walk in every function then
|
|
* steps through levels that bind nothing.
|
|
*
|
|
* Two emit-side filters keep the semantics and drop the waste:
|
|
* 1. a block that IS a function body duplicates the enclosing Function scope;
|
|
* 2. a block that declares no `let`/`const`/`class`/`function` binds nothing,
|
|
* so it is transparent to every lookup.
|
|
*
|
|
* This bench guards that. It counts scope captures over a synthetic corpus
|
|
* whose shape is fixed in this file, so the numbers are exact and independent
|
|
* of the machine — unlike wall-clock analyze, where a 2% effect sits well
|
|
* inside the noise of a shared runner (measured: ±10% run to run).
|
|
*
|
|
* BOTH languages are measured. The filters are implemented twice —
|
|
* `FUNCTION_BODY_OWNER_TYPES` in `typescript/captures.ts` and
|
|
* `JS_FUNCTION_BODY_OWNER_TYPES` in `javascript/captures.ts`, each with its own
|
|
* `blockDeclaresBinding` and its own `BLOCK_BINDING_CHILD_TYPES` — so a
|
|
* TypeScript-only bench would let a JavaScript-only regression ship green.
|
|
*
|
|
* Loop and catch environments also carry lexical ownership. On this corpus
|
|
* the two currently agree exactly (4 blocks per module, 2600
|
|
* scopes). That is a measured result, not a required invariant: the fixtures
|
|
* are structurally parallel and the TS-only syntax they drop carries no extra
|
|
* scopes. Each language is still gated against its OWN baseline, because the
|
|
* filters are separate code and nothing enforces that the counts stay equal.
|
|
*
|
|
* Usage:
|
|
* node bench/scope-emission/measure.mjs # print measurements
|
|
* node bench/scope-emission/measure.mjs --check # gate against baselines
|
|
*
|
|
* Build-free: imports the TypeScript sources through tsx, like the other
|
|
* benches here.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const { emitTsScopeCaptures } =
|
|
await import('../../src/core/ingestion/languages/typescript/captures.ts');
|
|
const { emitJsScopeCaptures } =
|
|
await import('../../src/core/ingestion/languages/javascript/captures.ts');
|
|
|
|
/**
|
|
* One synthetic TypeScript module, parameterised by index so names stay
|
|
* distinct.
|
|
*
|
|
* Deliberately mixes the shapes the filters discriminate between:
|
|
* - function/method/arrow bodies → block scope must be SUPPRESSED
|
|
* - empty `if`/`else`/`while`/`try` → suppressed
|
|
* - lexical `for` and `catch` → environment scope REQUIRED
|
|
* - blocks declaring `let`/`const` → block scope REQUIRED (shadowing)
|
|
* - a block declaring only `var` → suppressed (`var` hoists past it)
|
|
*/
|
|
const tsModuleSource = (i) => `
|
|
export class Svc${i} {
|
|
private total = 0;
|
|
run(xs: number[]): number {
|
|
for (const x of xs) {
|
|
if (x > 0) {
|
|
this.total += x;
|
|
} else {
|
|
this.total -= x;
|
|
}
|
|
}
|
|
while (this.total > 100) {
|
|
this.total = this.total / 2;
|
|
}
|
|
try {
|
|
this.total = Math.round(this.total);
|
|
} catch {
|
|
this.total = 0;
|
|
}
|
|
return this.total;
|
|
}
|
|
pick(flag: boolean): number {
|
|
if (flag) {
|
|
const chosen = (n: number) => n * 2;
|
|
return chosen(1);
|
|
} else {
|
|
const chosen = (n: number) => n * 3;
|
|
return chosen(2);
|
|
}
|
|
}
|
|
hoisted(flag: boolean): number {
|
|
if (flag) { var v = 1; }
|
|
return v ?? 0;
|
|
}
|
|
}
|
|
|
|
export function free${i}(): number {
|
|
const inner = (n: number) => n + 1;
|
|
return inner(1);
|
|
}
|
|
`;
|
|
|
|
/** The same shapes with the TypeScript-only syntax removed. Kept structurally
|
|
* parallel to `tsModuleSource` on purpose: when the two languages' block
|
|
* counts diverge, the cause is the emitter, not the fixture. */
|
|
const jsModuleSource = (i) => `
|
|
export class Svc${i} {
|
|
total = 0;
|
|
run(xs) {
|
|
for (const x of xs) {
|
|
if (x > 0) {
|
|
this.total += x;
|
|
} else {
|
|
this.total -= x;
|
|
}
|
|
}
|
|
while (this.total > 100) {
|
|
this.total = this.total / 2;
|
|
}
|
|
try {
|
|
this.total = Math.round(this.total);
|
|
} catch {
|
|
this.total = 0;
|
|
}
|
|
return this.total;
|
|
}
|
|
pick(flag) {
|
|
if (flag) {
|
|
const chosen = (n) => n * 2;
|
|
return chosen(1);
|
|
} else {
|
|
const chosen = (n) => n * 3;
|
|
return chosen(2);
|
|
}
|
|
}
|
|
hoisted(flag) {
|
|
if (flag) { var v = 1; }
|
|
return v ?? 0;
|
|
}
|
|
}
|
|
|
|
export function free${i}() {
|
|
const inner = (n) => n + 1;
|
|
return inner(1);
|
|
}
|
|
`;
|
|
|
|
const CORPUS_MODULES = 200;
|
|
const REPS = 7;
|
|
|
|
const LANGUAGES = [
|
|
{ name: 'typescript', ext: 'ts', emit: emitTsScopeCaptures, moduleSource: tsModuleSource },
|
|
{ name: 'javascript', ext: 'js', emit: emitJsScopeCaptures, moduleSource: jsModuleSource },
|
|
];
|
|
|
|
const measure = ({ ext, emit, moduleSource }) => {
|
|
const corpus = Array.from({ length: CORPUS_MODULES }, (_, i) => ({
|
|
path: `bench/mod${i}.${ext}`,
|
|
source: moduleSource(i),
|
|
}));
|
|
|
|
const tally = () => {
|
|
const counts = new Map();
|
|
for (const { path, source } of corpus) {
|
|
for (const match of emit(source, path)) {
|
|
for (const key of Object.keys(match)) {
|
|
// These decorate an existing scope; they do not create scopes.
|
|
if (key === '@scope.lookup-policy' || key === '@scope.name-claims') continue;
|
|
if (key.startsWith('@scope.')) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
}
|
|
}
|
|
}
|
|
return counts;
|
|
};
|
|
|
|
// Warm the parser + query caches so the timing reflects steady state.
|
|
tally();
|
|
|
|
let bestMs = Infinity;
|
|
let counts;
|
|
for (let r = 0; r < REPS; r++) {
|
|
const t0 = process.hrtime.bigint();
|
|
counts = tally();
|
|
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
|
if (ms < bestMs) bestMs = ms;
|
|
}
|
|
|
|
const scopes = Object.fromEntries([...counts.entries()].sort());
|
|
return {
|
|
modules: CORPUS_MODULES,
|
|
scopes,
|
|
total_scopes: Object.values(scopes).reduce((a, b) => a + b, 0),
|
|
emit_min_ms: Number(bestMs.toFixed(2)),
|
|
blocks_per_module: Number(((scopes['@scope.block'] ?? 0) / CORPUS_MODULES).toFixed(3)),
|
|
};
|
|
};
|
|
|
|
const result = Object.fromEntries(LANGUAGES.map((lang) => [lang.name, measure(lang)]));
|
|
|
|
if (!process.argv.includes('--check')) {
|
|
console.log(JSON.stringify(result, null, 2));
|
|
process.exit(0);
|
|
}
|
|
|
|
const baselines = JSON.parse(readFileSync(join(HERE, 'baselines.json'), 'utf8'));
|
|
const failures = [];
|
|
|
|
for (const { name } of LANGUAGES) {
|
|
const expected = baselines[name];
|
|
const actual = result[name];
|
|
if (expected === undefined) {
|
|
failures.push(`${name}: no baseline entry — add one rather than skipping the language`);
|
|
continue;
|
|
}
|
|
|
|
// Scope counts are EXACT — a synthetic corpus and a deterministic emitter. A
|
|
// mismatch means the emitted scope set moved and must be explained, never
|
|
// re-baselined to make CI green.
|
|
for (const [key, want] of Object.entries(expected.scopes)) {
|
|
const got = actual.scopes[key] ?? 0;
|
|
if (got !== want) failures.push(`${name} ${key}: expected ${want}, got ${got}`);
|
|
}
|
|
for (const key of Object.keys(actual.scopes)) {
|
|
if (!(key in expected.scopes)) {
|
|
failures.push(`${name}: unexpected capture ${key}: ${actual.scopes[key]}`);
|
|
}
|
|
}
|
|
|
|
// Timing carries deliberate headroom for shared CI runners; it exists to
|
|
// catch an order-of-magnitude regression, not to police a few percent.
|
|
if (actual.emit_min_ms > expected.emit_ms_budget) {
|
|
failures.push(
|
|
`${name} emit_min_ms ${actual.emit_min_ms} exceeds budget ${expected.emit_ms_budget}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// A language present in baselines but not measured means the bench stopped
|
|
// covering it — the exact way a gate goes quietly green.
|
|
for (const name of Object.keys(baselines)) {
|
|
if (name.startsWith('_')) continue;
|
|
if (!(name in result)) failures.push(`${name}: baselined but not measured`);
|
|
}
|
|
|
|
console.log(JSON.stringify(result, null, 2));
|
|
if (failures.length > 0) {
|
|
console.error('[scope-emission --check] FAIL');
|
|
for (const f of failures) console.error(` - ${f}`);
|
|
process.exit(1);
|
|
}
|
|
console.log('[scope-emission --check] PASS');
|