mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
* feat(scope-resolution): migrate Dart to registry-primary call resolution (#939) Add a Dart scope-resolution module (languages/dart/) mirroring the Swift template and flip Dart to registry-primary. Resolution edges (CALLS/IMPORTS/ACCESSES/EXTENDS/IMPLEMENTS/METHOD_IMPLEMENTS) now route through the shared registry pipeline with byte-for-byte parity against the legacy DAG: test/integration/resolvers/dart.test.ts passes 53/53 under both REGISTRY_PRIMARY_DART=0 and =1 (scripts/run-parity.ts --language dart: 2/2). Dart-specific handling: - Function scopes are synthesized to span signature..body (tree-sitter function_signature/function_body are siblings, not parent/child). - extends rides @reference.inherits (EXTENDS via the generic pre-pass); implements/with are carried as __heritage__ side-effect imports and emitted as IMPLEMENTS, since Dart `implements <class>` must be IMPLEMENTS regardless of the target's symbol kind. - imports are wildcard (whole-library) with expandsWildcardTo so imported return types propagate cross-file (var u = getUser(); u.save()). - getInnerSignature now self-returns a bare signature node so top-level function params/return/name extract (legacy-safe: legacy only ever passes method_signature/declaration wrappers). Also: add Dart scope-capture bench coverage (linear ~0.99 scaling); update two tests that used Dart as a non-migrated control (Vue / forced legacy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): close Dart registry-primary parity gaps from review Adversarial review of #1970 surfaced real divergences from the legacy DAG on constructs the 10 fixtures don't exercise. All fixed; parity gate still 2/2 (now 55/55 each mode): - Implicit-constructor construction (`Foo()` with no explicit ctor): the legacy DAG emits `caller -> Foo` (Class) but registry emitted nothing (callee tagged @reference.call.free never reaches constructorCallTargetsClass). Re-tag UpperCamelCase free-callees to @reference.call.constructor (Dart types are UpperCamelCase) so they link to the Class. Locked in with a regression fixture + test that passes in BOTH modes. - Cascade calls (`list..add(1)..sort()`) were dropped — cascade_section has no `selector` wrapper, so the reference walk never saw them while legacy emitted them as free calls. Add a cascade_section handler. - BUILT_INS (setState/then/push/pop/listen/...) were not suppressed on the registry path, so a user symbol shadowing one produced a spurious CALLS edge the legacy DAG suppresses. Skip built-in-named call refs at capture time (extract the set to a leaf module shared with the provider). - Enhanced-enum methods mis-parented to Module (no enum scope). Add `(enum_declaration) @scope.class` so enum members are owned by the enum. Re-baseline the Dart scope-capture fingerprint (linear ~0.95). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scope-resolution): apply issue #1926 F24/F25 findings to the Dart scope path Issue #1926 catalogs Dart parsing-layer coverage gaps. Apply the two that the registry-primary scope-resolution path owns (call edges + call attribution), registered as legacy-expected-failures since they are scope-resolver-only wins. - F24: the scope path's unified tree-walk already captures member calls (obj.method()) in return / list-literal / named-argument / arrow-body contexts — the legacy DAG only captures them under expression_statement / initialized_variable_definition. Lock it with the dart-member-call-contexts fixture + tests. - F25 (constructor portion): a constructor's body is a sibling of the WRAPPING method_signature (class_body > method_signature > constructor_signature, then function_body), so findFunctionBody now walks up to the method_signature wrapper. Constructor bodies get a Function scope and their body-calls attribute to the Constructor (a valid caller anchor) instead of the class. Add the dart-constructor-body fixture + test. Switch dart.test.ts to createResolverParityIt('dart') and add the dart entry to LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES (5 wins). Both modes pass: run-parity --language dart → 2/2 (registry 60/60; legacy 55 pass + 5 skipped). Not applicable to the scope path (structure-phase / shared-pipeline, tracked by #1926's legacy fix): F25 getter/setter (Property is not a caller anchor) and operator (no Method node emitted by the structure phase) bodies; F26 (static field Property nodes); F27 (no generic_type reference in the scope module); F28/F29 (typedef/variable node extraction). Re-baseline the Dart scope-capture fingerprint (linear ~1.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): fix Dart named-constructor file-drop + container-name mis-binding (tri-review) Multi-engine tri-review (GitNexus + CE personas + Codex gpt-5.5) of #1970 found a P0 the parity gate missed plus a P2 wrong-edge: - P0 (file drop): a named constructor with a body (`class A { A.named() {…} }`, idiomatic Dart) parses as ONE constructor_signature carrying multiple `name:` fields, so the scope query matched it more than once and synthesized two identical-range @scope.function captures → ScopeTreeInvariantError(duplicate- scope-id) → extractParsedFile swallowed it → the WHOLE file was dropped from registry-primary resolution (CALLS=0 vs legacy CALLS=2). Introduced by the #1926 F25 findFunctionBody change that started giving constructors body scopes. Fix: dedup function-like declarations by their statement node so each is emitted once. Add dart-named-constructor-body fixture + a parity guard test (both modes) that fails if the file is dropped, plus the named-ctor F25 attribution win (registry-only). - P2 (wrong edge): normalizeDartType's Future<X>/List<X> unwrap is unreachable (generic args are stripped upstream to a bare `Future`/`List`), so a return/ field type binding to the bare container name let a same-named user class (`class Stream {…}`) capture the receiver — a wrong CALLS edge legacy didn't emit. Suppress type bindings that normalize to a bare container name (leaving the call unresolved, matching legacy) instead of binding to the container. Both modes still pass: run-parity --language dart → 2/2 (registry 62/62; legacy 56 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Also: refresh the captures.ts module doc (constructors get scopes; cascade calls). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): address Dart tri-review follow-ups (heritage collision + polish) - P2 heritage cross-file name collision: emitDartHeritageEdges resolved both child and base by a global last-write-wins simple-name map, so two files each declaring `class Logger` (one `implements Logger`) produced a wrong-file IMPLEMENTS edge. Resolve with same-file affinity (prefer a same-file class, then a workspace-unique match, else refuse to guess) — the #1951 file-affinity pattern. Add dart-heritage-name-collision fixture + a parity test (both modes resolve same-file). Also reason-qualify the dedup key so `implements X` + `with X` keep distinct edges. - Polish: buildDartMro uses Sets instead of Array.includes-in-loop; merge-bindings uses named tier constants matching swift; drop the dead no-op stripQuotes in import-target (targetRaw already arrives quote-stripped). Both modes pass: run-parity --language dart → 2/2 (registry 63/63; legacy 57 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
409 lines
16 KiB
JavaScript
409 lines
16 KiB
JavaScript
/**
|
||
* Unified build-free scope-capture measurement harness for every currently
|
||
* benchmarked language (the ones with a `*-pipeline-benchmark.test.ts`):
|
||
* go, csharp, rust, php, ruby, cobol, swift — plus python lives in its own
|
||
* `bench/python-scope/` harness (richer: it also covers import resolution).
|
||
*
|
||
* For each language it:
|
||
* - times `emit<Lang>ScopeCaptures` on a synthetic DAO-style source at two
|
||
* sizes (250 / 800 top-level entities), reporting elapsed_ms + a scaling
|
||
* ratio `(t_large/t_small)/(800/250)`: ~1.0 is linear, ~3.2 is quadratic
|
||
* (the O(matches × rootChildren) shape #1848 hit in Go);
|
||
* - computes an order-independent sha256 fingerprint over the whole
|
||
* `lang-resolution/<lang>-*` fixture corpus + a fixed 20-entity synthetic
|
||
* source, as the correctness gate.
|
||
*
|
||
* Build-free: imports the `.ts` hotpaths through tsx
|
||
* (`node --import tsx bench/scope-capture/measure.mjs`). Static `.ts` imports
|
||
* work; a top-level `await import()` breaks tsx's lexer.
|
||
*
|
||
* Without args: prints one JSON object per language.
|
||
* With `--check`: asserts each language's fingerprint == its committed baseline
|
||
* (baselines.json) AND scaling_ratio < that language's recorded budget; exits
|
||
* non-zero on any drift/regression.
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import crypto from 'node:crypto';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { emitGoScopeCaptures } from '../../src/core/ingestion/languages/go/index.ts';
|
||
import { emitCsharpScopeCaptures } from '../../src/core/ingestion/languages/csharp/index.ts';
|
||
import { emitRustScopeCaptures } from '../../src/core/ingestion/languages/rust/index.ts';
|
||
import { emitPhpScopeCaptures } from '../../src/core/ingestion/languages/php/index.ts';
|
||
import { emitRubyScopeCaptures } from '../../src/core/ingestion/languages/ruby/index.ts';
|
||
import { emitCobolScopeCaptures } from '../../src/core/ingestion/languages/cobol/index.ts';
|
||
import { emitSwiftScopeCaptures } from '../../src/core/ingestion/languages/swift/index.ts';
|
||
import { emitTsScopeCaptures } from '../../src/core/ingestion/languages/typescript/index.ts';
|
||
import { emitJsScopeCaptures } from '../../src/core/ingestion/languages/javascript/index.ts';
|
||
import { emitKotlinScopeCaptures } from '../../src/core/ingestion/languages/kotlin/index.ts';
|
||
import { emitJavaScopeCaptures } from '../../src/core/ingestion/languages/java/index.ts';
|
||
import { emitCScopeCaptures } from '../../src/core/ingestion/languages/c/index.ts';
|
||
import { emitCppScopeCaptures } from '../../src/core/ingestion/languages/cpp/index.ts';
|
||
import { emitDartScopeCaptures } from '../../src/core/ingestion/languages/dart/index.ts';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'lang-resolution');
|
||
const BASELINE_PATH = path.resolve(__dirname, 'baselines.json');
|
||
|
||
// ---- correctness fingerprint (order-independent; mirrors python harness) ----
|
||
|
||
function canonicalizeMatch(match) {
|
||
const parts = [];
|
||
for (const tag of Object.keys(match)) {
|
||
const cap = match[tag];
|
||
if (cap === undefined || cap === null || cap.range === undefined) {
|
||
parts.push(`${tag}|<no-range>`);
|
||
continue;
|
||
}
|
||
const r = cap.range;
|
||
parts.push(`${tag}|${cap.text}|${r.startLine}:${r.startCol}-${r.endLine}:${r.endCol}`);
|
||
}
|
||
parts.sort();
|
||
return parts.join(';');
|
||
}
|
||
|
||
function digestCaptures(matches) {
|
||
return crypto
|
||
.createHash('sha256')
|
||
.update(matches.map(canonicalizeMatch).sort().join('\n'))
|
||
.digest('hex');
|
||
}
|
||
|
||
/** All fixture files for a language, sorted by repo-relative key. */
|
||
function collectFixtures(prefix, exts) {
|
||
const out = [];
|
||
for (const entry of fs.readdirSync(FIXTURE_ROOT, { withFileTypes: true })) {
|
||
if (!entry.isDirectory() || !entry.name.startsWith(`${prefix}-`)) continue;
|
||
const stack = [path.join(FIXTURE_ROOT, entry.name)];
|
||
while (stack.length) {
|
||
const dir = stack.pop();
|
||
for (const c of fs.readdirSync(dir, { withFileTypes: true })) {
|
||
const p = path.join(dir, c.name);
|
||
if (c.isDirectory()) stack.push(p);
|
||
else if (exts.some((e) => c.name.endsWith(e))) {
|
||
out.push({ key: path.relative(FIXTURE_ROOT, p).split(path.sep).join('/'), absPath: p });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out.sort((a, b) => a.key.localeCompare(b.key));
|
||
return out;
|
||
}
|
||
|
||
// ---- per-language config: synthetic DAO generators + fixture globs ----
|
||
|
||
const LANGS = [
|
||
{
|
||
name: 'go',
|
||
emit: emitGoScopeCaptures,
|
||
fixturePrefix: 'go',
|
||
exts: ['.go'],
|
||
file: 'bench.go',
|
||
// Heritage-bearing: each Entity embeds Base (Go inheritance = struct
|
||
// embedding) so the @reference.inherits synth (#1951) is driven at scale.
|
||
header:
|
||
'package generated\n\ntype Base struct{}\n\nfunc (b *Base) BaseMethod() string { return "base" }\n\n',
|
||
unit: (n) =>
|
||
`type Entity${n} struct {\n\tBase\n\tid int64\n\tname string\n}\n\n` +
|
||
`func (e *Entity${n}) GetID() int64 { return e.id }\n` +
|
||
`func (e *Entity${n}) SetName(v string) { e.name = v }\n\n`,
|
||
},
|
||
{
|
||
name: 'csharp',
|
||
emit: emitCsharpScopeCaptures,
|
||
fixturePrefix: 'csharp',
|
||
exts: ['.cs'],
|
||
file: 'bench.cs',
|
||
// Heritage-bearing: extends Base + implements IEntity (both forms) so the
|
||
// @reference.inherits synth (#1951) is driven at scale, not just the base loop.
|
||
header:
|
||
'namespace Generated;\n\npublic class Base { }\n\npublic interface IEntity {\n long GetId();\n}\n\n',
|
||
unit: (n) =>
|
||
`public class Entity${n} : Base, IEntity {\n` +
|
||
` public long Id;\n public string Name;\n` +
|
||
` public long GetId() { return Id; }\n` +
|
||
` public void SetName(string v) { Name = v; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'rust',
|
||
emit: emitRustScopeCaptures,
|
||
fixturePrefix: 'rust',
|
||
exts: ['.rs'],
|
||
file: 'bench.rs',
|
||
// Heritage-bearing: `impl Shape for Entity_n` (Rust inheritance lives on
|
||
// impl_item) so the @reference.inherits trait-impl synth (#1951) is driven
|
||
// at scale. The two methods move into the trait impl to keep unit size flat.
|
||
header: 'trait Shape {\n fn area(&self) -> i64;\n fn name(&self) -> String;\n}\n\n',
|
||
unit: (n) =>
|
||
`struct Entity${n} {\n id: i64,\n name: String,\n}\n\n` +
|
||
`impl Shape for Entity${n} {\n` +
|
||
` fn area(&self) -> i64 { self.id }\n` +
|
||
` fn name(&self) -> String { self.name.clone() }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'php',
|
||
emit: emitPhpScopeCaptures,
|
||
fixturePrefix: 'php',
|
||
exts: ['.php'],
|
||
file: 'bench.php',
|
||
// Heritage-bearing: extends Base + uses a trait (both forms) so the
|
||
// @reference.inherits synth (#1951) is driven at scale.
|
||
header:
|
||
'<?php\n\nclass Base {}\n\ntrait Auditable {\n public function audit() { return true; }\n}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} extends Base {\n` +
|
||
` use Auditable;\n` +
|
||
` public $id;\n public $name;\n` +
|
||
` function getId() { return $this->id; }\n` +
|
||
` function setName($v) { $this->name = $v; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'ruby',
|
||
emit: emitRubyScopeCaptures,
|
||
fixturePrefix: 'ruby',
|
||
exts: ['.rb'],
|
||
file: 'bench.rb',
|
||
// Heritage-bearing: `< Base` superclass + `include Trackable` mixin (both
|
||
// forms) so the @reference.inherits synth (#1951) is driven at scale.
|
||
header:
|
||
'class Base\n def base_id\n @id\n end\nend\n\nmodule Trackable\n def track\n @tracked = true\n end\nend\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} < Base\n` +
|
||
` include Trackable\n` +
|
||
` def get_id\n @id\n end\n` +
|
||
` def set_name(v)\n @name = v\n end\nend\n\n`,
|
||
},
|
||
{
|
||
name: 'cobol',
|
||
emit: emitCobolScopeCaptures,
|
||
fixturePrefix: 'cobol',
|
||
exts: ['.cbl', '.cpy'],
|
||
file: 'bench.cbl',
|
||
header:
|
||
' IDENTIFICATION DIVISION.\n' +
|
||
' PROGRAM-ID. BENCH.\n' +
|
||
' PROCEDURE DIVISION.\n',
|
||
unit: (n) => ` PARA-${String(n).padStart(5, '0')}.\n DISPLAY "P${n}".\n`,
|
||
},
|
||
{
|
||
name: 'c',
|
||
emit: emitCScopeCaptures,
|
||
fixturePrefix: 'c',
|
||
exts: ['.c', '.h'],
|
||
file: 'bench.c',
|
||
// C has no inheritance construct — flat scale source. Added (was unbenched);
|
||
// adding it exposed + fixed the same O(n²) findNodeAtRange root-walk (#1956).
|
||
header: '#include <stdint.h>\n#include <stddef.h>\n\ntypedef int64_t id_t;\n\n',
|
||
unit: (n) =>
|
||
`typedef struct Entity${n} {\n id_t id;\n const char *name;\n} Entity${n};\n\n` +
|
||
`id_t entity_${n}_get_id(Entity${n} *e) { return e->id; }\n` +
|
||
`void entity_${n}_set_name(Entity${n} *e, const char *v) { e->name = v; }\n\n`,
|
||
},
|
||
{
|
||
name: 'cpp',
|
||
emit: emitCppScopeCaptures,
|
||
fixturePrefix: 'cpp',
|
||
exts: ['.cpp', '.cc', '.cxx', '.hpp', '.h'],
|
||
file: 'bench.cpp',
|
||
// Heritage-bearing: `: public Base, public Mixin` (single + multiple
|
||
// inheritance) drives emitCppInheritanceCaptures (#1951) at scale. Added
|
||
// (was unbenched); adding it exposed + fixed the same O(n²) root-walk (#1956).
|
||
header:
|
||
'#include <string>\n\nclass Base {\n public:\n long baseId() const { return 0; }\n};\n\nclass Mixin {\n public:\n void mix() {}\n};\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} : public Base, public Mixin {\n public:\n long id;\n std::string name;\n` +
|
||
` long getId() const { return id; }\n` +
|
||
` void setName(std::string v) { name = v; }\n};\n\n`,
|
||
},
|
||
{
|
||
name: 'swift',
|
||
emit: emitSwiftScopeCaptures,
|
||
fixturePrefix: 'swift',
|
||
exts: ['.swift'],
|
||
file: 'bench.swift',
|
||
// Heritage-bearing: inherits Base + conforms to Serviceable (both forms) so
|
||
// the @reference.inherits synth (#1951) is driven at scale.
|
||
header:
|
||
'class Base {\n func ping() -> String { return "base" }\n}\n\nprotocol Serviceable {\n func serve() -> String\n}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n}: Base, Serviceable {\n` +
|
||
` var id: Int64 = 0\n var name: String = ""\n` +
|
||
` func getId() -> Int64 { return self.id }\n` +
|
||
` func serve() -> String { return self.name }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'dart',
|
||
emit: emitDartScopeCaptures,
|
||
fixturePrefix: 'dart',
|
||
exts: ['.dart'],
|
||
file: 'bench.dart',
|
||
// Heritage-bearing: `extends Base` (generic @reference.inherits pre-pass)
|
||
// + `implements Marker` (Dart `implements <class>` → IMPLEMENTS marker) so
|
||
// both heritage paths and the postfix-chain reference walk run at scale.
|
||
header:
|
||
'class Base {\n String ping() { return "base"; }\n}\n\nabstract class Marker {\n String mark();\n}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} extends Base implements Marker {\n` +
|
||
` int id = 0;\n String name = '';\n` +
|
||
` int getId() { return this.id; }\n` +
|
||
` String mark() { return this.name; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'java',
|
||
emit: emitJavaScopeCaptures,
|
||
fixturePrefix: 'java',
|
||
exts: ['.java'],
|
||
file: 'bench.java',
|
||
// Java was previously unbenched. Heritage-bearing: extends Base + implements
|
||
// Marker (both forms) so the @reference.inherits synth (#1951) is driven at scale.
|
||
header: 'package generated;\n\nclass Base {}\n\ninterface Marker {}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} extends Base implements Marker {\n` +
|
||
` long id = 0L;\n String name = "";\n` +
|
||
` public long getId() { return this.id; }\n` +
|
||
` public void setName(String v) { this.name = v; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'typescript',
|
||
emit: emitTsScopeCaptures,
|
||
fixturePrefix: 'typescript',
|
||
exts: ['.ts', '.tsx'],
|
||
file: 'bench.ts',
|
||
// Inheritance-bearing units so the @reference.inherits synth pass (#1951)
|
||
// is exercised at scale, not just the base capture loop.
|
||
header: 'class Base {}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} extends Base {\n` +
|
||
` id: number = 0;\n name: string = '';\n` +
|
||
` getId(): number { return this.id; }\n` +
|
||
` setName(v: string): void { this.name = v; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'javascript',
|
||
emit: emitJsScopeCaptures,
|
||
fixturePrefix: 'javascript',
|
||
exts: ['.js', '.jsx', '.mjs', '.cjs'],
|
||
file: 'bench.js',
|
||
header: 'class Base {}\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} extends Base {\n` +
|
||
` getId() { return this.id; }\n` +
|
||
` setName(v) { this.name = v; }\n}\n\n`,
|
||
},
|
||
{
|
||
name: 'kotlin',
|
||
emit: emitKotlinScopeCaptures,
|
||
fixturePrefix: 'kotlin',
|
||
exts: ['.kt', '.kts'],
|
||
file: 'bench.kt',
|
||
header: 'open class Base\n\n',
|
||
unit: (n) =>
|
||
`class Entity${n} : Base() {\n` +
|
||
` var id: Long = 0\n var name: String = ""\n` +
|
||
` fun getId(): Long { return id }\n` +
|
||
` fun setName(v: String) { name = v }\n}\n\n`,
|
||
},
|
||
];
|
||
|
||
function generate(lang, entityCount) {
|
||
let src = lang.header;
|
||
for (let i = 0; i < entityCount; i++) src += lang.unit(i);
|
||
return src;
|
||
}
|
||
|
||
// ---- timing ----
|
||
|
||
function median(xs) {
|
||
const s = [...xs].sort((a, b) => a - b);
|
||
const m = Math.floor(s.length / 2);
|
||
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
||
}
|
||
|
||
function timeEmit(emit, src, file, reps) {
|
||
emit(src, `warmup-${file}`); // warm parser/query JIT (not counted)
|
||
const samples = [];
|
||
let count = 0;
|
||
for (let i = 0; i < reps; i++) {
|
||
const start = process.hrtime.bigint();
|
||
const out = emit(src, file);
|
||
samples.push(Number(process.hrtime.bigint() - start) / 1e6);
|
||
count = out.length;
|
||
}
|
||
return { ms: median(samples), count };
|
||
}
|
||
|
||
const SMALL = 250;
|
||
const LARGE = 800;
|
||
const REPS = 7;
|
||
|
||
function measureLang(lang) {
|
||
// Correctness fingerprint over the fixture corpus + a fixed 20-entity source.
|
||
const perFixture = [];
|
||
let groups = 0;
|
||
for (const { key, absPath } of collectFixtures(lang.fixturePrefix, lang.exts)) {
|
||
const matches = lang.emit(fs.readFileSync(absPath, 'utf8'), absPath);
|
||
groups += matches.length;
|
||
perFixture.push(`${key}\t${matches.length}\t${digestCaptures(matches)}`);
|
||
}
|
||
const daoMatches = lang.emit(generate(lang, 20), `synthetic-dao-20${path.extname(lang.file)}`);
|
||
groups += daoMatches.length;
|
||
perFixture.push(`synthetic:dao-20\t${daoMatches.length}\t${digestCaptures(daoMatches)}`);
|
||
const fingerprint = crypto
|
||
.createHash('sha256')
|
||
.update(perFixture.sort().join('\n'))
|
||
.digest('hex');
|
||
|
||
// Scaling.
|
||
const small = timeEmit(lang.emit, generate(lang, SMALL), lang.file, REPS);
|
||
const large = timeEmit(lang.emit, generate(lang, LARGE), lang.file, REPS);
|
||
const scalingRatio = small.ms > 0 ? large.ms / small.ms / (LARGE / SMALL) : 0;
|
||
|
||
return {
|
||
language: lang.name,
|
||
elapsed_ms_small: Number(small.ms.toFixed(2)),
|
||
elapsed_ms_large: Number(large.ms.toFixed(2)),
|
||
scaling_ratio: Number(scalingRatio.toFixed(3)),
|
||
capture_groups_small: small.count,
|
||
capture_groups_large: large.count,
|
||
fingerprint,
|
||
capture_groups_fp: groups,
|
||
fixture_count: perFixture.length,
|
||
};
|
||
}
|
||
|
||
// ---- run ----
|
||
|
||
const CHECK = process.argv.includes('--check');
|
||
const results = LANGS.map(measureLang);
|
||
|
||
if (!CHECK) {
|
||
for (const r of results) process.stdout.write(JSON.stringify(r) + '\n');
|
||
} else {
|
||
const baselines = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'));
|
||
const failures = [];
|
||
for (const r of results) {
|
||
const base = baselines[r.language];
|
||
if (base === undefined) {
|
||
failures.push(`${r.language}: no baseline recorded`);
|
||
continue;
|
||
}
|
||
if (r.fingerprint !== base.fingerprint) {
|
||
failures.push(
|
||
`${r.language}: capture fingerprint drift (got ${r.fingerprint}, expected ${base.fingerprint})`,
|
||
);
|
||
}
|
||
if (r.scaling_ratio >= base.scaling_budget) {
|
||
failures.push(
|
||
`${r.language}: scaling ratio ${r.scaling_ratio} >= budget ${base.scaling_budget} ` +
|
||
`(${SMALL}->${LARGE} ms ${r.elapsed_ms_small}->${r.elapsed_ms_large})`,
|
||
);
|
||
}
|
||
process.stdout.write(JSON.stringify(r) + '\n');
|
||
}
|
||
if (failures.length > 0) {
|
||
for (const f of failures) process.stderr.write(`[scope-capture --check] FAIL: ${f}\n`);
|
||
process.exit(1);
|
||
}
|
||
process.stderr.write(`[scope-capture --check] PASS (${results.length} languages)\n`);
|
||
}
|