# Decompiling C# pattern matching and switch expressions


Modern C# leans hard on pattern matching — type patterns, property patterns, `and`/`or`/`not`, relational patterns, and the switch *expression* that ties them together. None of it exists in IL. A switch expression compiles to the same primitive control flow a hand-written `if`/`else` ladder would, plus one tell-tale detail the compiler adds for safety. Walking the lowering explains both what a decompiler shows you and why the reconstruction is sometimes a near-miss rather than a perfect echo of the source.

## A type-pattern switch

Start with a switch expression over types:

```csharp
public static decimal Price(object shape) => shape switch
{
    Circle c    => c.Radius * c.Radius * 3.14m,
    Rectangle r => r.Width * r.Height,
    null        => 0m,
    _           => throw new ArgumentException("unknown shape"),
};
```

There is no `switch` instruction for this. The compiler lowers each arm to a **type test**, and the only opcode that tests a type without throwing is `isinst` — it yields the value typed as the target or `null`, and the code branches on the result:

```
// shape is Circle c  →
ldarg.0
isinst   Circle          // Circle-or-null
dup
stloc.0                   // c
brtrue.s  CIRCLE_ARM      // matched → compute c.Radius * ...
// shape is Rectangle r →
ldarg.0
isinst   Rectangle
dup
stloc.1                   // r
brtrue.s  RECT_ARM
// shape is null →
ldarg.0
brfalse.s NULL_ARM
```

That is the signature of a type pattern: `isinst` immediately followed by a null-check branch (`brtrue`/`brfalse`). `castclass` never appears here, because a pattern that *doesn't* match must fall through, not throw — `isinst` gives exactly those semantics. A decompiler that sees `isinst; dup; stloc; brtrue` reads it straight back as `is Circle c`.

## The throw that gives it away

The `_` arm above threw explicitly, but watch what happens when a switch expression has no catch-all and the compiler can't prove it is exhaustive:

```csharp
public static string Name(DayOfWeek d) => d switch
{
    DayOfWeek.Saturday => "weekend",
    DayOfWeek.Sunday   => "weekend",
    _ when d >= 0      => "weekday",
};
```

Even here the compiler must guarantee the expression produces a value or fails deterministically, so it appends a hidden default arm:

```
// no arm matched →
ldloc.0                    // the switched value
newobj instance void [System.Runtime]System.Runtime.CompilerServices.SwitchExpressionException::.ctor(object)
throw
```

`SwitchExpressionException` is practically a fingerprint. Almost no hand-written code constructs and throws it; the compiler emits it *only* for a non-exhaustive switch expression. So a decompiler treats "branch tree that ends in a thrown `SwitchExpressionException`" as strong evidence the source was a switch *expression* — and renders a `switch { … }` rather than an `if`/`else` chain, which is what distinguishes the two in the output.

<svg viewBox="0 0 640 210" role="img" aria-label="A switch expression lowers to a sequence of pattern tests: each arm becomes a test that on success assigns the arm's value to a hidden temporary and branches to the end; if no arm matches, the compiler throws a SwitchExpressionException." xmlns="http://www.w3.org/2000/svg" style="max-width:640px;width:100%;height:auto;font-family:system-ui,sans-serif">
  <rect x="20" y="16" width="150" height="30" rx="5" fill="#1e293b" stroke="#475569"/>
  <text x="95" y="36" fill="#e2e8f0" font-size="12" text-anchor="middle">value switch</text>
  <line x1="95" y1="46" x2="95" y2="70" stroke="#64748b" stroke-width="1.5"/>
  <rect x="20" y="70" width="190" height="30" rx="5" fill="#0f172a" stroke="#475569"/>
  <text x="115" y="90" fill="#93c5fd" font-size="11" text-anchor="middle">test arm 1 (isinst / ceq)</text>
  <rect x="20" y="112" width="190" height="30" rx="5" fill="#0f172a" stroke="#475569"/>
  <text x="115" y="132" fill="#93c5fd" font-size="11" text-anchor="middle">test arm 2 …</text>
  <line x1="210" y1="85" x2="300" y2="85" stroke="#15803d" stroke-width="1.5"/>
  <line x1="210" y1="127" x2="300" y2="97" stroke="#15803d" stroke-width="1.5"/>
  <rect x="300" y="70" width="180" height="40" rx="5" fill="#14281e" stroke="#15803d"/>
  <text x="390" y="86" fill="#86efac" font-size="11" text-anchor="middle">match: result = arm value</text>
  <text x="390" y="101" fill="#86efac" font-size="11" text-anchor="middle">branch to end</text>
  <line x1="115" y1="142" x2="115" y2="166" stroke="#b45309" stroke-width="1.5" stroke-dasharray="4 3"/>
  <rect x="20" y="166" width="300" height="32" rx="5" fill="#2a1518" stroke="#b45309"/>
  <text x="170" y="186" fill="#fbbf24" font-size="11" text-anchor="middle">no match → throw SwitchExpressionException</text>
