mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
* feat(swift): migrate Swift to scope-based registry resolution (#937) Ring 3 of RFC #909 — Swift is the final language migrated to the scope-based registry resolution pipeline. Flips Swift into MIGRATED_LANGUAGES so registry-primary call resolution is the production default, with dual-mode parity proven: the resolver suite passes 77/77 under both the legacy DAG (REGISTRY_PRIMARY_SWIFT=0) and the registry-primary path (REGISTRY_PRIMARY_SWIFT=1). New language module src/core/ingestion/languages/swift/ (mirrors csharp/): query, captures, interpret, import-decomposer, receiver-binding, signature-bindings, arity (+metadata), merge-bindings, simple-hooks, import-target, target-siblings, implicit-imports, sibling-type-bindings, scope-resolver, cache-stats, index. Parse-time hooks wired into the existing flat languages/swift.ts (coexists with the swift/ dir, like kotlin) and the resolver registered in the scope-resolution registry. tree-sitter-swift 0.7.1 specifics handled in the Swift module (not in shared code): - class / struct / extension all parse to class_declaration; extensions are re-keyed onto the extended type so members hoist (like C# partial). - if-let / guard-let have no if_let_binding node — the optional binding is synthesized from if_statement / guard_statement. - the name: field is reused for func name, param labels, param types and return type, so param/return type-bindings are synthesized in code (signature-bindings.ts) rather than via a multi-name query. - no `new` keyword: Type(...) and Type.init(...) are synthesized into constructor type-bindings. Shared-pipeline additions are language-agnostic (AGENTS.md: no language names in shared ingestion code): - constructorCallTargetsClass on the ScopeResolver contract + free-call-fallback option + run.ts wiring: when true, Type(...) links to the Class def rather than its explicit init Constructor. - pickUniqueGlobalClass: constructor-branch global fallback for cross-file types absent from the call site's lexical bindings, deduped by qualifiedName so extension/partial fragments aren't seen as ambiguous. - emitImplicitImportEdges: same-module File->File IMPORTS edges (Swift whole-module visibility has no syntactic import to drive the generic ImportEdge pipeline). Import resolution: rewrote the O(n^2) module scan in import-resolvers/configs/swift.ts with a WeakMap-memoized index. Benchmarks & guards: - Swift added to bench/scope-capture/measure.mjs + baselines.json (fingerprint + 1.5x scaling budget); `--check` passes for all 7 languages, Swift scaling 0.98 (linear). - golden capture-parity test (test/unit/scope-resolution/swift/swift-captures-golden.test.ts + fixtures/swift-captures-golden/) mirrors the csharp golden. - O(n) scope-capture tripwire (test/integration/swift-scope-capture-tripwire.test.ts). Full Swift test glob: 3 files / 88 tests pass; tsc --noEmit clean; no cross-language resolver regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(swift): green CI — Dart canary, cascade-safe availability test, prettier, comment/order nits (U1) * test(swift): wire createResolverParityIt('swift') + empty legacy skip-set (U2) * fix(swift): group same-module files by SPM target subtree in registry-primary hooks (U3) * fix(swift): correct member-write, class-func self, multi-clause if-let, nested-extension (U4) * perf(scope-resolution): build global class index once for pickUniqueGlobalClass (U5) * fix(swift): re-baseline scope-capture fingerprint after member-write capture change (U4) * style(swift): prettier-format pick-unique-global-class test (U5 follow-up) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
80 lines
3.3 KiB
TypeScript
80 lines
3.3 KiB
TypeScript
/**
|
||
* Tripwire: emitSwiftScopeCaptures must stay O(n) in entity count.
|
||
*
|
||
* #1848 (Go) was an O(n²) scope-capture regression: each capture re-derived
|
||
* its node via findNodeAtRange(tree.rootNode, …), so capture extraction went
|
||
* quadratic on big files. Swift threads the captured node through directly
|
||
* (issue #937, RFC #909 Ring 3). This guards against a re-introduction by
|
||
* asserting the per-entity cost stays roughly flat across a 4× size increase.
|
||
*
|
||
* Complements bench/scope-capture/measure.mjs (which pins a fingerprint + a
|
||
* 1.5× scaling budget for the `--check` CI job): this always-on test fails the
|
||
* normal suite — no GITNEXUS_BENCH gate, no committed baseline — if the path
|
||
* regresses to quadratic.
|
||
*
|
||
* Swift is an optional dependency; skips gracefully if the grammar isn't built.
|
||
*
|
||
* Kept out of the unit suite's tight timeout: lives in integration where a few
|
||
* hundred ms of generated-source parsing is acceptable. Pattern mirrors
|
||
* test/integration/csharp-scope-capture-tripwire.test.ts.
|
||
*/
|
||
import { describe, it, expect } from 'vitest';
|
||
import { emitSwiftScopeCaptures } from '../../src/core/ingestion/languages/swift/index.js';
|
||
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
|
||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||
|
||
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
|
||
|
||
/** Generate a Swift source file with `n` DAO-style classes. */
|
||
function generateSource(n: number): string {
|
||
const classes: string[] = [];
|
||
for (let i = 0; i < n; i++) {
|
||
classes.push(
|
||
`class Entity${i} {\n` +
|
||
` var id: Int64 = 0\n` +
|
||
` var name: String = ""\n` +
|
||
` func getId() -> Int64 { return self.id }\n` +
|
||
` func setName(_ v: String) { self.name = v }\n` +
|
||
`}`,
|
||
);
|
||
}
|
||
return classes.join('\n\n');
|
||
}
|
||
|
||
function median(xs: number[]): number {
|
||
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;
|
||
}
|
||
|
||
/** Median elapsed ms to run `emitSwiftScopeCaptures` over `reps` runs. */
|
||
function timeEmit(n: number, reps: number): number {
|
||
const src = generateSource(n);
|
||
// Warm up parser/query compilation so the first run's JIT cost is excluded.
|
||
emitSwiftScopeCaptures(src, 'warmup.swift');
|
||
const samples: number[] = [];
|
||
for (let i = 0; i < reps; i++) {
|
||
const start = process.hrtime.bigint();
|
||
emitSwiftScopeCaptures(src, `bench-${n}.swift`);
|
||
samples.push(Number(process.hrtime.bigint() - start) / 1e6);
|
||
}
|
||
return median(samples);
|
||
}
|
||
|
||
describe.skipIf(!swiftAvailable)('swift scope-capture scaling (O(n) tripwire)', () => {
|
||
it('emits at least one capture per entity', () => {
|
||
const matches = emitSwiftScopeCaptures(generateSource(250), 'count.swift');
|
||
expect(matches.length).toBeGreaterThan(250);
|
||
});
|
||
|
||
it('per-entity cost stays roughly flat from 250 to 1000 entities', () => {
|
||
const reps = 5;
|
||
const tSmall = timeEmit(250, reps);
|
||
const tLarge = timeEmit(1000, reps);
|
||
|
||
// Linear would be ~4× (4× the entities). Allow generous headroom for noise
|
||
// and parser variance; quadratic would be ~16×, so 8× cleanly separates them.
|
||
const ratio = tLarge / Math.max(tSmall, 0.001);
|
||
expect(ratio).toBeLessThan(8);
|
||
});
|
||
});
|