Skip to content

Instantly share code, notes, and snippets.

@richlander
Created July 2, 2026 20:57
Show Gist options
  • Select an option

  • Save richlander/821c2c84cbc1cabd844085d8a6653da1 to your computer and use it in GitHub Desktop.

Select an option

Save richlander/821c2c84cbc1cabd844085d8a6653da1 to your computer and use it in GitHub Desktop.
Adversarial review — value-typed emission slice 4 (fix/issue-2145-invariant-hardening): BLOCKING EnumOverUnderlyingFamily regression

Adversarial review — value-typed emission slice 4 (fix/issue-2145-invariant-hardening)

Verdict: BLOCKING — do not merge as-is. The slice-4 importer join-typing opener (EnumOverUnderlyingFamily) is unsound: it changes program semantics and marks the result Full fidelity.

Reviewers: GPT-5.5 and Gemini 3.1 Pro (both non-Claude per the AGENTS.md roster), each in an isolated worktree. They independently found the same root cause via different fixtures. All findings below were reproduced by the requester on clean base vs branch checkouts using DecompilerHarness --dump.

  • Base (merge base): c48af393
  • Branch head: 842dd686 ("Type enum joins at the importer (value-typed emission, step 4 opener)")

Root cause: EnumOverUnderlyingFamily (src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs)

In MergeValueTypes, an enum ⊔ integer control-flow join is now typed as the enum. The check compares only TypeFamilies.Of(underlying) == integerFamily, which collapses byte/short/int into the I4 family — so a narrow-backed enum joined with a full int is typed as the (narrow) enum, with no width check and no guarantee the downstream integer sink is coerced. Two manifestations:

Finding 1 — Value corruption, marked Full (GPT-5.5) — HIGH/CRITICAL

Fixture:

public enum BE : byte { A = 1, B = 2 }
public static class WidthJoin {
    public static object ByteEnumOrIntBox(bool c, BE e, int x) {
        int y = c ? (int)e : x;   // original boxes an int
        return y;
    }
}
Emitted C# Fidelity Runtime ByteEnumOrIntBox(false, BE.A, 300)
Base c48af393 return !c ? x : e; Full 300 (System.Int32) — int value preserved
Branch 842dd686 return !c ? ((BE)x) : e; Full 44 (BE) — 300 narrowed to (byte)300

The branch narrows the int path to (BE)x and changes the boxed type and value. It compiles and runs, so every other check passes — this is the "recompiles to a different program" failure class. Compile-back opcode diff confirms it: original box int vs recompiled conv.u1; box BE.

Finding 2 — Invalid output promoted PartialFull (Gemini 3.1 Pro) — HIGH

Fixture:

public enum ByteEnum : byte { A = 0, B = 1 }
public class C {
    public int M1(bool c, ByteEnum e) {
        int x = c ? (int)e : 1;
        System.Console.WriteLine(x);
        return x;
    }
}
Emitted C# Fidelity
Base c48af393 ByteEnum S_256 = !c ? ByteEnum.B : e; Console.WriteLine(S_256); return S_256; Partial (DEC0004 flags the unresolved join)
Branch 842dd686 identical text Full

Console.WriteLine(S_256) and return S_256 are CS0266 (ByteEnum does not implicitly convert to int). The enum-typed slot reaches the integer sink through a LoadStackSlot, which RequiresCoercion deliberately excludes, so no Coerce is inserted. The branch reclassifies a known-invalid method from Partial to Full — invalid-Full is the burndown's worst row class.

Suggested fix: do not promote the join when the enum's underlying width is narrower than the integer sibling (require exact width/type match), and/or ensure an enum-typed value reaching an integer sink is coerced even through LoadStackSlot.


Non-blocking / divergence

  • -1.ExtMethod() receiver misbind (Gemini). IsSimpleAtomText no longer treats a leading - as an atom, but Operand marks any Constant atomic unconditionally, so a raw negative constant receiver still prints -1.ExtMethod() (parses as -(1.ExtMethod()), CS0023). This is pre-existing (present on base too), not introduced by slice 4 — but it shows the #2145 item-2 IsSimpleAtomText change is an incomplete fix for that class. Worth its own issue.
  • Width divergence between reviewers. Gemini judged the mirror arm rule (CSharpPrinter.Numerics.cs:1212) width-safe because its target is always 32-bit int there — correct. GPT found the join-typing path (EnumOverUnderlyingFamily) unsafe. Both are right about their respective paths; the blocking bug is in join typing, not the arm rule.
  • Cross-assembly over-assertion (probed, not falsified). Gemini tried to break the aggressive cross-assembly Unknown-is-an-enum assumption with a non-enum struct meeting an integer and could not: ECMA-335 stack-merge verification does not merge a non-enum struct with an I4 value, so the structural assumption held for valid IL in the cases tested. Not proven exhaustively, but no counterexample found.

What passed

  • Branch build: dotnet build src/dotnet-inspect -c Release --configfile nuget.config — clean.
  • Focused coercion/enum/importer test subsets passed (GPT: 41 + 70; Gemini: coercion/audit).
  • Audit partitioning (RequiresCoercion vs IsResidual) verified to partition cleanly; wrapped Coerce nodes stay out of the residual ledger via IsAtTarget.

Process note

Each reviewer ran in its own worktree (/tmp/slice4-gemini, /tmp/slice4-gpt) per the newly-added AGENTS.md rule requiring isolated review workspaces, so neither reviewer's scratch fixtures contaminated the other's checkout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment