mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
* docs: add Objective-C fork provider notes * feat(objective-c): add deterministic provider and grammar * feat(objective-c): finalize provider MVP * fix(objective-c): harden provider integration * fix(objective-c): normalize bare macro markers * docs(objective-c): integrate provider documentation * fix(objective-c): harden resolution and header classification * fix(objective-c): complete provider follow-ups * fix: address Objective-C review follow-ups * chore: format Objective-C grammar sources * fix(objective-c): harden review follow-ups * Address PR review feedback (#3179) Keep Objective-C chunking and macro recovery aligned with the grammar, and stop Community MEMBER_OF edges from leaking into symbol context. Co-authored-by: Cursor <cursoragent@cursor.com> * Address follow-up review on ObjC chunking and language fallback. Keep preprocessor directive text from changing file-scope brace depth, group real ivar nodes, skip header modifiers, and restore Rakefile/Gemfile detection through getLanguageFromFilename. Co-authored-by: Cursor <cursoragent@cursor.com> * Parse Objective-C headers with the objc grammar in embeddings. ensureAndParse and structural extraction now use the same content classifier as ingest, including method snippets from .h files, so Protocol/Category/Class chunks are not re-parsed as C++. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3179) Keep file-scope macro elision off C line splices and @interface/@protocol/@implementation bodies, and attach ivar attributes to the following instance variable when chunking. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(bench): rebaseline Objective-C CSV emit * feat(objective-c): add workspace resolution and linear emit benches Plain .h files are classified as C++, so the ObjC pass could not resolve #import of those headers. Load a C/C#-style workspace once per pass, and keep protocol-candidate USES linear. Refs #3179 Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3179) - Compare LadybugDB labels() as a scalar when excluding Community MEMBER_OF edges. - Walk superclass members, skip file-static C sibling defs, and ignore comments in ObjC header/macro scans. Note: pre-existing failure in objective-c-provider integration (worker-pool ready timeout) not addressed by this PR. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3179) Emit Objective-C declaration captures so compilation-unit siblings can share header/implementation bindings, and keep class vs protocol visibility groups distinct. Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3179) Emit every comma-separated property/ivar declarator, and count @interface after a multiline block comment closes so in-declaration macros stay intact. Note: pre-existing failure in worker-pool startup (GITNEXUS_WORKER_READY_TIMEOUT_MS) not addressed by this PR. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: ximengkai <ximengkai@soyoung.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Build-free scaling and correctness guard for Objective-C workspace
|
|
* resolution (#3179).
|
|
*
|
|
* Import lookup already lives in bench/import-target (the `objc` arm). This
|
|
* bench isolates `emitPostResolutionEdges`: heritage, category membership,
|
|
* and static message-send dispatch. Two shapes:
|
|
*
|
|
* spread — typed self / super / sibling receivers. Cost must stay
|
|
* linear in class count (the C# "namespaces spread" analog).
|
|
* protocol — every class conforms to one protocol and sends one
|
|
* protocol-typed message. Implementer USES live on one
|
|
* shared node per (protocol, selector); messages only add
|
|
* the source→shared hop. Both arms must stay linear.
|
|
*
|
|
* Usage:
|
|
* node --import tsx bench/objective-c-resolution/measure.mjs
|
|
* node --import tsx bench/objective-c-resolution/measure.mjs --check
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { performance } from 'node:perf_hooks';
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.ts';
|
|
import {
|
|
OBJECTIVE_C_GRAMMAR_PACKAGE,
|
|
OBJECTIVE_C_GRAMMAR_VERSION,
|
|
OBJECTIVE_C_PROVIDER_VERSION,
|
|
objcClassQualifiedName,
|
|
objcMethodQualifiedName,
|
|
objcProtocolQualifiedName,
|
|
} from '../../src/core/ingestion/languages/objective-c/facts.ts';
|
|
import { objectiveCScopeResolver } from '../../src/core/ingestion/languages/objective-c/scope-resolver.ts';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const SMALL_CLASSES = 40;
|
|
const LARGE_CLASSES = 160;
|
|
const REPS = 7;
|
|
|
|
const graphNodeId = (label, qualifiedName) => `${label}:${qualifiedName}`;
|
|
|
|
function emptyFacts(filePath) {
|
|
return {
|
|
providerVersion: OBJECTIVE_C_PROVIDER_VERSION,
|
|
grammarPackage: OBJECTIVE_C_GRAMMAR_PACKAGE,
|
|
grammarVersion: OBJECTIVE_C_GRAMMAR_VERSION,
|
|
filePath,
|
|
containers: [],
|
|
methods: [],
|
|
members: [],
|
|
functions: [],
|
|
imports: [],
|
|
messages: [],
|
|
unresolvedMessages: [],
|
|
};
|
|
}
|
|
|
|
function classContainer(filePath, name, extras = {}) {
|
|
const qualifiedName = objcClassQualifiedName(name);
|
|
return {
|
|
kind: 'class',
|
|
declarationRole: 'implementation',
|
|
name,
|
|
qualifiedName,
|
|
nodeId: graphNodeId('Class', qualifiedName),
|
|
label: 'Class',
|
|
filePath,
|
|
startLine: 1,
|
|
endLine: 20,
|
|
protocols: extras.protocols ?? [],
|
|
...(extras.superclass !== undefined ? { superclass: extras.superclass } : {}),
|
|
};
|
|
}
|
|
|
|
function protocolContainer(filePath, name) {
|
|
const qualifiedName = objcProtocolQualifiedName(name);
|
|
return {
|
|
kind: 'protocol',
|
|
declarationRole: 'interface',
|
|
name,
|
|
qualifiedName,
|
|
nodeId: graphNodeId('Protocol', qualifiedName),
|
|
label: 'Protocol',
|
|
filePath,
|
|
startLine: 1,
|
|
endLine: 4,
|
|
protocols: [],
|
|
};
|
|
}
|
|
|
|
function methodFact(filePath, owner, selector, methodKind, startLine) {
|
|
const qualifiedName = objcMethodQualifiedName(owner.qualifiedName, methodKind, selector);
|
|
return {
|
|
name: selector,
|
|
selector,
|
|
methodKind,
|
|
ownerQualifiedName: owner.qualifiedName,
|
|
ownerName: owner.name,
|
|
ownerKind: owner.kind,
|
|
qualifiedName,
|
|
nodeId: graphNodeId('Method', qualifiedName),
|
|
filePath,
|
|
startLine,
|
|
endLine: startLine,
|
|
declarationRole: 'implementation',
|
|
parameterTypes: [],
|
|
parameterNames: [],
|
|
};
|
|
}
|
|
|
|
function messageFact(method, selector, receiverKind, receiverText, extras = {}) {
|
|
return {
|
|
selector,
|
|
receiverText,
|
|
receiverKind,
|
|
sourceMethodQualifiedName: method.qualifiedName,
|
|
sourceMethodId: method.nodeId,
|
|
sourceOwnerQualifiedName: method.ownerQualifiedName,
|
|
sourceOwnerName: method.ownerName,
|
|
sourceMethodKind: method.methodKind,
|
|
filePath: method.filePath,
|
|
startLine: extras.startLine ?? method.startLine + 1,
|
|
startCol: extras.startCol ?? 2,
|
|
...(extras.receiverType !== undefined ? { receiverType: extras.receiverType } : {}),
|
|
};
|
|
}
|
|
|
|
function parsedFile(filePath, facts) {
|
|
return Object.freeze({
|
|
filePath,
|
|
moduleScope: 0,
|
|
scopes: Object.freeze([]),
|
|
parsedImports: Object.freeze([]),
|
|
localDefs: Object.freeze([]),
|
|
referenceSites: Object.freeze([]),
|
|
captureSideChannel: Object.freeze({ kind: 'objective-c', facts }),
|
|
});
|
|
}
|
|
|
|
function seedGraph(graph, factsList) {
|
|
for (const facts of factsList) {
|
|
graph.addNode({
|
|
id: graphNodeId('File', facts.filePath),
|
|
label: 'File',
|
|
properties: {
|
|
name: facts.filePath,
|
|
qualifiedName: facts.filePath,
|
|
filePath: facts.filePath,
|
|
startLine: 1,
|
|
endLine: 1,
|
|
language: 'objective-c',
|
|
isExported: false,
|
|
},
|
|
});
|
|
for (const container of facts.containers) {
|
|
graph.addNode({
|
|
id: container.nodeId,
|
|
label: container.label,
|
|
properties: {
|
|
name: container.name,
|
|
qualifiedName: container.qualifiedName,
|
|
filePath: facts.filePath,
|
|
startLine: container.startLine,
|
|
endLine: container.endLine,
|
|
language: 'objective-c',
|
|
isExported: true,
|
|
},
|
|
});
|
|
}
|
|
for (const method of facts.methods) {
|
|
graph.addNode({
|
|
id: method.nodeId,
|
|
label: 'Method',
|
|
properties: {
|
|
name: method.selector,
|
|
qualifiedName: method.qualifiedName,
|
|
filePath: facts.filePath,
|
|
startLine: method.startLine,
|
|
endLine: method.endLine,
|
|
language: 'objective-c',
|
|
isExported: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function summarize(graph) {
|
|
let calls = 0;
|
|
let extendsEdges = 0;
|
|
let implementsEdges = 0;
|
|
let protocolCandidateUses = 0;
|
|
for (const rel of graph.iterRelationships()) {
|
|
if (rel.type === 'CALLS') calls++;
|
|
else if (rel.type === 'EXTENDS') extendsEdges++;
|
|
else if (rel.type === 'IMPLEMENTS') implementsEdges++;
|
|
else if (rel.type === 'USES' && String(rel.reason).startsWith('objc-protocol-candidate:')) {
|
|
protocolCandidateUses++;
|
|
}
|
|
}
|
|
return {
|
|
calls,
|
|
extends: extendsEdges,
|
|
implements: implementsEdges,
|
|
protocol_candidate_uses: protocolCandidateUses,
|
|
};
|
|
}
|
|
|
|
function spreadCorpus(classCount) {
|
|
const factsList = [];
|
|
const parsedFiles = [];
|
|
for (let i = 0; i < classCount; i++) {
|
|
const filePath = `src/Class${i}.m`;
|
|
const name = `Class${i}`;
|
|
const sibling = `Class${(i + 1) % classCount}`;
|
|
const owner = classContainer(filePath, name, i === 0 ? {} : { superclass: 'Class0' });
|
|
const run = methodFact(filePath, owner, 'run', '-', 4);
|
|
const tick = methodFact(filePath, owner, 'tick', '-', 8);
|
|
const messages = [
|
|
messageFact(tick, 'run', 'self', 'self', { startLine: 9, startCol: 2 }),
|
|
messageFact(tick, 'run', 'local', 'sib', {
|
|
startLine: 10,
|
|
startCol: 2,
|
|
receiverType: { kind: 'class', name: sibling, raw: `${sibling} *` },
|
|
}),
|
|
];
|
|
if (i > 0) {
|
|
messages.push(messageFact(tick, 'run', 'super', 'super', { startLine: 11, startCol: 2 }));
|
|
}
|
|
const facts = {
|
|
...emptyFacts(filePath),
|
|
containers: [owner],
|
|
methods: [run, tick],
|
|
messages,
|
|
};
|
|
factsList.push(facts);
|
|
parsedFiles.push(parsedFile(filePath, facts));
|
|
}
|
|
return { factsList, parsedFiles };
|
|
}
|
|
|
|
function protocolCorpus(classCount) {
|
|
const factsList = [];
|
|
const parsedFiles = [];
|
|
const protocolPath = 'src/Runnable.h';
|
|
const protocol = protocolContainer(protocolPath, 'Runnable');
|
|
const protocolRun = methodFact(protocolPath, protocol, 'run', '-', 2);
|
|
const protocolFacts = {
|
|
...emptyFacts(protocolPath),
|
|
containers: [protocol],
|
|
methods: [protocolRun],
|
|
};
|
|
factsList.push(protocolFacts);
|
|
parsedFiles.push(parsedFile(protocolPath, protocolFacts));
|
|
|
|
for (let i = 0; i < classCount; i++) {
|
|
const filePath = `src/Class${i}.m`;
|
|
const owner = classContainer(filePath, `Class${i}`, { protocols: ['Runnable'] });
|
|
const run = methodFact(filePath, owner, 'run', '-', 4);
|
|
const tick = methodFact(filePath, owner, 'tick:', '-', 8);
|
|
const facts = {
|
|
...emptyFacts(filePath),
|
|
containers: [owner],
|
|
methods: [run, tick],
|
|
messages: [
|
|
messageFact(tick, 'run', 'local', 'runner', {
|
|
startLine: 9,
|
|
startCol: 2,
|
|
receiverType: { kind: 'protocol', name: 'Runnable', raw: 'id<Runnable>' },
|
|
}),
|
|
],
|
|
};
|
|
factsList.push(facts);
|
|
parsedFiles.push(parsedFile(filePath, facts));
|
|
}
|
|
return { factsList, parsedFiles };
|
|
}
|
|
|
|
function run(shape, classCount) {
|
|
const corpus = shape === 'spread' ? spreadCorpus(classCount) : protocolCorpus(classCount);
|
|
const graph = createKnowledgeGraph();
|
|
seedGraph(graph, corpus.factsList);
|
|
objectiveCScopeResolver.emitPostResolutionEdges(graph, corpus.parsedFiles);
|
|
return summarize(graph);
|
|
}
|
|
|
|
function measure(shape, classCount) {
|
|
run(shape, classCount);
|
|
let bestMs = Infinity;
|
|
let counts = { calls: 0, extends: 0, implements: 0, protocol_candidate_uses: 0 };
|
|
for (let i = 0; i < REPS; i++) {
|
|
const start = performance.now();
|
|
counts = run(shape, classCount);
|
|
bestMs = Math.min(bestMs, performance.now() - start);
|
|
}
|
|
return {
|
|
classes: classCount,
|
|
...counts,
|
|
min_ms: Number(bestMs.toFixed(2)),
|
|
};
|
|
}
|
|
|
|
function scalingReport(small, large) {
|
|
const workloadRatio = large.classes / small.classes;
|
|
const scalingRatio = Number((large.min_ms / Math.max(small.min_ms, 0.01)).toFixed(3));
|
|
return {
|
|
small,
|
|
large,
|
|
workload_ratio: workloadRatio,
|
|
scaling_ratio: scalingRatio,
|
|
linear_factor: Number((scalingRatio / workloadRatio).toFixed(3)),
|
|
};
|
|
}
|
|
|
|
const report = {
|
|
spread: scalingReport(measure('spread', SMALL_CLASSES), measure('spread', LARGE_CLASSES)),
|
|
protocol: scalingReport(measure('protocol', SMALL_CLASSES), measure('protocol', LARGE_CLASSES)),
|
|
};
|
|
|
|
if (!process.argv.includes('--check')) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
process.exit(0);
|
|
}
|
|
|
|
const baseline = JSON.parse(readFileSync(join(HERE, 'baseline.json'), 'utf8'));
|
|
const failures = [];
|
|
const requirePositiveNumber = (path, value) => {
|
|
if (typeof value !== 'number' || !Number.isFinite(value) || !(value > 0)) {
|
|
failures.push(`${path}: expected a finite positive number, got ${JSON.stringify(value)}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
for (const shape of ['spread', 'protocol']) {
|
|
const measured = report[shape];
|
|
const expected = baseline[shape];
|
|
for (const arm of ['small', 'large']) {
|
|
for (const key of ['classes', 'calls', 'extends', 'implements', 'protocol_candidate_uses']) {
|
|
if (measured[arm][key] !== expected[arm][key]) {
|
|
failures.push(
|
|
`${shape}.${arm}.${key}: expected ${expected[arm][key]}, got ${measured[arm][key]}`,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
requirePositiveNumber(`${shape}.${arm}.ms_budget`, expected[arm].ms_budget) &&
|
|
measured[arm].min_ms > expected[arm].ms_budget
|
|
) {
|
|
failures.push(
|
|
`${shape}.${arm}.min_ms ${measured[arm].min_ms} exceeds budget ${expected[arm].ms_budget}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
requirePositiveNumber('spread.linear_scaling_slack', baseline.spread.linear_scaling_slack) &&
|
|
report.spread.linear_factor > baseline.spread.linear_scaling_slack
|
|
) {
|
|
failures.push(
|
|
`spread.linear_factor ${report.spread.linear_factor} exceeds slack ` +
|
|
`${baseline.spread.linear_scaling_slack} (runtime ${report.spread.scaling_ratio}x ` +
|
|
`for ${report.spread.workload_ratio}x work)`,
|
|
);
|
|
}
|
|
|
|
if (
|
|
requirePositiveNumber('protocol.linear_scaling_slack', baseline.protocol.linear_scaling_slack) &&
|
|
report.protocol.linear_factor > baseline.protocol.linear_scaling_slack
|
|
) {
|
|
failures.push(
|
|
`protocol.linear_factor ${report.protocol.linear_factor} exceeds slack ` +
|
|
`${baseline.protocol.linear_scaling_slack} (runtime ${report.protocol.scaling_ratio}x ` +
|
|
`for ${report.protocol.workload_ratio}x work)`,
|
|
);
|
|
}
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (failures.length > 0) {
|
|
console.error('[objective-c-resolution --check] FAIL');
|
|
for (const failure of failures) console.error(` - ${failure}`);
|
|
process.exit(1);
|
|
}
|
|
console.log('[objective-c-resolution --check] PASS');
|