GitNexus/gitnexus/test/unit/scope-resolution/cpp/cpp-constraint.test.ts

397 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Unit tests for the C++ SFINAE / `requires`-clause constraint pipeline
* (issue #1579). Three sections per the plan:
* 1. Extractor — F1, F2, F4 shapes plus an unknown-bail row.
* 2. Kleene 3-valued evaluator — AND / OR / NOT truth-table rows.
* 3. Predicate registry — `is_integral_v`, `is_floating_point_v`,
* `is_arithmetic_v`, `is_same_v` × representative type tokens;
* surface-size assertion guards the registry shape.
*/
import { describe, it, expect } from 'vitest';
import { emitCppScopeCaptures } from '../../../../src/core/ingestion/languages/cpp/captures.js';
import type {
ConstraintExpr,
CppConstraintPayload,
} from '../../../../src/core/ingestion/languages/cpp/constraint-extractor.js';
import {
cppConstraintCompatibility,
evaluateForTest,
getRegistrySize,
} from '../../../../src/core/ingestion/languages/cpp/constraint-filter.js';
import type { ArityVerdict, ParameterTypeClass, SymbolDefinition } from 'gitnexus-shared';
function templateConstraintsFor(src: string): CppConstraintPayload | undefined {
const matches = emitCppScopeCaptures(src, 'test.cpp');
for (const m of matches) {
const cap = m['@declaration.template-constraints'];
if (cap !== undefined) return JSON.parse(cap.text) as CppConstraintPayload;
}
return undefined;
}
// ─── Section 1: Extractor ─────────────────────────────────────────────────
describe('extractCppTemplateConstraints — AST shapes', () => {
it('F1 — unqualified enable_if_t<P, int> = 0 default parameter', () => {
// Genuinely unqualified form — no `std::` prefix on `enable_if_t`,
// which exercises the `template_type`-direct branch in the extractor
// independently of the `qualified_identifier` unwrap covered by F2.
const payload = templateConstraintsFor(`
#include <type_traits>
using std::enable_if_t;
using std::is_integral_v;
template<class T, enable_if_t<is_integral_v<T>, int> = 0>
void process(T value);
`);
expect(payload).toBeDefined();
expect(payload!.templateParams).toContain('T');
expect(payload!.paramArgIndex).toEqual({ T: 0 });
expect(payload!.expr.kind).toBe('atomic');
if (payload!.expr.kind === 'atomic') {
expect(payload!.expr.name).toBe('is_integral_v');
expect(payload!.expr.args).toEqual(['T']);
}
});
it('F2 — std::-qualified enable_if_t (canonical ticket form)', () => {
const payload = templateConstraintsFor(`
#include <type_traits>
template<class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T value);
`);
expect(payload).toBeDefined();
if (payload!.expr.kind === 'atomic') {
// Qualified prefix stripped — registry lookup keys on the bare name.
expect(payload!.expr.name).toBe('is_floating_point_v');
expect(payload!.expr.args).toEqual(['T']);
} else {
throw new Error(`expected atomic, got ${payload!.expr.kind}`);
}
});
it('F4 — C++20 leading requires-clause', () => {
const payload = templateConstraintsFor(`
#include <type_traits>
template<class T> requires std::is_integral_v<T>
void process(T value);
`);
expect(payload).toBeDefined();
if (payload!.expr.kind === 'atomic') {
expect(payload!.expr.name).toBe('is_integral_v');
expect(payload!.expr.args).toEqual(['T']);
} else {
throw new Error(`expected atomic, got ${payload!.expr.kind}`);
}
});
it('unknown-bail row — non-template constraint payload returns unknown', () => {
// Use a predicate name the registry doesn't recognize, plus an
// unsupported boolean composition shape (decltype). Even if the
// extractor produces an `unknown` node here, monotonicity guarantees
// the candidate is kept at evaluation time.
const payload = templateConstraintsFor(`
#include <type_traits>
template<class T, std::enable_if_t<decltype(some_check<T>())::value, int> = 0>
void process(T value);
`);
// Extractor MAY succeed with kind: 'unknown' or return undefined —
// either is acceptable; the monotonicity invariant is what matters.
if (payload !== undefined) {
// Walk the expression tree: every leaf must be either an atomic
// outside the registry or an 'unknown' node — never a wrongly-typed
// boolean compose hiding an unrecognized shape.
const reachableKinds = collectKinds(payload.expr);
expect(reachableKinds.has('unknown')).toBe(true);
}
});
});
function collectKinds(expr: ConstraintExpr): Set<ConstraintExpr['kind']> {
const out = new Set<ConstraintExpr['kind']>([expr.kind]);
if (expr.kind === 'and' || expr.kind === 'or') {
for (const c of expr.children) for (const k of collectKinds(c)) out.add(k);
} else if (expr.kind === 'not') {
for (const k of collectKinds(expr.child)) out.add(k);
}
return out;
}
// ─── Section 2: Kleene 3-valued evaluator ──────────────────────────────────
describe('evaluate — Kleene 3-valued truth table', () => {
const payload: CppConstraintPayload = {
templateParams: ['T'],
paramArgIndex: { T: 0 },
expr: { kind: 'unknown' }, // unused; we pass expr to evaluate directly
};
const ctx = { argumentTypes: ['int'] as const };
const atomic = (verdict: ArityVerdict): ConstraintExpr => {
// Inject a verdict via a synthetic registry-miss-or-hit: use is_integral_v
// on T at argIdx 0 ('int') for compatible, is_floating_point_v for
// incompatible, and an unknown predicate for unknown.
if (verdict === 'compatible') return { kind: 'atomic', name: 'is_integral_v', args: ['T'] };
if (verdict === 'incompatible')
return { kind: 'atomic', name: 'is_floating_point_v', args: ['T'] };
return { kind: 'atomic', name: '__not_in_registry__', args: ['T'] };
};
it('AND: incompatible if any child incompatible', () => {
const expr: ConstraintExpr = {
kind: 'and',
children: [atomic('compatible'), atomic('incompatible')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('incompatible');
});
it('AND: compatible iff all children compatible', () => {
const expr: ConstraintExpr = {
kind: 'and',
children: [atomic('compatible'), atomic('compatible')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('compatible');
});
it('AND: unknown when no incompatible but at least one unknown', () => {
const expr: ConstraintExpr = {
kind: 'and',
children: [atomic('compatible'), atomic('unknown')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('unknown');
});
it('OR: compatible if any child compatible', () => {
const expr: ConstraintExpr = {
kind: 'or',
children: [atomic('incompatible'), atomic('compatible')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('compatible');
});
it('OR: incompatible iff all children incompatible', () => {
const expr: ConstraintExpr = {
kind: 'or',
children: [atomic('incompatible'), atomic('incompatible')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('incompatible');
});
it('OR: unknown when no compatible but at least one unknown', () => {
const expr: ConstraintExpr = {
kind: 'or',
children: [atomic('incompatible'), atomic('unknown')],
};
expect(evaluateForTest(expr, payload, ctx)).toBe('unknown');
});
it('NOT: flips compatible ↔ incompatible, passes through unknown', () => {
expect(evaluateForTest({ kind: 'not', child: atomic('compatible') }, payload, ctx)).toBe(
'incompatible',
);
expect(evaluateForTest({ kind: 'not', child: atomic('incompatible') }, payload, ctx)).toBe(
'compatible',
);
expect(evaluateForTest({ kind: 'not', child: atomic('unknown') }, payload, ctx)).toBe(
'unknown',
);
});
});
// ─── Section 3: Predicate registry ─────────────────────────────────────────
describe('Tier-A predicate registry', () => {
it('registry size is exactly 11 (surface-guard against accidental adds)', () => {
expect(getRegistrySize()).toBe(11);
});
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])),
expr: { kind: 'atomic', name, args },
};
const def: SymbolDefinition = {
nodeId: 'x',
filePath: 'x.cpp',
type: 'Function',
templateConstraints: payload,
...(opts.parameterTypeClasses !== undefined
? { parameterTypeClasses: opts.parameterTypeClasses }
: {}),
};
return cppConstraintCompatibility({ arity: argumentTypes.length }, def, {
argumentTypes,
...(opts.argumentTypeClasses !== undefined
? { argumentTypeClasses: opts.argumentTypeClasses }
: {}),
});
}
it('is_integral_v matches int, rejects double, unknown for blank', () => {
expect(verdict('is_integral_v', ['T'], ['int'])).toBe('compatible');
expect(verdict('is_integral_v', ['T'], ['double'])).toBe('incompatible');
expect(verdict('is_integral_v', ['T'], [''])).toBe('unknown');
});
it('is_integral_v accepts bool and char per ISO `<type_traits>`', () => {
// ISO §21.3.4 Table 48: bool and char are integral types.
expect(verdict('is_integral_v', ['T'], ['bool'])).toBe('compatible');
expect(verdict('is_integral_v', ['T'], ['char'])).toBe('compatible');
});
it('is_floating_point_v matches double, rejects int, unknown for blank', () => {
expect(verdict('is_floating_point_v', ['T'], ['double'])).toBe('compatible');
expect(verdict('is_floating_point_v', ['T'], ['int'])).toBe('incompatible');
expect(verdict('is_floating_point_v', ['T'], [''])).toBe('unknown');
});
it('is_arithmetic_v matches both int and double (integral floating)', () => {
expect(verdict('is_arithmetic_v', ['T'], ['int'])).toBe('compatible');
expect(verdict('is_arithmetic_v', ['T'], ['double'])).toBe('compatible');
expect(verdict('is_arithmetic_v', ['T'], ['bool'])).toBe('compatible');
expect(verdict('is_arithmetic_v', ['T'], ['char'])).toBe('compatible');
expect(verdict('is_arithmetic_v', ['T'], ['MyClass'])).toBe('incompatible');
});
it('is_same_v matches same tokens, rejects different, unknown on blanks', () => {
expect(verdict('is_same_v', ['A', 'B'], ['int', 'int'])).toBe('compatible');
expect(verdict('is_same_v', ['A', 'B'], ['int', 'double'])).toBe('incompatible');
expect(verdict('is_same_v', ['A', 'B'], ['int', ''])).toBe('unknown');
// Regression guard: even though `is_integral_v` now treats `bool` and
// `char` as integral, `is_same_v` must keep them distinct from `int`
// (precise `TypeClass` enum — widening lives only in the registry).
expect(verdict('is_same_v', ['A', 'B'], ['bool', 'int'])).toBe('incompatible');
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');
});
});