Summary
C# 11 allows interfaces to declare static abstract (or abstract static) operator members — the foundation of System.Numerics.INumber<TSelf> generic math:
public interface IMath<T> where T : IMath<T>
{
static abstract T operator +(T a, T b); // static-first
abstract static T operator -(T a, T b); // abstract-first (also valid)
static abstract implicit operator T(int x); // conversion operator
abstract static T Zero { get; } // captured correctly — baseline
abstract static int Compare(T a, T b); // captured correctly — baseline
}
The C# symbol extractor silently drops every operator line on the interface (both modifier orders, both regular operators and implicit/explicit conversion operators), while the same operators declared on a class or struct are captured. Properties and methods on the same interface are captured correctly — it is specifically the operator regexes that are broken.
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-iop
cat > /tmp/dogfood/cs-iop/I.cs <<'EOF'
namespace Demo;
public interface IMath<T> where T : IMath<T>
{
static abstract T operator +(T a, T b); // line 5 static-first
abstract static T operator -(T a, T b); // line 6 abstract-first
static abstract T operator *(T a, T b); // line 7 static-first
abstract static T Zero { get; } // line 8 property (captured)
static abstract implicit operator T(int x); // line 9 conversion operator
abstract static int Compare(T a, T b); // line 10 method (captured)
}
public struct N
{
public static N operator +(N a, N b) => a; // line 15 captured
public static N operator -(N a, N b) => a; // line 16 captured
}
EOF
"$CDIDX" /tmp/dogfood/cs-iop
"$CDIDX" symbols --db /tmp/dogfood/cs-iop/.cdidx/codeindex.db --lang csharp
Actual
function + I.cs:15 # struct N
function - I.cs:16 # struct N
interface IMath I.cs:3-11
struct N I.cs:13-17
function Compare I.cs:10 # baseline method — captured
property Zero I.cs:8 # baseline property — captured
namespace Demo I.cs:1
Missing: lines 5, 6, 7, 9 — all four interface operator declarations. Expected: every operator line to be captured the way line 15/16 are on the struct.
Downstream effects:
cdidx definition "+" --db ... on IMath<T> returns no hits even though the interface declares operator + on line 5.
cdidx callers / cdidx references cannot navigate through the interface's operator surface at all.
cdidx outline I.cs hides half of IMath<T>'s members — AI clients browsing a generic-math API see Zero and Compare but no arithmetic.
Suspected root cause (from reading the source)
Two regexes in src/CodeIndex/Indexer/SymbolExtractor.cs assume static is the first (and only) pre-return-type modifier, and allow exactly one return-type token between static and operator:
SymbolExtractor.cs:84 — implicit/explicit conversion operator:
@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+(?<name>implicit|explicit)\s+operator\b"
Requires static <name> operator. On an interface the line is static abstract implicit operator T(int x); — extra abstract between static and implicit breaks the match. Also, abstract static implicit operator T(...) (abstract-first) starts with abstract, which the pattern cannot handle at all.
SymbolExtractor.cs:87 — binary/unary operator:
@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+\S+\s+operator\s+(?<name>\S+)\s*\("
Requires static <one-token> operator. Fails two ways on interfaces:
static abstract T operator +(T a, T b); — between static and operator there are two tokens (abstract and T), but \S+\s+operator only allows one.
abstract static T operator +(T a, T b); — line starts with abstract, not static, so the required static literal at the front fails.
(abstract static int Compare(T a, T b) on line 10 of the repro is captured because the method regex on SymbolExtractor.cs:94 has a general modifier slot (?:(?:static|sealed|partial|readonly|unsafe|extern|virtual|override|abstract|async|new|file)\s+)* — that same slot is absent from both operator regexes.)
Suggested fix
Give both operator regexes the same permissive modifier slot the method regex already has, and allow the modifiers to appear in any order.
Conversion operator (SymbolExtractor.cs:84):
@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|sealed|override|extern|unsafe|new)\s+)*(?<name>implicit|explicit)\s+operator\b(?:\s+checked)?"
Binary/unary operator (SymbolExtractor.cs:87):
@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|sealed|override|extern|unsafe|new)\s+)*\S+\s+operator\s+(?:checked\s+)?(?<name>\S+)\s*\("
The (?:checked\s+)? slot is the same addition called out by #238, so fixing both at once is natural. A static keyword is still required on a legal operator declaration at the syntactic level, but in practice the modifier slot already requires at least one modifier via the non-empty name/body that follows; requiring the explicit static literal inside the modifier slot is the cleanest way to keep call-site lines out (x operator +(y) is not a C# statement). The existing static\s+ anchor can simply be dropped in favor of letting the modifier slot consume it — the operator literal plus ( at the end is what actually anchors this regex to a real declaration.
Optionally stash the modifiers (abstract, static, checked) in a signature/metadata field so definition + can disambiguate same-named operator rows in output. Not required for capture correctness.
Why it matters
System.Numerics.INumber<TSelf>, IBinaryInteger, IComparisonOperators, IAdditionOperators, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IModulusOperators, IUnaryNegationOperators, IEqualityOperators, IIncrementOperators, IBitwiseOperators — every generic-math interface in System.Numerics relies on static abstract T operator ?(...). Today cdidx sees only the Parse / TryParse / Compare helpers on those interfaces, not the operators.
- Custom numeric types (money, units of measure, saturating integers) declared against
INumber<TSelf> expose their entire arithmetic contract on an interface — it is invisible to cdidx.
- The bug is silent. No warning, no fallback, no lower-confidence capture — just missing rows.
Scope
src/CodeIndex/Indexer/SymbolExtractor.cs — loosen the two operator regexes (lines 84 and 87) to accept a modifier slot in any order and the C# 11 checked keyword.
tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for static abstract T operator +, abstract static T operator -, static abstract implicit operator T(int), on both interface and class/struct.
DEVELOPER_GUIDE.md language-pattern reference table — add C# 11 static abstract interface members.
Related
Environment
- cdidx: v1.10.0 (tarball from GitHub releases).
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
C# 11 allows interfaces to declare
static abstract(orabstract static) operator members — the foundation ofSystem.Numerics.INumber<TSelf>generic math:The C# symbol extractor silently drops every
operatorline on the interface (both modifier orders, both regular operators and implicit/explicit conversion operators), while the same operators declared on a class or struct are captured. Properties and methods on the same interface are captured correctly — it is specifically the operator regexes that are broken.Repro
Actual
Missing: lines 5, 6, 7, 9 — all four interface operator declarations. Expected: every
operatorline to be captured the way line 15/16 are on the struct.Downstream effects:
cdidx definition "+" --db ...onIMath<T>returns no hits even though the interface declaresoperator +on line 5.cdidx callers/cdidx referencescannot navigate through the interface's operator surface at all.cdidx outline I.cshides half ofIMath<T>'s members — AI clients browsing a generic-math API seeZeroandComparebut no arithmetic.Suspected root cause (from reading the source)
Two regexes in
src/CodeIndex/Indexer/SymbolExtractor.csassumestaticis the first (and only) pre-return-type modifier, and allow exactly one return-type token betweenstaticandoperator:SymbolExtractor.cs:84— implicit/explicit conversion operator:@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+(?<name>implicit|explicit)\s+operator\b"Requires
static <name> operator. On an interface the line isstatic abstract implicit operator T(int x);— extraabstractbetweenstaticandimplicitbreaks the match. Also,abstract static implicit operator T(...)(abstract-first) starts withabstract, which the pattern cannot handle at all.SymbolExtractor.cs:87— binary/unary operator:@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+\S+\s+operator\s+(?<name>\S+)\s*\("Requires
static <one-token> operator. Fails two ways on interfaces:static abstract T operator +(T a, T b);— betweenstaticandoperatorthere are two tokens (abstractandT), but\S+\s+operatoronly allows one.abstract static T operator +(T a, T b);— line starts withabstract, notstatic, so the requiredstaticliteral at the front fails.(
abstract static int Compare(T a, T b)on line 10 of the repro is captured because the method regex onSymbolExtractor.cs:94has a general modifier slot(?:(?:static|sealed|partial|readonly|unsafe|extern|virtual|override|abstract|async|new|file)\s+)*— that same slot is absent from both operator regexes.)Suggested fix
Give both operator regexes the same permissive modifier slot the method regex already has, and allow the modifiers to appear in any order.
Conversion operator (
SymbolExtractor.cs:84):@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|sealed|override|extern|unsafe|new)\s+)*(?<name>implicit|explicit)\s+operator\b(?:\s+checked)?"Binary/unary operator (
SymbolExtractor.cs:87):@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|sealed|override|extern|unsafe|new)\s+)*\S+\s+operator\s+(?:checked\s+)?(?<name>\S+)\s*\("The
(?:checked\s+)?slot is the same addition called out by #238, so fixing both at once is natural. Astatickeyword is still required on a legal operator declaration at the syntactic level, but in practice the modifier slot already requires at least one modifier via the non-empty name/body that follows; requiring the explicitstaticliteral inside the modifier slot is the cleanest way to keep call-site lines out (x operator +(y)is not a C# statement). The existingstatic\s+anchor can simply be dropped in favor of letting the modifier slot consume it — theoperatorliteral plus(at the end is what actually anchors this regex to a real declaration.Optionally stash the modifiers (
abstract,static,checked) in a signature/metadata field sodefinition +can disambiguate same-named operator rows in output. Not required for capture correctness.Why it matters
System.Numerics.INumber<TSelf>,IBinaryInteger,IComparisonOperators,IAdditionOperators,ISubtractionOperators,IMultiplyOperators,IDivisionOperators,IModulusOperators,IUnaryNegationOperators,IEqualityOperators,IIncrementOperators,IBitwiseOperators— every generic-math interface inSystem.Numericsrelies onstatic abstract T operator ?(...). Today cdidx sees only theParse/TryParse/Comparehelpers on those interfaces, not the operators.INumber<TSelf>expose their entire arithmetic contract on an interface — it is invisible to cdidx.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs— loosen the two operator regexes (lines 84 and 87) to accept a modifier slot in any order and the C# 11checkedkeyword.tests/CodeIndex.Tests/SymbolExtractorTests.cs— fixtures forstatic abstract T operator +,abstract static T operator -,static abstract implicit operator T(int), on both interface and class/struct.DEVELOPER_GUIDE.mdlanguage-pattern reference table — add C# 11 static abstract interface members.Related
operator checked +/operator checked -(C# 11 user-defined checked operators) are dropped from the symbol index #238 — C# 11operator checked +/operator checked -(same extractor family, adjacent modifier-slot fix). Consider landing both fixes in one pass since they touch the same two regexes.+,implicit,explicit,this) #213 — C# operator / indexer / conversion-operator naming (same extractor).ref/ref readonlyreturn types on methods and properties are silently dropped —public ref T Find(...)produces zero symbols #224 — C# ref-return modifier slot (same "static must be first" family).int*,void**,delegate*<...>,int*[]) #234 — C# pointer / function-pointer return types (same "narrow char class / narrow modifier slot" family).Environment
CLOUD_BOOTSTRAP_PROMPT.md.