Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/design/value-typed-emission.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ namespace ILInspector.Decompiler.Pipeline;
/// Materializes <c>bool</c> operands that feed integer arithmetic/shift opcodes.
/// IL comparisons carry an <c>i4</c> 0/1 value, so <c>add</c>/<c>sub</c>/...
/// can legally consume them, but C# has no <c>int + bool</c> 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.
/// </summary>
public sealed class ArithmeticBoolOperandPass : IIrPass
{
public string Name => "arithmetic-bool-operand";

public void Run(IrFunction function, PassContext context)
{
PreserveIntegerSlotIdentity(function, context.Stepper);

var matches = function.Descendants
.OfType<Binary>()
.Where(IsArithmeticBoolIntegerMix)
Expand All @@ -30,6 +35,54 @@ public void Run(IrFunction function, PassContext context)
}
}

static void PreserveIntegerSlotIdentity(IrFunction function, Stepper stepper)
{
IEnumerable<IrNode> scopes = new IrNode[] { function.Body }
.Concat(function.Descendants.OfType<Lambda>().Select(static lambda => (IrNode)lambda.Body))
.Concat(function.Descendants.OfType<LocalFunctionStatement>().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<LoadStackSlot>().ToLookup(static load => load.Slot);
foreach (var stores in nodes.OfType<StoreStackSlot>().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<IrExpression> BoolOperands(Binary binary)
{
if (TypeFamilies.IsBoolean(binary.Left.ResultType))
Expand Down
20 changes: 19 additions & 1 deletion src/ILInspector.Decompiler/Pipeline/Passes/BooleanFoldingPass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
.Where(load => IsBool(load.Type) || TypeFamilies.Of(load.Type) == StackFamily.I4)
.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)
Expand Down Expand Up @@ -140,6 +146,18 @@ static bool MaterializeBooleanSlots(IrFunction function, Stepper stepper)
return changed;
}

static bool SharesBooleanStorageFamily(
TypeRef? type,
IReadOnlyDictionary<TypeRef, TypeShape> 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" };
Expand Down
8 changes: 5 additions & 3 deletions src/ILInspector.Decompiler/Pipeline/Passes/IrPass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
118 changes: 118 additions & 0 deletions tests/ILInspector.Decompiler.Tests/ArithmeticBoolOperandPassTests.cs
Original file line number Diff line number Diff line change
@@ -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<StoreStackSlot>(block.Children[0]);
var storedAdd = Assert.IsType<Binary>(store.Value);
Assert.IsType<Conditional>(storedAdd.Left);
Assert.IsType<Conditional>(storedAdd.Right);
var returnedAdd = Assert.IsType<Binary>(Assert.IsType<Return>(block.Children[1]).Value);
Assert.Equal(Int32, Assert.IsType<LoadStackSlot>(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<Binary>(Assert.IsType<Return>(block.Children[2]).Value);
var normalized = Assert.IsType<Conditional>(returnedAdd.Left);
Assert.Equal(Boolean, Assert.IsType<LoadStackSlot>(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<Binary>(Assert.IsType<Return>(block.Children[1]).Value);
var normalized = Assert.IsType<Conditional>(returnedAdd.Left);
Assert.Equal(Boolean, Assert.IsType<LoadStackSlot>(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);
}
Original file line number Diff line number Diff line change
@@ -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<Constant>(Assert.IsType<StoreStackSlot>(block.Children[0]).Value);
Assert.Equal(0, constant.Value);
Assert.Equal(Int32, constant.Type);
Assert.Equal(Int32, Assert.Single(function.Descendants.OfType<LoadStackSlot>()).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<Constant>(Assert.IsType<StoreStackSlot>(block.Children[0]).Value);
Assert.Equal(0, constant.Value);
Assert.Equal(Int32, constant.Type);
Assert.Contains(function.Descendants.OfType<LoadStackSlot>(), static load => load.Type is null);
function.CheckInvariant();
}
}
Loading