# Decompiling yield return: rebuilding iterator blocks from IL


`yield return` is the quiet sibling of `async`/`await`. Both compile to a state machine; both replace the method you wrote with a generated class and an integer-driven `MoveNext`. But iterators came first — they shipped in C# 2.0 — and the machine the compiler builds for them has its own distinctive shape. A decompiler that hands you that raw machine gives you a class implementing `IEnumerable<T>` *and* `IEnumerator<T>` at once, a `<>1__state` field, a `<>2__current` field, and a `MoveNext` full of gotos. What you wrote was four lines with two `yield return`s. Closing that gap is iterator reconstruction, and it is a great way to understand exactly what the C# compiler is doing on your behalf.

## What `yield` actually compiles to

Consider a method that cannot get much simpler:

```csharp
public IEnumerable<int> Evens(int limit)
{
    for (int i = 0; i < limit; i += 2)
        yield return i;
}
```

There is no loop in the compiled `Evens`. The method body is replaced by a one-liner that constructs a generated class and returns it:

```csharp
public IEnumerable<int> Evens(int limit)
{
    var d = new <Evens>d__0(-2);   // -2 = "fresh, not yet enumerated"
    d.<>3__limit = limit;          // parameters are copied into fields
    return d;
}
```

All the logic moves into that `<Evens>d__0` class, which implements both `IEnumerable<int>` and `IEnumerator<int>`. The loop variable `i` becomes a field so it survives between calls to `MoveNext`, and the `for` loop is turned inside-out into a state machine driven by `<>1__state`:

```csharp
bool IEnumerator.MoveNext()
{
    switch (<>1__state)
    {
        case 0:
            <>1__state = -1;
            <i>5__1 = 0;                       // i = 0
            goto check;
        case 1:
            <>1__state = -1;
            <i>5__1 += 2;                       // i += 2  (resume point)
            goto check;
        check:
            if (<i>5__1 < <>3__limit)
            {
                <>2__current = <i>5__1;         // the value of `yield return i`
                <>1__state = 1;                 // remember where to resume
                return true;                    // hand control back to the caller
            }
            return false;                       // enumeration finished
    }
    return false;
}
public int Current => <>2__current;
```

The two things a reader needs — "it yields `i`" and "then it continues the loop" — are split across a `switch`, two gotos, and a field assignment. That is faithful to the IL and almost unreadable, which is the problem.

## How Glass.NET puts it back together

As with async methods, Glass does not trust names — an obfuscator renames `<Evens>d__0`, `<>1__state` and `<i>5__1` to noise, and the `<>` convention is not guaranteed. It matches on the **contract and shape** that the runtime requires and the compiler cannot avoid emitting:

- A generated class that implements `IEnumerator<T>` (and usually `IEnumerable<T>` as well, so the result can be re-enumerated), with an `int` state field and a `Current`-backing field.
- A source method whose entire body is "construct this class, copy the parameters into its fields, return it."
- A `MoveNext` whose returns are `return true` (a value is ready, read `Current`) and `return false` (done) — the tell that distinguishes an iterator from an async `MoveNext`, which drives a *builder* and never returns a bool.

Reconstruction then runs in four moves:

1. **Map the states to resume points.** Each `case` in the `switch` is a point `MoveNext` can re-enter. Glass builds the state → offset map, exactly as it does for async, so it knows where each resumption lands inside the original control flow.
2. **Recognise the yield pattern.** The sequence *assign `<>2__current`, set `<>1__state = N`, `return true`* is the fingerprint of one `yield return`. Glass collapses the three steps back into `yield return <the value assigned to current>`. A bare `return false` with the terminal state becomes `yield break` or the natural end of the method.
3. **De-hoist the fields.** `<i>5__1` is the loop variable `i` lifted to a field so it survives suspension. Glass lifts it back to a local. `<>3__limit` is the captured parameter `limit`; it maps back to the parameter name.
4. **Re-roll the control flow.** With yields folded and states understood, the inside-out `switch`/`goto` machine is matched against structured-control-flow templates and re-rolled into the original `for` loop.

