mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(cpp): Expand type_traits constraint registry (#1648)
This commit is contained in:
parent
2632bcccc0
commit
5f0c0eba0e
18 changed files with 659 additions and 56 deletions
|
|
@ -21,6 +21,7 @@
|
|||
* (defined in `./types.ts`).
|
||||
*/
|
||||
|
||||
import type { ParameterTypeClass } from './symbol-definition.js';
|
||||
import type { Range, ScopeId } from './types.js';
|
||||
|
||||
/**
|
||||
|
|
@ -79,4 +80,11 @@ export interface ReferenceSite {
|
|||
* (C#: `42` → `'int'`, `"alice"` → `'string'`).
|
||||
*/
|
||||
readonly argumentTypes?: readonly string[];
|
||||
/**
|
||||
* Optional per-argument type-shape sidecar for languages that need
|
||||
* cv/ref/pointer distinctions during constraint filtering. This is
|
||||
* intentionally separate from `argumentTypes`, which stays normalized
|
||||
* for existing overload narrowing and conversion-rank logic.
|
||||
*/
|
||||
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
*/
|
||||
|
||||
import type { NodeLabel } from '../../graph/types.js';
|
||||
import type { SymbolDefinition } from '../symbol-definition.js';
|
||||
import type { ParameterTypeClass, SymbolDefinition } from '../symbol-definition.js';
|
||||
import type { Callsite, DefId } from '../types.js';
|
||||
import type { DefIndex } from '../def-index.js';
|
||||
import type { QualifiedNameIndex } from '../qualified-name-index.js';
|
||||
|
|
@ -65,6 +65,13 @@ export interface ConstraintContext {
|
|||
* `narrowOverloadCandidates`' `argTypes` parameter.
|
||||
*/
|
||||
readonly argumentTypes?: readonly string[];
|
||||
/**
|
||||
* Optional shape-preserving sidecar aligned with `argumentTypes`.
|
||||
* Unknown or unsupported slots should be omitted by producers or
|
||||
* marked with `indirection: 'unknown'`; consumers must preserve the
|
||||
* monotonic fallback and return 'unknown' instead of guessing.
|
||||
*/
|
||||
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
|
||||
}
|
||||
|
||||
// ─── Owner-scoped contributor (concrete shape for `RegistryContributor`) ────
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import type { Capture, CaptureMatch, ParameterTypeClass } from 'gitnexus-shared';
|
||||
import {
|
||||
findNodeAtRange,
|
||||
nodeToCapture,
|
||||
|
|
@ -9,7 +9,11 @@ import { getCppParser, getCppScopeQuery } from './query.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { splitCppInclude, splitCppUsingDecl } from './import-decomposer.js';
|
||||
import { computeCppDeclarationArity, computeCppCallArity } from './arity-metadata.js';
|
||||
import {
|
||||
classifyCppParameterType,
|
||||
computeCppDeclarationArity,
|
||||
computeCppCallArity,
|
||||
} from './arity-metadata.js';
|
||||
import { markCppAnonymousNamespaceRange, markFileLocal } from './file-local-linkage.js';
|
||||
import { markCppDependentBase } from './two-phase-lookup.js';
|
||||
import { markCppAdlSiteArgs, markCppAdlSiteNoAdl, type CppAdlArgInfo } from './adl.js';
|
||||
|
|
@ -217,6 +221,14 @@ export function emitCppScopeCaptures(
|
|||
JSON.stringify(argTypes),
|
||||
);
|
||||
}
|
||||
const argTypeClasses = inferCppCallArgTypeClasses(cNode);
|
||||
if (argTypeClasses !== undefined && argTypeClasses.length > 0) {
|
||||
grouped['@reference.parameter-type-classes'] = syntheticCapture(
|
||||
'@reference.parameter-type-classes',
|
||||
cNode,
|
||||
JSON.stringify(argTypeClasses),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -683,6 +695,35 @@ function inferCppCallArgTypes(node: SyntaxNode): string[] | undefined {
|
|||
return types.length > 0 ? types : undefined;
|
||||
}
|
||||
|
||||
function inferCppCallArgTypeClasses(node: SyntaxNode): ParameterTypeClass[] | undefined {
|
||||
const argList = node.childForFieldName('arguments');
|
||||
if (argList === null) return undefined;
|
||||
|
||||
const classes: ParameterTypeClass[] = [];
|
||||
for (let i = 0; i < argList.childCount; i++) {
|
||||
const child = argList.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === ',' || child.type === '(' || child.type === ')') continue;
|
||||
const litType = inferCppLiteralType(child);
|
||||
if (litType !== '') {
|
||||
classes.push(valueTypeClass(litType));
|
||||
} else if (child.type === 'identifier') {
|
||||
classes.push(lookupDeclaredTypeClassForIdentifier(child));
|
||||
} else {
|
||||
classes.push(unknownTypeClass('unknown'));
|
||||
}
|
||||
}
|
||||
return classes.length > 0 ? classes : undefined;
|
||||
}
|
||||
|
||||
function valueTypeClass(base: string): ParameterTypeClass {
|
||||
return { base, cv: 'none', indirection: 'value', pointerDepth: 0 };
|
||||
}
|
||||
|
||||
function unknownTypeClass(base: string): ParameterTypeClass {
|
||||
return { base, cv: 'unknown', indirection: 'unknown', pointerDepth: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the canonical type name of a C++ literal AST node.
|
||||
* Returns empty string for non-literal / unknown nodes.
|
||||
|
|
@ -750,6 +791,9 @@ function lookupDeclaredTypeForIdentifier(identNode: SyntaxNode): string {
|
|||
}
|
||||
if (scope === null) return '';
|
||||
|
||||
const paramType = lookupFunctionParameterType(scope, varName);
|
||||
if (paramType !== '') return paramType;
|
||||
|
||||
// Scan declarations in the scope for a matching variable name
|
||||
for (let i = 0; i < scope.childCount; i++) {
|
||||
const stmt = scope.child(i);
|
||||
|
|
@ -763,18 +807,118 @@ function lookupDeclaredTypeForIdentifier(identNode: SyntaxNode): string {
|
|||
// Check init_declarator children for the variable name
|
||||
const declarator = stmt.childForFieldName('declarator');
|
||||
if (declarator === null) continue;
|
||||
if (declarator.type === 'init_declarator') {
|
||||
const nameChild = declarator.childForFieldName('declarator');
|
||||
if (nameChild !== null && nameChild.text === varName) {
|
||||
return normalizeCppTypeText(typeNode.text);
|
||||
}
|
||||
} else if (declarator.text === varName) {
|
||||
const nameChild = declaredNameNode(declarator);
|
||||
if (nameChild !== null && extractDeclaratorLeafName(nameChild) === varName) {
|
||||
return normalizeCppTypeText(typeNode.text);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function lookupDeclaredTypeClassForIdentifier(identNode: SyntaxNode): ParameterTypeClass {
|
||||
const varName = identNode.text;
|
||||
let scope: SyntaxNode | null = identNode.parent;
|
||||
while (
|
||||
scope !== null &&
|
||||
scope.type !== 'compound_statement' &&
|
||||
scope.type !== 'translation_unit'
|
||||
) {
|
||||
scope = scope.parent;
|
||||
}
|
||||
if (scope === null) return unknownTypeClass('unknown');
|
||||
|
||||
const paramTypeClass = lookupFunctionParameterTypeClass(scope, varName, identNode);
|
||||
if (paramTypeClass !== undefined) return paramTypeClass;
|
||||
|
||||
for (let i = 0; i < scope.childCount; i++) {
|
||||
const stmt = scope.child(i);
|
||||
if (stmt === null || stmt.type !== 'declaration') continue;
|
||||
|
||||
const typeNode = stmt.childForFieldName('type');
|
||||
if (typeNode === null) continue;
|
||||
if (typeNode.type === 'placeholder_type_specifier') continue;
|
||||
|
||||
const declarator = stmt.childForFieldName('declarator');
|
||||
if (declarator === null) continue;
|
||||
const nameChild = declaredNameNode(declarator);
|
||||
if (nameChild === null || extractDeclaratorLeafName(nameChild) !== varName) continue;
|
||||
|
||||
const typeClass = classifyCppParameterType(
|
||||
typeNode.text,
|
||||
nameChild.text,
|
||||
stmt.text.replace(/;\s*$/, ''),
|
||||
);
|
||||
if (isKnownEnumName(identNode, typeClass.base)) {
|
||||
return { ...typeClass, base: `enum:${typeClass.base}` };
|
||||
}
|
||||
return typeClass;
|
||||
}
|
||||
return unknownTypeClass('unknown');
|
||||
}
|
||||
|
||||
function lookupFunctionParameterType(scope: SyntaxNode, varName: string): string {
|
||||
const param = findEnclosingFunctionParameter(scope, varName);
|
||||
if (param === null) return '';
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode === null) return '';
|
||||
return normalizeCppTypeText(typeNode.text);
|
||||
}
|
||||
|
||||
function lookupFunctionParameterTypeClass(
|
||||
scope: SyntaxNode,
|
||||
varName: string,
|
||||
identNode: SyntaxNode,
|
||||
): ParameterTypeClass | undefined {
|
||||
const param = findEnclosingFunctionParameter(scope, varName);
|
||||
if (param === null) return undefined;
|
||||
const typeNode = param.childForFieldName('type');
|
||||
if (typeNode === null) return undefined;
|
||||
const declarator = param.childForFieldName('declarator');
|
||||
if (declarator === null) return undefined;
|
||||
const typeClass = classifyCppParameterType(typeNode.text, declarator.text, param.text);
|
||||
if (isKnownEnumName(identNode, typeClass.base)) {
|
||||
return { ...typeClass, base: `enum:${typeClass.base}` };
|
||||
}
|
||||
return typeClass;
|
||||
}
|
||||
|
||||
function findEnclosingFunctionParameter(scope: SyntaxNode, varName: string): SyntaxNode | null {
|
||||
let node: SyntaxNode | null = scope.parent;
|
||||
while (node !== null) {
|
||||
if (node.type === 'function_definition' || node.type === 'function_declarator') {
|
||||
const fnDecl =
|
||||
node.type === 'function_declarator'
|
||||
? node
|
||||
: findFirstDescendantOfType(node, 'function_declarator');
|
||||
const params = fnDecl?.childForFieldName('parameters') ?? null;
|
||||
if (params !== null) {
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const param = params.namedChild(i);
|
||||
if (param === null || param.type !== 'parameter_declaration') continue;
|
||||
const declarator = param.childForFieldName('declarator');
|
||||
if (declarator !== null && extractDeclaratorLeafName(declarator) === varName) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function declaredNameNode(declarator: SyntaxNode): SyntaxNode | null {
|
||||
if (declarator.type !== 'init_declarator') return declarator;
|
||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||
const child = declarator.namedChild(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'identifier') return child;
|
||||
if (child.type.endsWith('_declarator')) return child;
|
||||
}
|
||||
return declarator.childForFieldName('declarator');
|
||||
}
|
||||
|
||||
/** Normalize a type-specifier text for argument type matching.
|
||||
* Strips qualifiers (const, volatile), namespace prefixes (std::),
|
||||
* and pointer/reference markers. */
|
||||
|
|
@ -786,6 +930,25 @@ function normalizeCppTypeText(text: string): string {
|
|||
return t;
|
||||
}
|
||||
|
||||
function isKnownEnumName(node: SyntaxNode, typeName: string): boolean {
|
||||
if (typeName === '' || typeName === 'unknown') return false;
|
||||
let root: SyntaxNode = node;
|
||||
while (root.parent !== null) root = root.parent;
|
||||
const stack: SyntaxNode[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const cur = stack.pop()!;
|
||||
if (cur.type === 'enum_specifier') {
|
||||
const name = cur.childForFieldName('name');
|
||||
if (name?.text === typeName) return true;
|
||||
}
|
||||
for (let i = 0; i < cur.childCount; i++) {
|
||||
const child = cur.child(i);
|
||||
if (child !== null) stack.push(child);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether a `namespace_definition` AST node is inline.
|
||||
* Tree-sitter-cpp exposes the `inline` keyword as an anonymous child
|
||||
|
|
@ -1247,7 +1410,9 @@ function extractDeclaratorLeafName(node: SyntaxNode): string | null {
|
|||
const next =
|
||||
cur.childForFieldName('declarator') ??
|
||||
// parenthesized_declarator: single named child
|
||||
(cur.type === 'parenthesized_declarator' ? cur.namedChild(0) : null);
|
||||
(cur.type === 'parenthesized_declarator' || cur.type.endsWith('_declarator')
|
||||
? cur.namedChild(0)
|
||||
: null);
|
||||
if (next === null) return null;
|
||||
cur = next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,20 +20,27 @@
|
|||
* NOT: flip compatible↔incompatible; pass through unknown.
|
||||
*/
|
||||
|
||||
import type { ArityVerdict, Callsite, ConstraintContext, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type {
|
||||
ArityVerdict,
|
||||
Callsite,
|
||||
ConstraintContext,
|
||||
ParameterTypeClass,
|
||||
SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
import { classifyType, type TypeClass } from './type-classifier.js';
|
||||
import type { ConstraintExpr, CppConstraintPayload } from './constraint-extractor.js';
|
||||
|
||||
type AtomicEvaluator = (argClasses: readonly TypeClass[]) => ArityVerdict;
|
||||
interface ConstraintArgClass {
|
||||
readonly typeClass: TypeClass;
|
||||
readonly shape?: ParameterTypeClass;
|
||||
}
|
||||
|
||||
type AtomicEvaluator = (args: readonly ConstraintArgClass[]) => ArityVerdict;
|
||||
|
||||
/**
|
||||
* Curated Tier-A predicate registry — the four canonical
|
||||
* `<type_traits>` variable templates whose truth tables are closed-form
|
||||
* over our coarse `TypeClass` enum.
|
||||
*
|
||||
* Deferred predicates that need a cv/ref/pointer sidecar on
|
||||
* `normalizeCppParamType` (today the normalizer strips those markers
|
||||
* before storage) live in #1579 as one-line follow-up adds.
|
||||
* Curated Tier-A predicate registry. Predicates that depend on pointer,
|
||||
* reference, or cv shape consult `ConstraintContext.argumentTypeClasses`.
|
||||
* Missing or unsupported shape returns 'unknown' to preserve monotonicity.
|
||||
*/
|
||||
// ISO `<type_traits>` treats `bool`, `char`, and the signed/unsigned char
|
||||
// variants as integral types (§21.3.4 Table 48), so `is_integral_v<bool>`
|
||||
|
|
@ -46,30 +53,135 @@ function isIntegralClass(c: TypeClass | undefined): boolean {
|
|||
}
|
||||
|
||||
const REGISTRY = new Map<string, AtomicEvaluator>([
|
||||
['is_integral_v', (cls) => verdictFromBool(isIntegralClass(cls[0]), cls)],
|
||||
['is_floating_point_v', (cls) => verdictFromBool(cls[0] === 'floating', cls)],
|
||||
[
|
||||
'is_void_v',
|
||||
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'void'),
|
||||
],
|
||||
[
|
||||
'is_integral_v',
|
||||
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && isIntegralClass(arg.typeClass)),
|
||||
],
|
||||
[
|
||||
'is_floating_point_v',
|
||||
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'floating'),
|
||||
],
|
||||
[
|
||||
'is_arithmetic_v',
|
||||
(cls) => verdictFromBool(isIntegralClass(cls[0]) || cls[0] === 'floating', cls),
|
||||
(args) =>
|
||||
unaryVerdict(
|
||||
args,
|
||||
(arg) =>
|
||||
isPlainValue(arg) && (isIntegralClass(arg.typeClass) || arg.typeClass === 'floating'),
|
||||
),
|
||||
],
|
||||
[
|
||||
'is_enum_v',
|
||||
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'enum'),
|
||||
],
|
||||
[
|
||||
'is_class_v',
|
||||
(args) => unaryVerdict(args, (arg) => isPlainValue(arg) && arg.typeClass === 'class'),
|
||||
],
|
||||
[
|
||||
'is_pointer_v',
|
||||
(args) =>
|
||||
unaryShapeVerdict(args, (shape) => shape.indirection === 'pointer' && shape.pointerDepth > 0),
|
||||
],
|
||||
[
|
||||
'is_reference_v',
|
||||
(args) =>
|
||||
unaryShapeVerdict(
|
||||
args,
|
||||
(shape) => shape.indirection === 'lvalue-ref' || shape.indirection === 'rvalue-ref',
|
||||
),
|
||||
],
|
||||
[
|
||||
'is_const_v',
|
||||
(args) =>
|
||||
unaryShapeVerdict(args, (shape) => shape.cv === 'const' || shape.cv === 'const volatile', {
|
||||
requireTopLevelCv: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'is_volatile_v',
|
||||
(args) =>
|
||||
unaryShapeVerdict(args, (shape) => shape.cv === 'volatile' || shape.cv === 'const volatile', {
|
||||
requireTopLevelCv: true,
|
||||
}),
|
||||
],
|
||||
// NOTE: cv-qualifiers are stripped by `normalizeCppParamType` before the
|
||||
// type token reaches `classifyType`, so `is_same_v<const T, T>` returns
|
||||
// `'compatible'` instead of the ISO-correct `false`. Tracked under the
|
||||
// cv-sidecar refactor in #1579's "Out of scope" list; until that lands
|
||||
// this approximation matches the common `is_same_v<T, ConcreteType>`
|
||||
// dispatch idiom and silently degrades on cv-distinct compares.
|
||||
[
|
||||
'is_same_v',
|
||||
(cls) => {
|
||||
if (cls.length < 2 || cls[0] === 'unknown' || cls[1] === 'unknown') return 'unknown';
|
||||
return cls[0] === cls[1] ? 'compatible' : 'incompatible';
|
||||
(args) => {
|
||||
if (args.length < 2 || args[0].typeClass === 'unknown' || args[1].typeClass === 'unknown') {
|
||||
return 'unknown';
|
||||
}
|
||||
return args[0].typeClass === args[1].typeClass ? 'compatible' : 'incompatible';
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
function verdictFromBool(predicate: boolean, cls: readonly TypeClass[]): ArityVerdict {
|
||||
if (cls[0] === 'unknown') return 'unknown';
|
||||
return predicate ? 'compatible' : 'incompatible';
|
||||
function unaryVerdict(
|
||||
args: readonly ConstraintArgClass[],
|
||||
predicate: (arg: ConstraintArgClass) => boolean,
|
||||
): ArityVerdict {
|
||||
const arg = args[0];
|
||||
if (arg === undefined || arg.typeClass === 'unknown') return 'unknown';
|
||||
return predicate(arg) ? 'compatible' : 'incompatible';
|
||||
}
|
||||
|
||||
function unaryShapeVerdict(
|
||||
args: readonly ConstraintArgClass[],
|
||||
predicate: (shape: ParameterTypeClass) => boolean,
|
||||
options: { readonly requireTopLevelCv?: boolean } = {},
|
||||
): ArityVerdict {
|
||||
const arg = args[0];
|
||||
if (arg === undefined || arg.typeClass === 'unknown') return 'unknown';
|
||||
const shape = arg.shape;
|
||||
if (shape === undefined || shape.indirection === 'unknown' || shape.cv === 'unknown') {
|
||||
return 'unknown';
|
||||
}
|
||||
if (options.requireTopLevelCv === true && shape.indirection === 'pointer') {
|
||||
return 'unknown';
|
||||
}
|
||||
return predicate(shape) ? 'compatible' : 'incompatible';
|
||||
}
|
||||
|
||||
function isPlainValue(arg: ConstraintArgClass): boolean {
|
||||
const shape = arg.shape;
|
||||
if (shape === undefined) return true;
|
||||
return shape.indirection === 'value';
|
||||
}
|
||||
|
||||
function classifyConstraintArg(
|
||||
token: string | undefined,
|
||||
shape?: ParameterTypeClass,
|
||||
): ConstraintArgClass {
|
||||
if (shape !== undefined && shape.base.startsWith('enum:')) {
|
||||
return { typeClass: 'enum', shape };
|
||||
}
|
||||
const typeClass = token === undefined || token === '' ? 'unknown' : classifyType(token);
|
||||
return { typeClass, ...(shape !== undefined ? { shape } : {}) };
|
||||
}
|
||||
|
||||
function tokenForArg(ctx: ConstraintContext, argIdx: number): string | undefined {
|
||||
const shape = ctx.argumentTypeClasses?.[argIdx];
|
||||
if (shape?.base.startsWith('enum:')) return shape.base;
|
||||
return ctx.argumentTypes?.[argIdx];
|
||||
}
|
||||
|
||||
function shapeForTemplateParam(
|
||||
ctx: ConstraintContext,
|
||||
paramName: string,
|
||||
argIdx: number,
|
||||
def?: SymbolDefinition,
|
||||
): ParameterTypeClass | undefined {
|
||||
const argShape = ctx.argumentTypeClasses?.[argIdx];
|
||||
if (argShape === undefined) return undefined;
|
||||
|
||||
const paramShape = def?.parameterTypeClasses?.[argIdx];
|
||||
if (paramShape === undefined) return argShape;
|
||||
if (paramShape.base === paramName && paramShape.indirection === 'value') return argShape;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Public surface — registered as `ScopeResolver.constraintCompatibility`. */
|
||||
|
|
@ -80,13 +192,14 @@ export function cppConstraintCompatibility(
|
|||
): ArityVerdict {
|
||||
const payload = def.templateConstraints as CppConstraintPayload | undefined;
|
||||
if (payload === undefined) return 'unknown';
|
||||
return evaluate(payload.expr, payload, ctx);
|
||||
return evaluate(payload.expr, payload, ctx, def);
|
||||
}
|
||||
|
||||
function evaluate(
|
||||
expr: ConstraintExpr,
|
||||
payload: CppConstraintPayload,
|
||||
ctx: ConstraintContext,
|
||||
def?: SymbolDefinition,
|
||||
): ArityVerdict {
|
||||
switch (expr.kind) {
|
||||
case 'unknown':
|
||||
|
|
@ -96,17 +209,18 @@ function evaluate(
|
|||
if (evaluator === undefined) return 'unknown';
|
||||
const classes = expr.args.map((paramName) => {
|
||||
const argIdx = payload.paramArgIndex[paramName];
|
||||
if (argIdx === undefined) return 'unknown' as TypeClass;
|
||||
const token = ctx.argumentTypes?.[argIdx];
|
||||
if (token === undefined || token === '') return 'unknown' as TypeClass;
|
||||
return classifyType(token);
|
||||
if (argIdx === undefined) return { typeClass: 'unknown' as TypeClass };
|
||||
return classifyConstraintArg(
|
||||
tokenForArg(ctx, argIdx),
|
||||
shapeForTemplateParam(ctx, paramName, argIdx, def),
|
||||
);
|
||||
});
|
||||
return evaluator(classes);
|
||||
}
|
||||
case 'and': {
|
||||
let result: ArityVerdict = 'compatible';
|
||||
for (const child of expr.children) {
|
||||
const v = evaluate(child, payload, ctx);
|
||||
const v = evaluate(child, payload, ctx, def);
|
||||
if (v === 'incompatible') return 'incompatible';
|
||||
if (v === 'unknown') result = 'unknown';
|
||||
}
|
||||
|
|
@ -115,14 +229,14 @@ function evaluate(
|
|||
case 'or': {
|
||||
let result: ArityVerdict = 'incompatible';
|
||||
for (const child of expr.children) {
|
||||
const v = evaluate(child, payload, ctx);
|
||||
const v = evaluate(child, payload, ctx, def);
|
||||
if (v === 'compatible') return 'compatible';
|
||||
if (v === 'unknown') result = 'unknown';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case 'not': {
|
||||
const v = evaluate(expr.child, payload, ctx);
|
||||
const v = evaluate(expr.child, payload, ctx, def);
|
||||
if (v === 'compatible') return 'incompatible';
|
||||
if (v === 'incompatible') return 'compatible';
|
||||
return 'unknown';
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@
|
|||
* the call-site inference in `captures.ts`) to one of the categories
|
||||
* the `<type_traits>` predicate registry uses for SFINAE filtering.
|
||||
*
|
||||
* Intentionally coarse: cv / pointer / reference qualifiers are stripped
|
||||
* upstream by `normalizeCppParamType`. Tier-A predicates
|
||||
* (`is_integral_v`, `is_floating_point_v`, `is_arithmetic_v`, `is_same_v`)
|
||||
* are insensitive to those modifiers per ISO `<type_traits>` semantics
|
||||
* ("including any cv-qualified variants").
|
||||
* `argumentTypes` remain normalized for overload narrowing, while
|
||||
* constraint predicates that need cv/ref/pointer shape read the parallel
|
||||
* `argumentTypeClasses` sidecar. Unknown shapes must stay unknown rather
|
||||
* than being guessed as incompatible.
|
||||
*/
|
||||
|
||||
export type TypeClass =
|
||||
|
|
@ -21,7 +20,11 @@ export type TypeClass =
|
|||
| 'char'
|
||||
| 'string'
|
||||
| 'null'
|
||||
| 'void'
|
||||
| 'enum'
|
||||
| 'class'
|
||||
| 'pointer'
|
||||
| 'reference'
|
||||
| 'unknown';
|
||||
|
||||
/**
|
||||
|
|
@ -29,13 +32,17 @@ export type TypeClass =
|
|||
* inference table in `captures.ts:inferCppLiteralType` plus the std::
|
||||
* normalization in `arity-metadata.ts:normalizeCppParamType`.
|
||||
*
|
||||
* Caller note: token must already be normalized (no `const`, no `&` / `*`,
|
||||
* no `std::` prefix). Tokens passed via `ConstraintContext.argumentTypes`
|
||||
* coming from `inferCppCallArgTypes` satisfy this.
|
||||
* Caller note: token should be normalized for overload matching. Enum
|
||||
* tokens produced by the C++ adapter use the internal `enum:<Name>`
|
||||
* prefix so `is_enum_v` does not have to guess that every user token is
|
||||
* class-like.
|
||||
*/
|
||||
export function classifyType(token: string): TypeClass {
|
||||
if (token.length === 0) return 'unknown';
|
||||
if (token.startsWith('enum:')) return 'enum';
|
||||
switch (token) {
|
||||
case 'void':
|
||||
return 'void';
|
||||
case 'int':
|
||||
return 'integral';
|
||||
case 'double':
|
||||
|
|
|
|||
|
|
@ -913,6 +913,9 @@ function pass5CollectReferences(
|
|||
const explicitReceiver = extractExplicitReceiver(match);
|
||||
const arity = extractArity(match);
|
||||
const argumentTypes = extractArgumentTypes(match);
|
||||
const argumentTypeClasses = parseJsonParameterTypeClassesCapture(
|
||||
match['@reference.parameter-type-classes'],
|
||||
);
|
||||
|
||||
const site: ReferenceSite = {
|
||||
name: nameCap.text,
|
||||
|
|
@ -923,6 +926,7 @@ function pass5CollectReferences(
|
|||
...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
|
||||
...(arity !== undefined ? { arity } : {}),
|
||||
...(argumentTypes !== undefined ? { argumentTypes } : {}),
|
||||
...(argumentTypeClasses !== undefined ? { argumentTypeClasses } : {}),
|
||||
};
|
||||
referenceSites.push(site);
|
||||
}
|
||||
|
|
@ -1040,9 +1044,11 @@ const KNOWN_SUB_TAGS: ReadonlySet<string> = new Set<string>([
|
|||
'@reference.receiver',
|
||||
'@reference.arity',
|
||||
'@reference.parameter-types',
|
||||
'@reference.parameter-type-classes',
|
||||
'@declaration.parameter-count',
|
||||
'@declaration.required-parameter-count',
|
||||
'@declaration.parameter-types',
|
||||
'@declaration.parameter-type-classes',
|
||||
'@declaration.template-constraints',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ export function emitFreeCallFallback(
|
|||
site.arity,
|
||||
site.argumentTypes,
|
||||
{
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
},
|
||||
|
|
@ -196,6 +197,7 @@ export function emitFreeCallFallback(
|
|||
fnDef = ordinary[0];
|
||||
} else {
|
||||
const narrowed = narrowOverloadCandidates(ordinary, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
});
|
||||
|
|
@ -231,6 +233,7 @@ export function emitFreeCallFallback(
|
|||
push(adl);
|
||||
|
||||
const narrowed = narrowOverloadCandidates(merged, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: options.conversionRankFn,
|
||||
constraintCompatibility: options.constraintCompatibility,
|
||||
});
|
||||
|
|
@ -490,6 +493,7 @@ export function pickImplicitThisOverload(
|
|||
readonly name: string;
|
||||
readonly arity?: number;
|
||||
readonly argumentTypes?: readonly string[];
|
||||
readonly argumentTypeClasses?: readonly import('gitnexus-shared').ParameterTypeClass[];
|
||||
},
|
||||
scopes: ScopeResolutionIndexes,
|
||||
workspaceIndex: WorkspaceResolutionIndex,
|
||||
|
|
@ -526,6 +530,7 @@ export function pickImplicitThisOverload(
|
|||
// disambiguating signal) leaves the call unresolved rather than
|
||||
// routing to an arbitrary first overload by registration order.
|
||||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: hookCtx?.conversionRankFn,
|
||||
constraintCompatibility: hookCtx?.constraintCompatibility,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ export type ConversionRankFn = (argType: string, paramType: string) => number;
|
|||
* undefined preserves the legacy arity + exact-type behavior.
|
||||
*/
|
||||
export interface OverloadNarrowingHookCtx {
|
||||
/** Shape-preserving per-argument sidecar aligned with `argTypes`. */
|
||||
readonly argumentTypeClasses?: ConstraintContext['argumentTypeClasses'];
|
||||
/** Conversion-rank scoring fallback (step 4b). Engages when the
|
||||
* exact-type filter rejects every candidate. */
|
||||
readonly conversionRankFn?: ConversionRankFn;
|
||||
|
|
@ -163,7 +165,15 @@ export function narrowOverloadCandidates(
|
|||
// than emitting a wrong edge.
|
||||
if (hookCtx?.constraintCompatibility !== undefined && argCount !== undefined) {
|
||||
const callsite: Callsite = { arity: argCount };
|
||||
const ctx: ConstraintContext = argTypes !== undefined ? { argumentTypes: argTypes } : {};
|
||||
const ctx: ConstraintContext =
|
||||
argTypes !== undefined
|
||||
? {
|
||||
argumentTypes: argTypes,
|
||||
...(hookCtx.argumentTypeClasses !== undefined
|
||||
? { argumentTypeClasses: hookCtx.argumentTypeClasses }
|
||||
: {}),
|
||||
}
|
||||
: {};
|
||||
result = result.filter((def) => {
|
||||
if (def.templateConstraints === undefined) return true;
|
||||
return hookCtx.constraintCompatibility!(callsite, def, ctx) !== 'incompatible';
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ export function emitReceiverBoundCalls(
|
|||
site.arity,
|
||||
site.argumentTypes,
|
||||
{
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
},
|
||||
|
|
@ -732,6 +733,7 @@ function pickOverload(
|
|||
if (overloads.length === 1) return overloads[0];
|
||||
|
||||
const candidates = narrowOverloadCandidates(overloads, site.arity, site.argumentTypes, {
|
||||
argumentTypeClasses: site.argumentTypeClasses,
|
||||
conversionRankFn: provider.conversionRankFn,
|
||||
constraintCompatibility: provider.constraintCompatibility,
|
||||
});
|
||||
|
|
|
|||
16
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-class/main.cpp
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-class/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#include <type_traits>
|
||||
|
||||
struct S {};
|
||||
|
||||
template <class T, std::enable_if_t<std::is_class_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run() {
|
||||
S s;
|
||||
int n = 0;
|
||||
pick(s);
|
||||
pick(n);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-const-volatile/main.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-const-volatile/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <type_traits>
|
||||
|
||||
template <class T, std::enable_if_t<std::is_const_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_volatile_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run() {
|
||||
const int c = 0;
|
||||
volatile int v = 0;
|
||||
pick(c);
|
||||
pick(v);
|
||||
}
|
||||
16
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-enum/main.cpp
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-enum/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#include <type_traits>
|
||||
|
||||
enum Color { Red };
|
||||
|
||||
template <class T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run() {
|
||||
Color color = Red;
|
||||
int n = 0;
|
||||
pick(color);
|
||||
pick(n);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-pointer/main.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-pointer/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <type_traits>
|
||||
|
||||
struct S {};
|
||||
|
||||
template <class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_class_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run(S* p, S s) {
|
||||
pick(p);
|
||||
pick(s);
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-reference/main.cpp
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-reference/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <type_traits>
|
||||
|
||||
template <class T, std::enable_if_t<std::is_reference_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run() {
|
||||
int n = 0;
|
||||
int& r = n;
|
||||
pick(r);
|
||||
pick(n);
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-void/main.cpp
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/cpp-sfinae-is-void/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#include <type_traits>
|
||||
|
||||
template <class T, std::enable_if_t<std::is_void_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
template <class T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
|
||||
void pick(T value) {}
|
||||
|
||||
void run() {
|
||||
void* p;
|
||||
pick(p);
|
||||
}
|
||||
|
|
@ -3156,6 +3156,59 @@ describe('C++ SFINAE filter — C++20 requires-clause shape', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('C++ SFINAE filter — Tier-A type_traits predicates', () => {
|
||||
async function runFixture(name: string): Promise<PipelineResult> {
|
||||
return runPipelineFromRepo(path.join(FIXTURES, name), () => {});
|
||||
}
|
||||
|
||||
function callsFromRunToPick(result: PipelineResult) {
|
||||
return getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'run' && c.target === 'pick',
|
||||
);
|
||||
}
|
||||
|
||||
it('is_pointer_v and is_class_v disambiguate pointer vs class arguments', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-pointer');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(2);
|
||||
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
|
||||
}, 60000);
|
||||
|
||||
it('is_reference_v keeps reference-shaped arguments distinct from values', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-reference');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(2);
|
||||
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
|
||||
}, 60000);
|
||||
|
||||
it('is_class_v rejects primitive arguments while keeping class arguments', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-class');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(2);
|
||||
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
|
||||
}, 60000);
|
||||
|
||||
it('is_enum_v distinguishes known enum declarations from primitives', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-enum');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(2);
|
||||
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
|
||||
}, 60000);
|
||||
|
||||
it('is_const_v and is_volatile_v disambiguate cv-qualified locals', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-const-volatile');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(2);
|
||||
expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2);
|
||||
}, 60000);
|
||||
|
||||
it('is_void_v does not misclassify void pointers as void values', async () => {
|
||||
const result = await runFixture('cpp-sfinae-is-void');
|
||||
const calls = callsFromRunToPick(result);
|
||||
expect(calls.length).toBe(1);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('C++ SFINAE filter — unknown predicate keeps both candidates (monotonicity contract)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,12 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
'enable_if_t<is_integral_v<T>> overload binds only on integral call sites',
|
||||
'enable_if_t<is_floating_point_v<T>> overload binds only on floating call sites',
|
||||
'requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)',
|
||||
'is_pointer_v and is_class_v disambiguate pointer vs class arguments',
|
||||
'is_reference_v keeps reference-shaped arguments distinct from values',
|
||||
'is_class_v rejects primitive arguments while keeping class arguments',
|
||||
'is_enum_v distinguishes known enum declarations from primitives',
|
||||
'is_const_v and is_volatile_v disambiguate cv-qualified locals',
|
||||
'is_void_v does not misclassify void pointers as void values',
|
||||
// The legacy DAG path has no inline-namespace same-name ambiguity
|
||||
// detection. When two inline children declare the same name, the
|
||||
// legacy path picks an arbitrary match. The scope-resolver returns
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
evaluateForTest,
|
||||
getRegistrySize,
|
||||
} from '../../../../src/core/ingestion/languages/cpp/constraint-filter.js';
|
||||
import type { ArityVerdict, SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { ArityVerdict, ParameterTypeClass, SymbolDefinition } from 'gitnexus-shared';
|
||||
|
||||
function templateConstraintsFor(src: string): CppConstraintPayload | undefined {
|
||||
const matches = emitCppScopeCaptures(src, 'test.cpp');
|
||||
|
|
@ -201,11 +201,26 @@ describe('evaluate — Kleene 3-valued truth table', () => {
|
|||
// ─── Section 3: Predicate registry ─────────────────────────────────────────
|
||||
|
||||
describe('Tier-A predicate registry', () => {
|
||||
it('registry size is exactly 4 (surface-guard against accidental adds)', () => {
|
||||
expect(getRegistrySize()).toBe(4);
|
||||
it('registry size is exactly 11 (surface-guard against accidental adds)', () => {
|
||||
expect(getRegistrySize()).toBe(11);
|
||||
});
|
||||
|
||||
function verdict(name: string, args: string[], argumentTypes: readonly string[]): ArityVerdict {
|
||||
const shape = (
|
||||
base: string,
|
||||
indirection: ParameterTypeClass['indirection'] = 'value',
|
||||
cv: ParameterTypeClass['cv'] = 'none',
|
||||
pointerDepth = indirection === 'pointer' ? 1 : 0,
|
||||
): ParameterTypeClass => ({ base, cv, indirection, pointerDepth });
|
||||
|
||||
function verdict(
|
||||
name: string,
|
||||
args: string[],
|
||||
argumentTypes: readonly string[],
|
||||
opts: {
|
||||
readonly argumentTypeClasses?: readonly ParameterTypeClass[];
|
||||
readonly parameterTypeClasses?: readonly ParameterTypeClass[];
|
||||
} = {},
|
||||
): ArityVerdict {
|
||||
const payload: CppConstraintPayload = {
|
||||
templateParams: args,
|
||||
paramArgIndex: Object.fromEntries(args.map((a, i) => [a, i])),
|
||||
|
|
@ -216,8 +231,16 @@ describe('Tier-A predicate registry', () => {
|
|||
filePath: 'x.cpp',
|
||||
type: 'Function',
|
||||
templateConstraints: payload,
|
||||
...(opts.parameterTypeClasses !== undefined
|
||||
? { parameterTypeClasses: opts.parameterTypeClasses }
|
||||
: {}),
|
||||
};
|
||||
return cppConstraintCompatibility({ arity: argumentTypes.length }, def, { argumentTypes });
|
||||
return cppConstraintCompatibility({ arity: argumentTypes.length }, def, {
|
||||
argumentTypes,
|
||||
...(opts.argumentTypeClasses !== undefined
|
||||
? { argumentTypeClasses: opts.argumentTypeClasses }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
it('is_integral_v matches int, rejects double, unknown for blank', () => {
|
||||
|
|
@ -257,6 +280,117 @@ describe('Tier-A predicate registry', () => {
|
|||
expect(verdict('is_same_v', ['A', 'B'], ['char', 'int'])).toBe('incompatible');
|
||||
});
|
||||
|
||||
it('is_void_v matches void, rejects int, unknown for blank', () => {
|
||||
expect(verdict('is_void_v', ['T'], ['void'])).toBe('compatible');
|
||||
expect(verdict('is_void_v', ['T'], ['int'])).toBe('incompatible');
|
||||
expect(verdict('is_void_v', ['T'], [''])).toBe('unknown');
|
||||
});
|
||||
|
||||
it('is_enum_v matches known enum tokens, rejects class, unknown for blank', () => {
|
||||
expect(
|
||||
verdict('is_enum_v', ['T'], ['Color'], {
|
||||
argumentTypeClasses: [shape('enum:Color')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('compatible');
|
||||
expect(verdict('is_enum_v', ['T'], ['Widget'])).toBe('incompatible');
|
||||
expect(verdict('is_enum_v', ['T'], [''])).toBe('unknown');
|
||||
});
|
||||
|
||||
it('is_class_v matches class-like tokens, rejects primitives, unknown for blank', () => {
|
||||
expect(verdict('is_class_v', ['T'], ['Widget'])).toBe('compatible');
|
||||
expect(verdict('is_class_v', ['T'], ['int'])).toBe('incompatible');
|
||||
expect(verdict('is_class_v', ['T'], [''])).toBe('unknown');
|
||||
});
|
||||
|
||||
it('is_pointer_v uses the argument type-class sidecar conservatively', () => {
|
||||
expect(
|
||||
verdict('is_pointer_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'pointer')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('compatible');
|
||||
expect(
|
||||
verdict('is_pointer_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('incompatible');
|
||||
expect(verdict('is_pointer_v', ['T'], ['int'])).toBe('unknown');
|
||||
expect(
|
||||
verdict('is_pointer_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'unknown', 'none')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('unknown');
|
||||
});
|
||||
|
||||
it('is_reference_v uses the argument type-class sidecar conservatively', () => {
|
||||
expect(
|
||||
verdict('is_reference_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'lvalue-ref')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('compatible');
|
||||
expect(
|
||||
verdict('is_reference_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('incompatible');
|
||||
expect(verdict('is_reference_v', ['T'], ['int'])).toBe('unknown');
|
||||
expect(
|
||||
verdict('is_reference_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'unknown', 'none')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('unknown');
|
||||
});
|
||||
|
||||
it('is_const_v and is_volatile_v read top-level cv from the sidecar conservatively', () => {
|
||||
expect(
|
||||
verdict('is_const_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'value', 'const')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('compatible');
|
||||
expect(
|
||||
verdict('is_const_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('incompatible');
|
||||
expect(verdict('is_const_v', ['T'], ['int'])).toBe('unknown');
|
||||
expect(
|
||||
verdict('is_volatile_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'value', 'volatile')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('compatible');
|
||||
expect(verdict('is_volatile_v', ['T'], ['int'])).toBe('unknown');
|
||||
expect(
|
||||
verdict('is_const_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'pointer', 'const')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('unknown');
|
||||
expect(
|
||||
verdict('is_const_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'value', 'unknown')],
|
||||
parameterTypeClasses: [shape('T')],
|
||||
}),
|
||||
).toBe('unknown');
|
||||
});
|
||||
|
||||
it('shape-sensitive predicates stay unknown when T is not the whole parameter type', () => {
|
||||
expect(
|
||||
verdict('is_pointer_v', ['T'], ['int'], {
|
||||
argumentTypeClasses: [shape('int', 'pointer')],
|
||||
parameterTypeClasses: [shape('T', 'pointer')],
|
||||
}),
|
||||
).toBe('unknown');
|
||||
});
|
||||
|
||||
it('unregistered predicate yields unknown (monotonicity)', () => {
|
||||
expect(verdict('__not_in_registry__', ['T'], ['int'])).toBe('unknown');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue