# Decompiling static abstract interface members: how generic math compiles


C# 11 added a small-sounding feature with large consequences: interfaces can declare `static abstract` members. That is what made *generic math* possible — you can now write `Sum<T>(ReadOnlySpan<T>)` once, constrain `T` to `INumber<T>`, and have it run over `int`, `double`, `decimal`, or a type you defined yourself, with the `+` operator resolving to each type's own implementation. It reads like ordinary generics. Underneath, it depends on a dispatch mechanism that did not exist before: calling a *static* interface method through a type parameter, resolved to the type argument at runtime.

That mechanism is where decompilers earn their keep. The IL for it reuses an old prefix in a new way, and a tool that learned the old rule — "`constrained.` precedes `callvirt`" — reads the new shape wrong. This post walks the exact IL the compiler emits for `T.Zero` and `a + b` over a generic constraint, explains why the `constrained.` prefix sits on a plain `call`, and shows how Glass.NET turns it back into the operators and static members you wrote.

## The source, and what makes it tricky

Here is the canonical generic-math method. There is no mention of any concrete numeric type; everything flows through the constraint:

```csharp
using System.Numerics;

public static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    T total = T.Zero;            // static abstract property on the interface
    foreach (T v in values)
        total += v;              // static abstract operator op_Addition
    return total;
}
```

`T.Zero` and `total += v` look like member access and an operator. But `T` is a type *parameter* — at compile time there is no concrete type to bind the call to. The runtime has to resolve `Zero` and `op_Addition` to whatever `T` turns out to be. That resolution is the job of the `constrained.` prefix.

<figure class="my-8"><svg viewBox="0 0 700 210" role="img" aria-label="An instance-member constrained call uses the constrained prefix on a callvirt, dispatching on a boxed-or-managed receiver. A static abstract member constrained call uses the same prefix on a plain call with no receiver, carrying the type argument into static dispatch. A naive decompiler that assumes constrained always precedes callvirt mis-reads the static case." style="width:100%;height:auto"><defs><style>.t{font:600 12px system-ui;fill:#1547e0}.x{font:11px system-ui;fill:#475569}.m{font:600 10.5px ui-monospace,monospace;fill:#0f172a}.inst{fill:#eef4ff;stroke:#8eb4ff}.stat{fill:#e6fffb;stroke:#2dd4bf}.bad{fill:#fee2e2;stroke:#fca5a5}.ar{stroke:#2dd4bf;stroke-width:2;fill:none}</style><marker id="g1" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0 0l6 3-6 3z" fill="#2dd4bf"/></marker></defs><rect class="inst" x="12" y="20" width="330" height="76" rx="6"/><text class="t" x="24" y="40">instance member (since generics shipped)</text><text class="m" x="24" y="60">constrained. !!T · callvirt IFoo::Bar()</text><text class="x" x="24" y="78">dispatch on receiver; avoids boxing a value type</text><rect class="stat" x="12" y="110" width="330" height="76" rx="6"/><text class="t" x="24" y="130">static abstract member (C# 11)</text><text class="m" x="24" y="150">constrained. !!T · call T::op_Addition(..)</text><text class="x" x="24" y="168">no receiver; type arg carried into static dispatch</text><rect class="bad" x="372" y="56" width="316" height="86" rx="6"/><text class="t" x="384" y="78" fill="#b91c1c">naive decompiler assumes</text><text class="m" x="384" y="98">constrained. ⇒ callvirt only</text><text class="x" x="384" y="116">→ mis-reads the static call: phantom</text><text class="x" x="384" y="132">receiver, or an invented cast</text><path class="ar" d="M342 150 C356 150 360 120 372 110" marker-end="url(#g1)"/></svg></figure>

The two rows are the crux. The prefix is identical, but what it decorates — and what it means — is not.

## The IL the compiler emits

Build in Release and the body of `Sum<T>` reduces to this (annotated). Note there is no `callvirt` for the operator or the property; both are plain `call`s with a `constrained.` prefix carrying `!!T`, the method's own type parameter:

```
.method public hidebysig static !!T Sum<(class System.Numerics.INumber`1<!!T>) T>(
        valuetype System.ReadOnlySpan`1<!!T> values) cil managed
{
  // T total = T.Zero;
  constrained. !!T
  call       !!T System.Numerics.INumberBase`1<!!T>::get_Zero()
  stloc.0                                   // total

  // foreach (T v in values) total += v;
  ...                                        // span enumeration elided
  ldloc.0                                    // total
  ldloc.2                                    // v
  constrained. !!T
  call       !!T System.Numerics.IAdditionOperators`3<!!T,!!T,!!T>::op_Addition(!0, !1)
  stloc.0                                    // total = total + v
  ...
  ldloc.0
  ret
}
```

Two things to read off this. First, `T.Zero` is a call to `get_Zero` — the *getter* of a static property declared on `INumberBase<T>` — prefixed with `constrained. !!T`. Second, `total += v` is a call to `op_Addition`, the special-named operator method on `IAdditionOperators<T,T,T>`, again prefixed with `constrained. !!T`. The prefix is what tells the runtime: do not look for a static method on the interface itself; resolve it against the concrete type bound to `!!T` at this instantiation. Strip the prefix mentally and the instruction is nonsense — a `call` to an *abstract* interface method has no body to run. The prefix is the entire mechanism.

A decompiler that carries the old assumption — that `constrained.` only ever decorates a `callvirt`, as it has since C# 2 for value-type instance receivers — hits this `call` and has no rule for it. The typical failure modes are a phantom receiver (it tries to render the first stack operand as the thing being called into) or an invented cast to the interface (it "explains" the prefix as a conversion). Either way the output no longer matches the source and no longer recompiles.

## Reading it back

Glass handles the shape generically. When it sees `constrained. !!X` followed by a `call` to a method that is `static abstract` (or `static virtual`) on an interface, it records that the effective receiver type is the type parameter `!!X` and that dispatch is static. From there it resolves the *spelling* from the method's metadata:

- `get_Zero` / `get_One` carry the `specialname` flag and the property-accessor shape, so they read back as the `Zero` / `One` property: `T.Zero`.
- `op_Addition`, `op_Subtraction`, `op_Equality`, and the rest carry `specialname static` and a reserved operator name, so they read back as the operator: `total + v`.

Applying both, the recovered source is the method you started with:

```csharp
public static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    T total = T.Zero;
    foreach (T v in values)
        total += v;
    return total;
}
```

The constraint itself comes straight from the generic parameter's metadata — `(class System.Numerics.INumber`1<!!T>) T` in the `.method` signature becomes `where T : INumber<T>`. The `foreach` reconstructs from the span-enumeration pattern (its own decompilation problem, handled separately), but the generic-math core — `T.Zero` and `total += v` — hinges entirely on reading the constrained static call correctly.

## It is not just the BCL

Generic math is the famous customer, but `static abstract` members are a general feature, and the same IL shape shows up wherever you use one. A factory constraint, for instance:

```csharp
public interface IFactory<TSelf> where TSelf : IFactory<TSelf>
{
    static abstract TSelf Create();
}

public static TSelf MakeOne<TSelf>() where TSelf : IFactory<TSelf>
    => TSelf.Create();          // constrained. !!TSelf · call !!TSelf IFactory::Create()
```

`TSelf.Create()` compiles to the identical pattern — `constrained. !!TSelf` on a `call` to the static abstract `Create`. Because Glass keys off the instruction shape and the member flags, not a hard-coded list of numeric interfaces, your own `static abstract` members decompile exactly the way the framework's do: a static member access on the type parameter, spelled the way you wrote it. A tool that special-cased `INumber<T>` to pass its own test suite would render this one wrong; one that handles the ECMA mechanism handles all of them.

## What to take away

Static abstract interface members made generic math possible, and they did it by reusing the `constrained.` prefix on a plain `call` to carry a type argument into *static* dispatch — a shape that did not exist before C# 11. The naive rule "`constrained.` precedes `callvirt`" mis-reads it, producing phantom receivers or invented casts, so a correct decompiler has to recognise the constrained static call as its own pattern: receiver type is the type parameter, dispatch is static, and the spelling comes from the member's special name — `get_Zero` to `T.Zero`, `op_Addition` to `+`. Do that generically, off the instruction and the metadata flags rather than a list of known interfaces, and both the BCL's numeric stack and your own factory- or parser-style `static abstract` members read back into the source you wrote. For a related dispatch puzzle — recovering calls that carry no target token at all — see [decompiling function pointers and calli](https://delta1labs.com/blog/decompile-function-pointers-calli); for the metadata tables these flags live in, [.NET metadata tables explained](https://delta1labs.com/blog/dotnet-metadata-tables-explained).

