Merge branch 'main' into feat/Desktop-app

This commit is contained in:
Sparsh 2026-05-15 00:31:34 +05:30 committed by GitHub
commit 9e23c80cab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 451 additions and 46 deletions

View file

@ -1,5 +1,5 @@
/**
* C++ argument-dependent lookup (ADL / Koenig lookup) V1.
* C++ argument-dependent lookup (ADL / Koenig lookup).
*
* When ordinary unqualified lookup fails for a free-call site, ADL also
* considers candidates declared in the **associated namespaces** of the
@ -13,17 +13,18 @@
* `using` anything. With V1 ADL: `audit::record` is discovered via
* `audit::Event`'s associated namespace.
*
* ## V1 boundary
* ## Current boundary
*
* V1 covers ONE associated-entity rule: an argument that's a directly-named
* The current implementation covers ONE associated-entity rule: an argument that's a directly-named
* class type (`audit::Event e`) contributes its **direct enclosing
* namespace** to the candidate set. Anything else pointer/reference
* namespace** to the candidate set. V2 extends that one step to
* pointer-typed class args (`audit::Event* p`, `audit::Event** pp`):
* they contribute the pointee class's enclosing namespace too. Reference
* arguments, function-pointer arguments, template specializations,
* base-class associated namespaces is V2 closure work and is
* deliberately excluded. The `cpp-adl-pointer-arg-boundary` fixture
* locks the exclusion in CI.
* base-class associated namespaces, and the rest of the full closure are
* still deliberately excluded.
*
* V1 also short-circuits to ADL only when ordinary lookup is empty
* The current implementation also short-circuits to ADL only when ordinary lookup is empty
* (`findCallableBindingInScope` returned undefined). ISO C++ would
* normally merge ADL candidates with ordinary-lookup candidates and
* run overload resolution over the union; V1 defers that merge to V2.
@ -60,16 +61,16 @@ import {
} from '../../scope-resolution/passes/overload-narrowing.js';
/**
* Per-argument shape information collected at capture time. ADL only
* fires for arguments where `simpleClassName !== ''` AND `!isPointer`
* AND `!isReference` i.e., directly-named class-type values.
* Per-argument shape information collected at capture time. ADL fires for
* arguments where `simpleClassName !== ''` AND `!isReference`, including
* class pointers whose declarator chain resolves to a named class type.
*/
export interface CppAdlArgInfo {
/** Simple class-like type name (last segment of qualified name); empty
* for primitives, literals, function pointers, template specs, etc. */
readonly simpleClassName: string;
/** True when the variable's declarator was a `pointer_declarator`. V1
* excludes pointer-typed args (closure rules deferred to V2). */
/** True when the variable's declarator contained one or more
* `pointer_declarator` wrappers. */
readonly isPointer: boolean;
/** True when the variable's declarator was a `reference_declarator`. */
readonly isReference: boolean;
@ -151,8 +152,8 @@ export function populateCppAssociatedNamespaces(parsed: ParsedFile): void {
*
* Fires only when:
* - the call site is not in `noAdlSites` (parenthesized form), AND
* - at least one argument is a directly-named class type (not pointer,
* not reference, not literal/primitive).
* - at least one argument resolves to a named class type (value or
* pointer, but not reference, function pointer, literal, or primitive).
*/
export function pickCppAdlCandidates(
site: {
@ -170,11 +171,11 @@ export function pickCppAdlCandidates(
const args = argInfoBySite.get(key);
if (args === undefined || args.length === 0) return undefined;
// Collect associated namespace QNames from every value-class-typed arg.
// Collect associated namespace QNames from every participating class-typed arg.
const associatedNamespaces = new Set<string>();
for (const arg of args) {
if (arg.simpleClassName === '') continue;
if (arg.isPointer || arg.isReference) continue;
if (arg.isReference) continue;
const classDef = findCppClassDefBySimpleName(arg.simpleClassName, scopes);
if (classDef === undefined) continue;
const nsQName = classToNamespaceQualifiedName.get(classDef.nodeId);

View file

@ -720,14 +720,15 @@ function isParenthesizedFunctionCall(callNode: SyntaxNode): boolean {
/**
* Per-argument ADL classification: walk each argument of a free call and
* decide whether it's a directly-named class type (V1 ADL fires) or
* something V1 excludes (pointer, reference, primitive, literal, function
* pointer, template specialization).
* decide whether it resolves to a directly-named class or class-pointer
* type (ADL fires) or to an excluded shape such as a reference, function
* pointer, primitive, literal, or template specialization.
*
* V1 only fires for value class-typed args: `void f(N::S); N::S s; f(s);`.
* Pointer args (`N::S* p; f(p);`) intentionally return `simpleClassName=''`
* to lock the V1 boundary the `cpp-adl-pointer-arg-boundary` fixture
* regression-tests this.
* Class-typed values and class pointers (`N::S`, `N::S*`, `N::S**`) all
* preserve the pointee class name for associated-namespace lookup.
* Function pointers remain excluded even when their return type names a
* class, because the associated entity is the pointed-to function type,
* not the return type.
*/
function inferCppCallAdlArgs(callNode: SyntaxNode): CppAdlArgInfo[] {
const argList = callNode.childForFieldName('arguments');
@ -792,15 +793,23 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
// Unwrap declarator chain to find pointer/reference markers and the
// variable name. `init_declarator > pointer_declarator > identifier`
// means pointer-typed; `init_declarator > reference_declarator > ...`
// means reference-typed; bare `init_declarator > identifier` is value.
// means pointer-typed; repeated pointer wrappers still count as pointer
// typed; `init_declarator > reference_declarator > ...` means
// reference-typed; bare `init_declarator > identifier` is value.
// Function-pointer wrappers (`pointer_declarator > function_declarator`)
// must not contribute ADL associated namespaces.
let isPointer = false;
let isReference = false;
let isFunctionPointer = false;
let inner: SyntaxNode = declarator;
let nameText: string | null = null;
let safety = 16; // bound walk depth defensively
while (safety-- > 0) {
if (inner.type === 'pointer_declarator') {
if (findFirstDescendantOfType(inner, 'function_declarator') !== null) {
isFunctionPointer = true;
break;
}
isPointer = true;
const next = inner.childForFieldName('declarator');
if (next === null) break;
@ -828,11 +837,15 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
inner = next;
continue;
}
if (inner.type === 'function_declarator') {
isFunctionPointer = true;
break;
}
// Reached the leaf — usually `identifier`. Take its text.
nameText = inner.text;
break;
}
if (nameText !== varName) continue;
if (isFunctionPointer || nameText !== varName) continue;
const simpleClassName = extractAdlSimpleTypeName(typeNode);
return { simpleClassName, isPointer, isReference };
@ -841,9 +854,9 @@ function lookupAdlIdentifierType(identNode: SyntaxNode): CppAdlArgInfo {
}
/** Extract the simple class-like type name from a `type:` field node.
* Returns '' for primitives, template specializations, function pointers,
* and any other shape V1 ADL doesn't support those args are excluded
* from associated-namespace closure. */
* Returns '' for primitives, template specializations, and any other
* unsupported type-only shape. Function pointers are filtered at the
* declarator level in `lookupAdlIdentifierType`. */
function extractAdlSimpleTypeName(typeNode: SyntaxNode): string {
if (typeNode.type === 'primitive_type') return '';
if (typeNode.type === 'sized_type_specifier') return '';

View file

@ -36,6 +36,10 @@ import {
resolveCppQualifiedNamespaceMember,
} from './inline-namespaces.js';
import { populateCppRangeBindings } from './range-bindings.js';
import {
isOverloadAmbiguousAfterNormalization,
narrowOverloadCandidates,
} from '../../scope-resolution/passes/overload-narrowing.js';
/**
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
@ -178,6 +182,8 @@ export const cppScopeResolver: ScopeResolver = {
// for cross-file propagation and compound-receiver chain resolution.
// cppBindingScopeFor hoists @type-binding.return to Module scope.
hoistTypeBindingsToModule: true,
// Enable receiver-bound explicit-`this` fallback only for C++.
resolveThisViaEnclosingClass: true,
// The `isFileLocalDef` hook on the global free-call fallback names
// file-local linkage historically, but semantically gates "logically
// invisible cross-file" defs. C++ extends this to also reject class-
@ -219,6 +225,33 @@ export const cppScopeResolver: ScopeResolver = {
// V1 limitation: only direct enclosing-namespace closure for value
// class-typed args; pointer/reference/template-spec args excluded.
resolveAdlCandidates: (site, callerParsed, scopes, parsedFiles) => {
// `using ns::name;` introduces `name` into ordinary unqualified lookup.
// For template-class method bodies, lexical scope walks can miss this
// named-using visibility; recover by resolving the imported namespace
// member directly when the local call name matches a named using import.
const usingNamedHits: SymbolDefinition[] = [];
const seenUsing = new Set<string>();
for (const imp of callerParsed.parsedImports) {
if (imp.kind !== 'named') continue;
if (imp.localName !== site.name) continue;
const member = resolveCppQualifiedNamespaceMember(
imp.targetRaw,
imp.importedName,
parsedFiles,
scopes,
);
if (member === undefined) continue;
if (seenUsing.has(member.nodeId)) continue;
seenUsing.add(member.nodeId);
usingNamedHits.push(member);
}
if (usingNamedHits.length > 0) {
const narrowed = narrowOverloadCandidates(usingNamedHits, site.arity, site.argumentTypes);
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) return 'ambiguous';
if (narrowed.length === 1) return narrowed[0];
if (narrowed.length > 1) return 'ambiguous';
}
const result = pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles);
if (result === ADL_AMBIGUOUS) return 'ambiguous';
return result;

View file

@ -627,6 +627,18 @@ export interface ScopeResolver {
parsedFiles: readonly ParsedFile[],
) => SymbolDefinition | undefined;
/**
* Enable the receiver-bound Case 0.5 fallback for explicit `this`
* receivers (`this->m()` / `this.m()`) that resolves against the
* enclosing class + MRO even when no explicit `this` typeBinding is
* present in scope.
*
* Keep disabled for languages where the existing type-binding path
* (Case 4) already handles `this` correctly and overload ambiguity
* suppression must remain unchanged.
*/
readonly resolveThisViaEnclosingClass?: boolean;
/**
* Optional post-finalize hook to inject cross-file bindings that
* aren't modeled via explicit imports. Runs after

View file

@ -72,6 +72,7 @@ type ReceiverBoundProviderSubset = Pick<
| 'unwrapCollectionAccessor'
| 'hoistTypeBindingsToModule'
| 'resolveQualifiedReceiverMember'
| 'resolveThisViaEnclosingClass'
>;
function normalizeTemplateArgToken(value: string): string {
@ -321,6 +322,88 @@ export function emitReceiverBoundCalls(
}
}
// ── Case 0.5: implicit `this` receiver ───────────────────────
// C++ `this->member()` (and same-shape receivers in other OO
// languages) should resolve against the enclosing class + MRO
// even when there is no explicit `this` typeBinding in scope.
if (provider.resolveThisViaEnclosingClass === true && receiverName === 'this') {
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
if (enclosingClass !== undefined) {
const chain = [
enclosingClass.nodeId,
...scopes.methodDispatch.mroFor(enclosingClass.nodeId),
];
let memberDef: SymbolDefinition | undefined;
let ambiguous = false;
let hiddenByName = false;
for (const ownerId of chain) {
const methodOverloads = model.methods.lookupAllByOwner(ownerId, memberName);
if (methodOverloads.length > 0) {
const narrowed = narrowOverloadCandidates(
methodOverloads,
site.arity,
site.argumentTypes,
);
if (isOverloadAmbiguousAfterNormalization(narrowed, site.arity)) {
ambiguous = true;
break;
}
if (narrowed.length === 0) {
// C++ name hiding: if the derived class declares `f`, base-class
// overloads named `f` are hidden for member lookup
// ([basic.lookup.classref]). A non-viable derived overload set
// therefore terminates lookup instead of falling through to base.
hiddenByName = true;
break;
}
memberDef = narrowed[0] ?? methodOverloads[0];
break;
}
// Field/property lookup intentionally runs only after the method
// lookup above: in C++ member-name lookup, functions with this
// name hide same-named base members; we therefore prefer method
// candidates first and only target a field when no methods with
// this name exist on the current owner.
memberDef = model.fields.lookupFieldByOwner(ownerId, memberName);
if (memberDef !== undefined) {
break;
}
}
if (ambiguous) {
handledSites.add(siteKey);
continue;
}
if (hiddenByName) {
handledSites.add(siteKey);
continue;
}
if (memberDef !== undefined) {
const reason =
site.kind === 'write' || site.kind === 'read'
? site.kind
: memberDef.filePath !== parsed.filePath
? 'import-resolved'
: 'global';
const confidence = site.kind === 'write' || site.kind === 'read' ? 1.0 : 0.85;
const ok = tryEmitEdge(
graph,
scopes,
nodeLookup,
site,
memberDef,
reason,
seen,
confidence,
collapse,
);
if (ok) emitted++;
handledSites.add(siteKey);
continue;
}
}
}
// ── Case 1: namespace receiver ───────────────────────────────
const targetFiles = namespaceTargets.get(receiverName);
if (targetFiles !== undefined) {

View file

@ -0,0 +1,8 @@
#include "audit.h"
namespace app {
void run() {
void (*g)();
record(g);
}
}

View file

@ -0,0 +1,5 @@
#pragma once
namespace audit {
void record(void (*g)());
}

View file

@ -0,0 +1,9 @@
#include "audit.h"
namespace app {
void run() {
void (*fp)();
audit::Event e;
record(e);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace audit {
struct Event {};
void record(Event e);
}

View file

@ -0,0 +1,8 @@
#include "audit.h"
namespace app {
void run() {
audit::Event (*factory)();
record(factory);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace audit {
struct Event {};
void record(Event (*factory)());
}

View file

@ -0,0 +1,8 @@
#include "audit.h"
namespace app {
void run() {
audit::Event** pp;
record(pp);
}
}

View file

@ -0,0 +1,6 @@
#pragma once
namespace audit {
struct Event {};
void record(Event** e);
}

View file

@ -3,9 +3,12 @@
#include "base.h"
#include "helpers.h"
using utils::ns_helper_2;
template<class T>
struct D : Base<T> {
void g() {
utils::ns_helper();
ns_helper_2();
}
};

View file

@ -2,4 +2,5 @@
namespace utils {
void ns_helper();
void ns_helper_2();
}

View file

@ -0,0 +1,6 @@
#pragma once
template<class T>
struct Base {
void f();
};

View file

@ -0,0 +1,14 @@
#pragma once
#include "base.h"
template<class T>
struct Derived : Base<T> {
void g_unqualified() {
f();
}
void g_this() {
this->f();
}
};

View file

@ -0,0 +1,6 @@
#pragma once
template<class T>
struct Base {
void f();
};

View file

@ -0,0 +1,16 @@
#pragma once
#include "base.h"
template<class T>
struct Derived : Base<T> {
void f(int);
void g() {
this->f();
}
void g_ok() {
this->f(42);
}
};

View file

@ -3,5 +3,6 @@
template<class T>
struct Base {
void f();
void base_method();
int i;
};

View file

@ -7,6 +7,9 @@ struct Derived : Base<T> {
void g() {
this->f();
}
void k() {
this->base_method();
}
int h() {
return this->i;
}

View file

@ -1972,14 +1972,100 @@ describe('C++ two-phase template lookup — dependent base suppression', () => {
});
});
// NOTE: positive guards (this->f() resolves, non-dependent-base unqualified
// f() resolves, namespace-qualified utils::ns_helper() resolves) inside
// template bodies are documented gaps in C++ template-context resolution
// independent of U3's dependent-base suppression. The U3 core asserts only
// the negative behavior (dependent-base members are NOT bound by unqualified
// calls); the positive cases would require additional `this` type-binding
// and template-body member-lookup work tracked separately. See plan
// 2026-05-13-001 follow-ups.
describe('C++ two-phase template lookup — positive this-qualified calls', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-two-phase-this-qualified'),
() => {},
);
}, 60000);
it('Derived<T>::g() -> this->f() resolves to f (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const thisCalls = calls.filter((c) => c.source === 'g' && c.target === 'f');
expect(thisCalls.length).toBe(1);
expect(thisCalls[0].targetFilePath).toContain('base.h');
});
it('Derived<T>::k() -> this->base_method() resolves via EXTENDS chain (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const inheritedCalls = calls.filter((c) => c.source === 'k' && c.target === 'base_method');
expect(inheritedCalls.length).toBe(1);
expect(inheritedCalls[0].targetFilePath).toContain('base.h');
});
});
describe('C++ two-phase template lookup — paired unqualified + this-qualified in one fixture', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-two-phase-paired'), () => {});
}, 60000);
it('Derived<T>::g_unqualified() -> f() does NOT bind to Base<T>::f', () => {
const calls = getRelationships(result, 'CALLS');
const leaks = calls.filter((c) => c.source === 'g_unqualified' && c.target === 'f');
expect(leaks.length).toBe(0);
});
it('Derived<T>::g_this() -> this->f() resolves to Base<T>::f (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const resolved = calls.filter((c) => c.source === 'g_this' && c.target === 'f');
expect(resolved.length).toBe(1);
expect(resolved[0].targetFilePath).toContain('base.h');
});
});
describe('C++ two-phase template lookup — namespace calls inside template body', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-two-phase-namespace-free-call-inside-template'),
() => {},
);
}, 60000);
it('D<T>::g() -> utils::ns_helper() resolves (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const qualifiedCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper');
expect(qualifiedCalls.length).toBe(1);
expect(qualifiedCalls[0].targetFilePath).toContain('helpers.h');
});
it('D<T>::g() -> ns_helper_2() resolves after using-declaration (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const usingCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper_2');
expect(usingCalls.length).toBe(1);
expect(usingCalls[0].targetFilePath).toContain('helpers.h');
});
});
describe('C++ two-phase template lookup — this-> name-hiding arity mismatch', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-two-phase-this-name-hiding-arity'),
() => {},
);
}, 60000);
it('Derived<T>::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible', () => {
const calls = getRelationships(result, 'CALLS');
const fCalls = calls.filter((c) => c.source === 'g' && c.target === 'f');
expect(fCalls.length).toBe(0);
});
it('Derived<T>::g_ok() -> this->f(42) resolves to derived overload (1 edge)', () => {
const calls = getRelationships(result, 'CALLS');
const fCalls = calls.filter((c) => c.source === 'g_ok' && c.target === 'f');
expect(fCalls.length).toBe(1);
expect(fCalls[0].targetFilePath).toContain('derived.h');
});
});
// ---------------------------------------------------------------------------
// U3 cross-file namespace variant: Base lives in a different file AND
@ -2057,7 +2143,7 @@ describe('C++ ADL — parenthesized name suppresses ADL', () => {
});
});
describe('C++ ADL — pointer-arg V1 boundary', () => {
describe('C++ ADL — pointer arg unwrapping', () => {
let result: PipelineResult;
beforeAll(async () => {
@ -2067,19 +2153,81 @@ describe('C++ ADL — pointer-arg V1 boundary', () => {
);
}, 60000);
it('record(p) where p is audit::Event* emits zero CALLS — V1 ADL excludes pointer args', () => {
it('record(p) where p is audit::Event* resolves to audit::record via ADL', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
});
describe('C++ ADL — function pointer args do not participate', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-function-pointer-arg'),
() => {},
);
}, 60000);
it('record(g) where g is void (*)() emits zero CALLS edges', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// Exact .toBe(0): V1 ADL covers only directly-named class-type values
// (per plan 2026-05-13-001 R4). Pointer-typed args fall under
// associated-entity closure rules deferred to V2. This fixture locks
// the boundary in CI so the implementer cannot accidentally extend
// V1 to include pointer types. Real ISO C++ would resolve via V2
// closure; matching that requires the V2 follow-up plan.
expect(recordCalls.length).toBe(0);
});
});
describe('C++ ADL — preceding function-pointer declarations do not block class args', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-function-pointer-before-class-arg'),
() => {},
);
}, 60000);
it('record(e) still resolves via ADL when an earlier declaration is void (*)()', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
});
describe('C++ ADL — class-returning function pointer args do not participate', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-function-pointer-class-return-arg'),
() => {},
);
}, 60000);
it('record(factory) where factory is audit::Event (*)() emits zero CALLS edges', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
expect(recordCalls.length).toBe(0);
});
});
describe('C++ ADL — pointer-to-pointer args participate', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-pointer-to-pointer'), () => {});
}, 60000);
it('record(pp) where pp is audit::Event** resolves to audit::record via ADL', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
});
describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => {
let result: PipelineResult;

View file

@ -168,6 +168,15 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'emits distinct Class nodes for List<User> and List<Order>',
'callSave() in each specialization resolves to its own save()',
'save specialization bodies route to their own sibling method',
// PR #1590 follow-up: explicit `this->` resolution in template class
// bodies and paired two-phase assertions are scope-resolver-only.
// Legacy DAG lacks this receiver-bound template semantics and
// dependent-base suppression parity for these shapes.
'Derived<T>::g() -> this->f() resolves to f (1 edge)',
'Derived<T>::k() -> this->base_method() resolves via EXTENDS chain (1 edge)',
'Derived<T>::g_unqualified() -> f() does NOT bind to Base<T>::f',
'Derived<T>::g_this() -> this->f() resolves to Base<T>::f (1 edge)',
'Derived<T>::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible',
]),
};