<figure><svg viewBox="0 0 640 176" role="img" aria-label="Iterator MoveNext switch and gotos folding back into a for loop with yield return" style="width:100%;height:auto;font-family:ui-sans-serif,system-ui,sans-serif"><rect x="0" y="0" width="640" height="176" fill="#0f172a" rx="10"/><text x="20" y="26" fill="#94a3b8" font-size="13">MoveNext — switch + gotos</text><rect x="24" y="42" width="168" height="24" rx="5" fill="#1e293b" stroke="#3b82f6"/><text x="108" y="58" fill="#e2e8f0" font-size="11" text-anchor="middle">case 0: i = 0 &#8594; check</text><rect x="24" y="72" width="168" height="24" rx="5" fill="#1e293b" stroke="#3b82f6"/><text x="108" y="88" fill="#e2e8f0" font-size="11" text-anchor="middle">case 1: i += 2 &#8594; check</text><rect x="24" y="102" width="168" height="24" rx="5" fill="#1e293b" stroke="#f59e0b"/><text x="108" y="118" fill="#e2e8f0" font-size="11" text-anchor="middle">check: current=i; return true</text><text x="300" y="86" fill="#64748b" font-size="22" text-anchor="middle">&#8594;</text><text x="300" y="106" fill="#64748b" font-size="10.5" text-anchor="middle">fold yield</text><rect x="396" y="56" width="220" height="64" rx="8" fill="#052e2b" stroke="#22c55e"/><text x="506" y="82" fill="#e2e8f0" font-size="12" text-anchor="middle">for (i = 0; i &lt; limit; i += 2)</text><text x="506" y="104" fill="#e2e8f0" font-size="12" text-anchor="middle">yield return i;</text><text x="300" y="158" fill="#64748b" font-size="10.5" text-anchor="middle">two states + a current-field assignment re-roll into one loop with one yield</text></svg></figure>

## Telling a yield apart from an await — and the lazy-execution trap

The most common mistake a weak decompiler makes here is confusing the two state machines. They look alike, but three signals keep them apart, and Glass checks all three: iterators implement `IEnumerator<T>` and their `MoveNext` returns `bool`; async methods implement `IAsyncStateMachine` and drive an `AsyncTaskMethodBuilder` that has no bool return. Get this wrong and you print an `await` where a `yield` belongs, which is not just ugly — it is *semantically false*.

There is a subtler trap that matters when you are reading someone else's binary to understand its behaviour: **iterators are lazy**. The body of `Evens` does not run when you call it — it runs as you enumerate. That is why the parameter copy happens in the stub and the real work happens in `MoveNext`. A decompiler that silently inlined the machine back into an eager method would hide a real bug class: an argument validated inside an iterator block is not checked until the first `MoveNext`, so the exception surfaces at the `foreach`, not at the call. Glass preserves the iterator structure precisely so that this laziness stays visible in the reconstructed source — because if you are reading the code to find out *why* a guard clause "isn't firing," that deferral is the answer.

## What obfuscation does and doesn't do to this

The same boundary applies as with async, and it is worth stating plainly rather than overselling the tool:

- **Renaming changes nothing.** `IEnumerator<T>` is a contract the runtime enforces; the bool-returning `MoveNext` and the `Current` property cannot be renamed away. Glass reconstructs a renamed iterator exactly as well as a named one — the identifiers it prints are synthesised, the control flow is real.
- **Control-flow flattening reaches inside `MoveNext`.** A tool like Nebula.NET can flatten the iterator's own `MoveNext`, dissolving its tidy `switch` into a dispatcher with opaque predicates. Glass still produces correct, compilable code, but the clean `for`-loop re-rolling degrades toward literal gotos the more the method was flattened. That is the honest trade: **decompilation always recovers behaviour; it recovers the original *shape* only as far as obfuscation left it intact.**

The practical upshot is the mirror image of the obfuscation story: if you ship un-obfuscated iterators, assume they decompile back to near-original source — a state machine is a compiler convenience, not protection. If the logic inside an iterator is worth protecting, it needs a real obfuscation pass on top, and even then the honest goal is cost, not impossibility.

*Glass.NET is a free, VS-native .NET decompiler that reconstructs iterators, async/await, and `yield` state machines back to readable C#. [Download Glass.NET](https://delta1labs.com/download) and open your first assembly.*

