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() {