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)")
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:
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.
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.
-1.ExtMethod()receiver misbind (Gemini).IsSimpleAtomTextno longer treats a leading-as an atom, butOperandmarks anyConstantatomic 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-2IsSimpleAtomTextchange 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-bitintthere — 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.
- 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 (
RequiresCoercionvsIsResidual) verified to partition cleanly; wrappedCoercenodes stay out of the residual ledger viaIsAtTarget.
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.