From 914e2f4de404684b4865faa5591ef72cda35b9c1 Mon Sep 17 00:00:00 2001
From: Rich Lander <2608468+richlander@users.noreply.github.com>
Date: Sat, 10 Oct 2026 15:48:15 -0700
Subject: [PATCH 1/2] Preserve integer identity for Boolean arithmetic slots
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
docs/design/value-typed-emission.md | 54 ++++++++
.../Passes/ArithmeticBoolOperandPass.cs | 53 ++++++++
.../Pipeline/Passes/BooleanFoldingPass.cs | 18 +++
.../Pipeline/Passes/IrPass.cs | 8 +-
.../ArithmeticBoolOperandPassTests.cs | 118 ++++++++++++++++++
.../BooleanFoldingSlotIdentityTests.cs | 68 ++++++++++
.../ResidualSlotBindingPassTests.cs | 48 +++++--
7 files changed, 353 insertions(+), 14 deletions(-)
create mode 100644 tests/ILInspector.Decompiler.Tests/ArithmeticBoolOperandPassTests.cs
create mode 100644 tests/ILInspector.Decompiler.Tests/BooleanFoldingSlotIdentityTests.cs
diff --git a/docs/design/value-typed-emission.md b/docs/design/value-typed-emission.md
index b2fdf9985a..0a7fcbd0b3 100644
--- a/docs/design/value-typed-emission.md
+++ b/docs/design/value-typed-emission.md
@@ -367,6 +367,60 @@ bugs remain unchanged. Exact `reference-slot-target-binding` impact grows from
the retired population. The five non-reference `UnrenderableStoreType` split
webs remain outside this claim.
+### Bounded I4 slot identity preservation
+
+An evaluation-stack I4 carrier does not acquire Boolean storage identity merely
+because one store is the canonical integer zero or one and one load is observed
+as a condition. `BooleanFoldingPass` may retype the I4 family of one slot to
+`bool` only when **every** store in that family is already Boolean or a
+canonical `0`/`1` integer constant, every I4 load has Boolean-or-integer stack
+identity, and every such load is consumed where Boolean is valid. Proven
+reference and other non-I4 uses of the same numeric slot are unrelated reuse
+and remain outside the decision. A non-Boolean I4 producer, including a count,
+declines the whole retype; candidate subsets are not proof of the slot's
+Boolean identity.
+
+Integer arithmetic over comparison results supplies the complementary bounded
+case. IL comparison results are I4 values, so a compiler may add several of
+them, spill the sum, and later add that carrier to another integer. Before
+`ArithmeticBoolOperandPass` spells each Boolean operand as `condition ? 1 : 0`,
+it preserves the carrier's `int` result identity only when, within one function
+scope:
+
+- every store is an arithmetic Boolean/`int` mix that the pass itself will
+ normalize to `int` arithmetic; and
+- every load is Boolean-typed only because it is an operand of the same
+ integer-arithmetic domain.
+
+The load is then retagged `int` before arithmetic normalization. This keeps the
+producer normalization while preventing the slot load from becoming a second
+`? 1 : 0`, which would collapse a multi-term count to zero-or-one and sever it
+from its integer store.
+
+These two owner-local checks do not form a general bidirectional slot-typing
+rule. They exclude arbitrary Boolean producers, partial store or load subsets,
+mixed scopes, enum identity, and loads outside the owning Boolean or arithmetic
+domain. In particular, they do not admit
+`TypeRef.FunctionPointerParameterText`, whose Boolean branch joins an
+`ArgumentRefKind` producer and requires separate enum-specific proof.
+
+The pinned Microsoft.CodeAnalysis 5.0.0 witnesses are
+`ForEachLoopOperation.get_ChildOperationsCount` and
+`ForToLoopOperation.get_ChildOperationsCount` for arithmetic-result identity,
+and Microsoft.CodeAnalysis.CSharp 5.0.0
+`EarlyWellKnownAttributeBinder.CanBeValidAttributeArgument` plus the
+repository's `EhStructuringPass.InlineFilterLocalLoads` for complete-store
+Boolean-folding decline. Synthetic gates require all stores and loads to belong
+to the admitted domain and reject an arbitrary Boolean producer.
+
+On the fixed 14-assembly, 89,065-method corpus at base `de207b24a`, the four
+admitted webs retire. Residual-bound webs fall from 61 to 57, residual-bound
+locals from 104 to 96, affected methods from 35 to 31, and split webs from 30
+to 26. The `UnrenderableStoreType` split group falls from five webs and ten
+locals to the one two-local Boolean-to-enum web above. Every other residual
+group, zero late-decidable and nested webs, and the same four known pass bugs
+remain unchanged.
+
### Join target testimony
Join target compatibility is decided before printing (#2095). Its
diff --git a/src/ILInspector.Decompiler/Pipeline/Passes/ArithmeticBoolOperandPass.cs b/src/ILInspector.Decompiler/Pipeline/Passes/ArithmeticBoolOperandPass.cs
index 3c50664701..1ce824a8f1 100644
--- a/src/ILInspector.Decompiler/Pipeline/Passes/ArithmeticBoolOperandPass.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Passes/ArithmeticBoolOperandPass.cs
@@ -7,6 +7,9 @@ namespace ILInspector.Decompiler.Pipeline;
/// Materializes bool operands that feed integer arithmetic/shift opcodes.
/// IL comparisons carry an i4 0/1 value, so add/sub/...
/// can legally consume them, but C# has no int + bool spelling.
+/// When every store into one scoped slot is such arithmetic and every load
+/// returns to integer arithmetic, the slot carries the arithmetic's I4 result,
+/// not Boolean identity; preserve that result before materializing operands.
///
public sealed class ArithmeticBoolOperandPass : IIrPass
{
@@ -14,6 +17,8 @@ public sealed class ArithmeticBoolOperandPass : IIrPass
public void Run(IrFunction function, PassContext context)
{
+ PreserveIntegerSlotIdentity(function, context.Stepper);
+
var matches = function.Descendants
.OfType()
.Where(IsArithmeticBoolIntegerMix)
@@ -30,6 +35,54 @@ public void Run(IrFunction function, PassContext context)
}
}
+ static void PreserveIntegerSlotIdentity(IrFunction function, Stepper stepper)
+ {
+ IEnumerable scopes = new IrNode[] { function.Body }
+ .Concat(function.Descendants.OfType().Select(static lambda => (IrNode)lambda.Body))
+ .Concat(function.Descendants.OfType().Select(static local => (IrNode)local.Body));
+ var intType = TypeRef.CoreLib("System", "Int32");
+ foreach (var scope in scopes)
+ {
+ var nodes = CoercionSinks.ScopeNodes(scope).ToList();
+ var loads = nodes.OfType().ToLookup(static load => load.Slot);
+ foreach (var stores in nodes.OfType().GroupBy(static store => store.Slot))
+ {
+ var slotLoads = loads[stores.Key].ToList();
+ if (slotLoads.Count == 0
+ || !stores.All(static store =>
+ store.Value is Binary binary
+ && IsArithmeticBoolIntegerMix(binary)
+ && HasBooleanOrInt32Operands(binary))
+ || !slotLoads.All(static load =>
+ TypeFamilies.IsBoolean(load.Type)
+ && load.Parent is Binary binary
+ && IsArithmeticBoolIntegerMix(binary)
+ && HasBooleanOrInt32Operands(binary)))
+ {
+ continue;
+ }
+
+ stepper.StepOver($"preserve integer identity of arithmetic stack slot {stores.Key}", stores.First());
+ foreach (var load in slotLoads)
+ {
+ var replacement = new LoadStackSlot(load.Slot, intType);
+ replacement.InheritSourceOffset(load);
+ load.ReplaceWith(replacement);
+ }
+ }
+ }
+ }
+
+ static bool HasBooleanOrInt32Operands(Binary binary)
+ {
+ var intType = TypeRef.CoreLib("System", "Int32");
+ return IsBooleanOrInt32(binary.Left.ResultType)
+ && IsBooleanOrInt32(binary.Right.ResultType);
+
+ bool IsBooleanOrInt32(TypeRef? type)
+ => TypeFamilies.IsBoolean(type) || intType.Equals(type);
+ }
+
static IEnumerable BoolOperands(Binary binary)
{
if (TypeFamilies.IsBoolean(binary.Left.ResultType))
diff --git a/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs b/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
index 3a146c78af..f960e11ed3 100644
--- a/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
@@ -99,11 +99,17 @@ static bool MaterializeBooleanSlots(IrFunction function, Stepper stepper)
var candidateStores = stores
.Where(s => IsZeroOne(s.Value) || IsBool(s.Value.ResultType))
.ToList();
+ if (stores.Except(candidateStores).Any(store =>
+ SharesBooleanStorageFamily(store.Value.ResultType, function.TypeShapes)))
+ continue;
bool evidence = candidateStores.Any(s => s.Value is not Constant && IsBool(s.Value.ResultType));
var loads = loadsBySlot.GetValueOrDefault(slot) ?? [];
var candidateLoads = loads
.Where(load => IsBool(load.Type) || load.Type is { } type && TypeFamilies.IsIntegerLike(type))
.ToList();
+ if (loads.Except(candidateLoads).Any(load =>
+ SharesBooleanStorageFamily(load.Type, function.TypeShapes)))
+ continue;
bool allLoadsAcceptBool = candidateLoads.All(load => ConsumerAcceptsBool(function, load));
evidence = evidence || (candidateLoads.Count > 0 && allLoadsAcceptBool);
if (!evidence)
@@ -140,6 +146,18 @@ static bool MaterializeBooleanSlots(IrFunction function, Stepper stepper)
return changed;
}
+ static bool SharesBooleanStorageFamily(
+ TypeRef? type,
+ IReadOnlyDictionary shapes)
+ {
+ if (type is null)
+ return true;
+ if (TypeFamilies.Of(type) == StackFamily.I4)
+ return true;
+ return TypeFamilies.Of(type) is null
+ && !CoercionRendering.IsProvenReference(type, shapes);
+ }
+
static bool IsZeroOne(IrExpression expression) => expression is Constant { Value: int value } && value is 0 or 1;
static bool IsBool(TypeRef? type) => type is { Namespace: "System", Name: "Boolean" };
diff --git a/src/ILInspector.Decompiler/Pipeline/Passes/IrPass.cs b/src/ILInspector.Decompiler/Pipeline/Passes/IrPass.cs
index ed1bc5c4b3..28e03599f5 100644
--- a/src/ILInspector.Decompiler/Pipeline/Passes/IrPass.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Passes/IrPass.cs
@@ -279,9 +279,11 @@ public static class IrPasses
// flat it renders `b > false` (CS0019) and the method never binds. Runs
// after inlining so the bool operand is in its final position.
new BoolToIntNormalizationPass(),
- // A comparison or bool slot consumed by integer arithmetic is an i4 0/1
- // value in IL, but C# rejects `int + bool`; materialize the bool operand
- // before the printer sees the binary.
+ // A comparison consumed by integer arithmetic is an i4 0/1 value in
+ // IL, but C# rejects `int + bool`; materialize the bool operand before
+ // the printer sees the binary. When a complete slot carries the result
+ // of that arithmetic back into arithmetic, preserve its integer result
+ // identity before normalizing the producer operands.
new ArithmeticBoolOperandPass(),
// The operand-side complement of the bool→int normalization: a bool
// operand of an integer bitwise &/|/^ (a comparison result or bool slot
diff --git a/tests/ILInspector.Decompiler.Tests/ArithmeticBoolOperandPassTests.cs b/tests/ILInspector.Decompiler.Tests/ArithmeticBoolOperandPassTests.cs
new file mode 100644
index 0000000000..1f468d1846
--- /dev/null
+++ b/tests/ILInspector.Decompiler.Tests/ArithmeticBoolOperandPassTests.cs
@@ -0,0 +1,118 @@
+using ILInspector.Decompiler.Pipeline;
+
+namespace ILInspector.Decompiler.Tests;
+
+[Trait("Area", "Pass")]
+public class ArithmeticBoolOperandPassTests
+{
+ static readonly TypeRef Boolean = TypeRef.CoreLib("System", "Boolean");
+ static readonly TypeRef Int32 = TypeRef.CoreLib("System", "Int32");
+
+ [Fact]
+ public void ArithmeticSlotStoresPreserveIntegerLoadIdentity()
+ {
+ var body = new BlockContainer();
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, BoolAdd()));
+ block.Add(new Return(new Binary(
+ BinaryKind.Add,
+ isChecked: false,
+ isUnsigned: false,
+ new LoadStackSlot(0, Boolean),
+ new Constant(1, Int32))));
+ body.Add(block);
+ var function = Function(body);
+
+ new ArithmeticBoolOperandPass().Run(function, PassContext.None);
+
+ var store = Assert.IsType(block.Children[0]);
+ var storedAdd = Assert.IsType(store.Value);
+ Assert.IsType(storedAdd.Left);
+ Assert.IsType(storedAdd.Right);
+ var returnedAdd = Assert.IsType(Assert.IsType(block.Children[1]).Value);
+ Assert.Equal(Int32, Assert.IsType(returnedAdd.Left).Type);
+ function.CheckInvariant();
+ }
+
+ [Fact]
+ public void ArbitraryBooleanStoreDeclinesIntegerLoadIdentity()
+ {
+ var body = new BlockContainer();
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, BoolAdd()));
+ block.Add(new StoreStackSlot(0, new LoadArgument(0, "left", Boolean)));
+ block.Add(new Return(new Binary(
+ BinaryKind.Add,
+ isChecked: false,
+ isUnsigned: false,
+ new LoadStackSlot(0, Boolean),
+ new Constant(1, Int32))));
+ body.Add(block);
+ var function = Function(body);
+
+ new ArithmeticBoolOperandPass().Run(function, PassContext.None);
+
+ var returnedAdd = Assert.IsType(Assert.IsType(block.Children[2]).Value);
+ var normalized = Assert.IsType(returnedAdd.Left);
+ Assert.Equal(Boolean, Assert.IsType(normalized.Condition).Type);
+ function.CheckInvariant();
+ }
+
+ [Fact]
+ public void NonInt32ArithmeticDeclinesInt32SlotIdentity()
+ {
+ var uint32 = TypeRef.CoreLib("System", "UInt32");
+ var body = new BlockContainer();
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, new Binary(
+ BinaryKind.Add,
+ isChecked: false,
+ isUnsigned: false,
+ new LoadArgument(0, "left", Boolean),
+ new Constant(1u, uint32))));
+ block.Add(new Return(new Binary(
+ BinaryKind.Add,
+ isChecked: false,
+ isUnsigned: false,
+ new LoadStackSlot(0, Boolean),
+ new Constant(1u, uint32))));
+ body.Add(block);
+ var function = new IrFunction(
+ "M",
+ TypeRef.Definition("Synthetic", "Samples", "Owner"),
+ new MethodSignature(
+ uint32,
+ [new Parameter("left", Boolean)],
+ HasThis: false,
+ GenericParameterCount: 0),
+ [],
+ body);
+
+ new ArithmeticBoolOperandPass().Run(function, PassContext.None);
+
+ var returnedAdd = Assert.IsType(Assert.IsType(block.Children[1]).Value);
+ var normalized = Assert.IsType(returnedAdd.Left);
+ Assert.Equal(Boolean, Assert.IsType(normalized.Condition).Type);
+ function.CheckInvariant();
+ }
+
+ static Binary BoolAdd()
+ => new(
+ BinaryKind.Add,
+ isChecked: false,
+ isUnsigned: false,
+ new LoadArgument(0, "left", Boolean),
+ new LoadArgument(1, "right", Boolean));
+
+ static IrFunction Function(BlockContainer body)
+ => new(
+ "M",
+ TypeRef.Definition("Synthetic", "Samples", "Owner"),
+ new MethodSignature(
+ Int32,
+ [new Parameter("left", Boolean), new Parameter("right", Boolean)],
+ HasThis: false,
+ GenericParameterCount: 0),
+ [],
+ body);
+}
diff --git a/tests/ILInspector.Decompiler.Tests/BooleanFoldingSlotIdentityTests.cs b/tests/ILInspector.Decompiler.Tests/BooleanFoldingSlotIdentityTests.cs
new file mode 100644
index 0000000000..7b6e76af6d
--- /dev/null
+++ b/tests/ILInspector.Decompiler.Tests/BooleanFoldingSlotIdentityTests.cs
@@ -0,0 +1,68 @@
+using ILInspector.Decompiler.Pipeline;
+
+namespace ILInspector.Decompiler.Tests;
+
+[Trait("Area", "Pass")]
+public class BooleanFoldingSlotIdentityTests
+{
+ static readonly TypeRef Boolean = TypeRef.CoreLib("System", "Boolean");
+ static readonly TypeRef Int32 = TypeRef.CoreLib("System", "Int32");
+
+ [Fact]
+ public void MixedIntegerStoresDoNotAcquireBooleanIdentity()
+ {
+ var body = new BlockContainer();
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, new Constant(0, Int32)));
+ block.Add(new StoreStackSlot(0, new LoadArgument(0, "count", Int32)));
+ block.Add(new Return(new LogicalNot(new LoadStackSlot(0, Int32))));
+ body.Add(block);
+ var function = new IrFunction(
+ "M",
+ TypeRef.Definition("Synthetic", "Samples", "Owner"),
+ new MethodSignature(
+ Boolean,
+ [new Parameter("count", Int32)],
+ HasThis: false,
+ GenericParameterCount: 0),
+ [],
+ body);
+
+ new BooleanFoldingPass().Run(function, PassContext.None);
+
+ var constant = Assert.IsType(Assert.IsType(block.Children[0]).Value);
+ Assert.Equal(0, constant.Value);
+ Assert.Equal(Int32, constant.Type);
+ Assert.Equal(Int32, Assert.Single(function.Descendants.OfType()).Type);
+ function.CheckInvariant();
+ }
+
+ [Fact]
+ public void UnclassifiedLoadDeclinesBooleanIdentityForTheWholeSlot()
+ {
+ var body = new BlockContainer();
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, new Constant(0, Int32)));
+ block.Add(new StoreLocal(0, Boolean, new LoadStackSlot(0, Int32)));
+ block.Add(new StoreLocal(1, Int32, new LoadStackSlot(0, type: null)));
+ body.Add(block);
+ var function = new IrFunction(
+ "M",
+ TypeRef.Definition("Synthetic", "Samples", "Owner"),
+ new MethodSignature(
+ TypeRef.CoreLib("System", "Void"),
+ [],
+ HasThis: false,
+ GenericParameterCount: 0),
+ [Boolean, Int32],
+ body);
+
+ new BooleanFoldingPass().Run(function, PassContext.None);
+
+ var constant = Assert.IsType(Assert.IsType(block.Children[0]).Value);
+ Assert.Equal(0, constant.Value);
+ Assert.Equal(Int32, constant.Type);
+ Assert.Contains(function.Descendants.OfType(), static load => load.Type is null);
+ function.CheckInvariant();
+ }
+}
diff --git a/tests/ILInspector.Decompiler.Tests/ResidualSlotBindingPassTests.cs b/tests/ILInspector.Decompiler.Tests/ResidualSlotBindingPassTests.cs
index 06caeb3673..d3f5480a7c 100644
--- a/tests/ILInspector.Decompiler.Tests/ResidualSlotBindingPassTests.cs
+++ b/tests/ILInspector.Decompiler.Tests/ResidualSlotBindingPassTests.cs
@@ -329,12 +329,12 @@ [new Parameter("l", Int64)],
}
[Fact]
- public void RealSplitPieceReadWithoutReachingStoreDeclaresBare()
+ public void RealArithmeticBooleanSlotMaterializesAsInteger()
{
// Microsoft.CodeAnalysis 5.0.0, ForEachLoopOperation.get_ChildOperationsCount
- // (0x060029CB): slot 256 splits into an int piece and a Boolean piece
- // the return reads with no reaching store. The bare declaration keeps
- // the gap visible instead of compiling to a dropped term.
+ // (0x060029CB): the slot stores the I4 sum of two comparison results and
+ // is later added to another integer. Its imported Boolean load identity
+ // must not split the one integer value into disconnected residual pieces.
string path = Path.Combine(AppContext.BaseDirectory, "RealAssets", "PrimitiveJoin", "Microsoft.CodeAnalysis.dll");
using var source = MetadataSource.Open(path);
var function = IrImporter.Import(source, "Microsoft.CodeAnalysis.Operations.ForEachLoopOperation", "get_ChildOperationsCount");
@@ -342,15 +342,41 @@ public void RealSplitPieceReadWithoutReachingStoreDeclaresBare()
Assert.Equal(0x060029CB, function.MetadataToken);
IrPasses.Run(function, IrPasses.Default, PassContext.ForImport(reference => IrImporter.Import(source, reference), source.AreProvablyDisjoint));
- var pieces = function.ResidualSlotBindings.Where(static entry => entry.Value.Slot == 256).ToList();
- Assert.Equal(2, pieces.Count);
- Assert.All(pieces, static entry => Assert.Equal(ResidualSlotBindingKind.Split, entry.Value.Kind));
+ Assert.DoesNotContain(function.ResidualSlotBindings, static entry => entry.Value.Slot == 256);
string output = DecidedPrint.Print(function).Output!;
- Assert.Contains("bool S_256_1;", output);
- Assert.Contains("return (S_256_1 ? 1 : 0)", output);
- Assert.DoesNotContain("S_256_1 = default", output);
- Assert.DoesNotContain("= default", output);
+ Assert.Contains("int S_256 = (LoopControlVariable is not null ? 1 : 0) + (Collection is not null ? 1 : 0);", output);
+ Assert.Contains("return S_256 + V_0.Length + (base.Body is not null ? 1 : 0);", output);
+ Assert.DoesNotContain("S_256_1", output);
+ }
+
+ [Fact]
+ public void RealMixedIntegerStoresDoNotAcquireBooleanIdentity()
+ {
+ // Microsoft.CodeAnalysis.CSharp 5.0.0,
+ // EarlyWellKnownAttributeBinder.CanBeValidAttributeArgument: slot 1
+ // joins integer zero with an argument count and is observed as `== 0`.
+ // Boolean folding must preserve that integer storage identity.
+ string path = Path.Combine(
+ AppContext.BaseDirectory,
+ "RealAssets",
+ "ReferenceCoalesceLiveRange",
+ "Microsoft.CodeAnalysis.CSharp.dll");
+ using var source = MetadataSource.Open(path);
+ var function = IrImporter.Import(
+ source,
+ "Microsoft.CodeAnalysis.CSharp.EarlyWellKnownAttributeBinder",
+ "CanBeValidAttributeArgument");
+ Assert.NotNull(function);
+
+ IrPasses.Run(function, IrPasses.Default, PassContext.ForImport(reference => IrImporter.Import(source, reference), source.AreProvablyDisjoint));
+ Assert.DoesNotContain(function.ResidualSlotBindings, static entry => entry.Value.Slot == 1);
+ string output = DecidedPrint.Print(function).Output!;
+
+ Assert.Contains("int S_1;", output);
+ Assert.Contains("S_1 = 0;", output);
+ Assert.Contains("S_1 = S_0.Arguments.Count;", output);
+ Assert.Contains("return S_1 == 0;", output);
}
[Fact]
From a48017a48d7ae4e9e4ab0d15df5e20cb8103958a Mon Sep 17 00:00:00 2001
From: Rich Lander <2608468+richlander@users.noreply.github.com>
Date: Sat, 10 Oct 2026 17:59:10 -0700
Subject: [PATCH 2/2] Keep non-I4 slot reuse outside Boolean folding
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Pipeline/Passes/BooleanFoldingPass.cs | 2 +-
.../StackSlotReuseRenderingTests.cs | 27 +++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs b/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
index f960e11ed3..6355721b21 100644
--- a/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
+++ b/src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
@@ -105,7 +105,7 @@ static bool MaterializeBooleanSlots(IrFunction function, Stepper stepper)
bool evidence = candidateStores.Any(s => s.Value is not Constant && IsBool(s.Value.ResultType));
var loads = loadsBySlot.GetValueOrDefault(slot) ?? [];
var candidateLoads = loads
- .Where(load => IsBool(load.Type) || load.Type is { } type && TypeFamilies.IsIntegerLike(type))
+ .Where(load => IsBool(load.Type) || TypeFamilies.Of(load.Type) == StackFamily.I4)
.ToList();
if (loads.Except(candidateLoads).Any(load =>
SharesBooleanStorageFamily(load.Type, function.TypeShapes)))
diff --git a/tests/ILInspector.Decompiler.Tests/StackSlotReuseRenderingTests.cs b/tests/ILInspector.Decompiler.Tests/StackSlotReuseRenderingTests.cs
index 60d0a2b262..ed63e19d55 100644
--- a/tests/ILInspector.Decompiler.Tests/StackSlotReuseRenderingTests.cs
+++ b/tests/ILInspector.Decompiler.Tests/StackSlotReuseRenderingTests.cs
@@ -11,6 +11,7 @@ public class StackSlotReuseRenderingTests
static readonly TypeRef Exception = TypeRef.CoreLib("System", "Exception");
static readonly TypeRef Bool = TypeRef.CoreLib("System", "Boolean");
static readonly TypeRef Int32 = TypeRef.CoreLib("System", "Int32");
+ static readonly TypeRef Int64 = TypeRef.CoreLib("System", "Int64");
[Fact]
public void FoldedDiamondUsesPostDiamondStackSlotType()
@@ -69,6 +70,32 @@ public void BooleanSlotMaterializationIgnoresUnrelatedSameNumberLiveRange()
Assert.Contains("ConsumeBool(S_0_1 | other);", output);
}
+ [Fact]
+ public void BooleanSlotMaterializationIgnoresUnrelatedInt64LiveRange()
+ {
+ var consumeInt64 = new MethodRef(Holder, "ConsumeInt64", Void, [Int64], HasThis: false);
+ var consumeBool = new MethodRef(Holder, "ConsumeBool", Void, [Bool], HasThis: false);
+ var block = new Block(0);
+ block.Add(new StoreStackSlot(0, new LoadArgument(0, "value", Int64)));
+ block.Add(new ExpressionStatement(new Call(consumeInt64, isVirtual: false, [new LoadStackSlot(0, Int64)])));
+ block.Add(new StoreStackSlot(0, new Constant(0, Int32)));
+ block.Add(new StoreStackSlot(0, new LoadArgument(1, "flag", Bool)));
+ block.Add(new ExpressionStatement(new Call(
+ consumeBool,
+ isVirtual: false,
+ [new Binary(BinaryKind.Or, isChecked: false, isUnsigned: false, new LoadStackSlot(0, Int32), new LoadArgument(2, "other", Bool))])));
+ block.Add(new Return(null));
+
+ var function = Function(Void, block);
+ new BooleanFoldingPass().Run(function, PassContext.None);
+ var output = PrintBound(function).Output!;
+
+ Assert.Contains("long S_0 = value;", output);
+ Assert.Contains("bool S_0_1 = false;", output);
+ Assert.DoesNotContain("int S_0_1", output);
+ Assert.Contains("ConsumeBool(S_0_1 | other);", output);
+ }
+
[Fact]
public void BoolSlotWithIntegerTypedConsumerUsesBoolNameWhenTargetIsBool()
{