/** * 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 = 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 using std::enable_if_t; using std::is_integral_v; template, 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 template, 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 template requires std::is_integral_v 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 template())::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 { const out = new Set([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 ``', () => { // 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'); }); });