</svg>

## Property, relational and logical patterns

Richer patterns lower to *more of the same* — member reads and comparisons, combined with ordinary boolean branches. A property pattern with a relational and a logical operator:

```csharp
string Band(Measurement m) => m switch
{
    { Celsius: < 0 }            => "freezing",
    { Celsius: >= 0 and < 100 } => "liquid",
    { Celsius: >= 100 }         => "boiling",
    _                           => "unknown",
};
```

becomes a null-check on `m`, then reads of the `Celsius` property compared with the relational operators, `and` collapsing to sequential branches that must both pass:

```
ldloc.0                       // m
brfalse   UNKNOWN             // { ... } requires non-null
ldloc.0
callvirt  instance float64 Measurement::get_Celsius()
stloc.1                       // cache the property read
ldloc.1
ldc.r8    0.0
bge.s     CHECK_LIQUID        // not < 0 → try next arm
// → "freezing"
CHECK_LIQUID:
ldloc.1
ldc.r8    0.0
blt.s     CHECK_BOILING       // < 0 fails the >= 0 half
ldloc.1
ldc.r8    100.0
bge.s     CHECK_BOILING
// → "liquid"
```

Three details are worth noting. First, the property is read **once** and cached in a temporary even though three arms inspect it — the compiler hoists the read. Second, `and` is not a special opcode: it is simply two comparisons that both have to pass to reach the arm, and `or` is two that each branch to it. Third, `not` typically becomes an inverted branch condition. A decompiler can render these either as the reconstructed pattern (`>= 0 and < 100`) or as the expanded comparisons; both are faithful, which is why the surface syntax of the output sometimes differs from the source even though the behaviour is identical.

## `is` patterns outside a switch

The same lowering powers a standalone `is`:

```csharp
if (node is { Parent: not null } n && n.Depth > 3) { … }
```

The `{ Parent: not null }` becomes a null test on `node`, a read of `Parent`, and a null test on that; the declared `n` is the `isinst`/cast result stored to a local; `n.Depth > 3` is a plain comparison. There is no pattern metadata to recover — a decompiler reconstructs `is { … }` from the *shape* of the tests, which is why clean, regular lowering (as the C# compiler emits) reconstructs well, while heavily optimized or obfuscated branch trees may come back as explicit `if` checks instead.

## What this means when you read decompiled code

Pattern matching is pure syntactic sugar over branches, type tests and comparisons. Three fingerprints let a decompiler rebuild it: `isinst` + null-branch for **type patterns**, a hoisted member read feeding relational comparisons for **property/relational patterns**, and a trailing `throw new SwitchExpressionException` for a **non-exhaustive switch expression**. When the lowering is regular, the reconstruction is clean; when it isn't, you still get correct, readable control flow — just expressed as the `if`/`else` the sugar stood for.

Want to see the exact lowering for your own code? Open the assembly in [Glass.NET](https://delta1labs.com/glass), switch any method to the IL view, and compare it with the C# — the free way to watch pattern matching turn into branches. See also [decompiling records](https://delta1labs.com/blog/decompile-csharp-records) and [lambdas and closures](https://delta1labs.com/blog/decompile-lambdas-closures-display-classes) for more compiler fingerprints.

