Skip to content
Open
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
29 changes: 24 additions & 5 deletions docs/design/value-typed-emission.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,11 +1291,29 @@ change (instance 3); no claim that the frozen policy is a correctness proof.

Which up-front locals keep `= default` is decided before printing, not by the
writer (#2095). The analysis is the conservative structured definite-assignment
walk in `DefiniteAssignment` (the `#631` flow walk), unchanged: it yields the
walk in `DefiniteAssignment` (the `#631` flow walk): it yields the
locals that may be read before they are definitely assigned and gives up —
marking every local — on control flow it does not model, so it can only keep a
redundant initializer, never drop a required one.

Primitive-boolean short-circuit conditions carry separate definitely-assigned
sets for their true and false outcomes. Calls establish assignment only for
metadata-verified `out` arguments; a skipped call, an input reference, or an
unknown ref kind cannot.
A branch body consumes its outcome's set, and only paths that reach the normal
continuation participate in its join. Unsupported control flow retains the
conservative fallback, as do logical chains containing user-defined truthiness:
the printer can expose a user-typed operand and bind an overloaded `&&`/`||`,
which does not establish C#'s primitive-boolean outcome facts.
`ShortCircuitDefiniteAssignmentTests` gates both
outcomes, returning guards, nested short-circuit reads, and close declines.
The motivating real witness is `dotnet-inspect.any` 0.14.0,
`ILInspector.Decompiler.Pipeline.TypeRef::DisplayName`: the previous projection
added `string keyword = default` before a guarded `TryGetValue`, introducing
an `ldnull stloc` pair absent from the original. The pinned witness and a
compiler-produced returning-guard sample retain durable regression coverage;
Issue #9931 owns the focused slice; #9929 records the remaining fidelity families.

`DefiniteAssignmentPass` runs last in the emission tail, after residual storage
binding and every statement-rewriting pass, and issues
`IrFunction.ZeroInitializedLocals`: the analysis result minus the residual-bound
Expand Down Expand Up @@ -1323,10 +1341,11 @@ depend on it. Two pre-existing analysis limits are unchanged: a lambda that
shares its enclosing scope has no separate decision, and a local bound by an
`is` pattern counts as read before assignment.

On the pinned 14-assembly, 89,065-method corpus the decision reaches 10,077
methods (42,822 up-front declarations): 7,392 declarations in 1,781 methods
keep `= default`, 35,309 declare bare, and the residual exclusion changes 121
declarations in 73 methods; the analysis gives up in 474 methods. Computed at
At the Instance 3 introduction, the pinned 14-assembly, 89,065-method corpus
decision reached 10,077 methods (42,822 up-front declarations): 7,392
declarations in 1,781 methods kept `= default`, 35,309 declared bare, and the
residual exclusion changed 121 declarations in 73 methods; the analysis gave
up in 474 methods. Computed at
the host's tail, all 594 analysed nested bodies match what the writer computed
at print time. `DefiniteAssignmentPassTests` gates the host set, the residual
exclusion (synthetic and Newtonsoft.Json 13.0.4
Expand Down
77 changes: 65 additions & 12 deletions src/ILInspector.Decompiler/Pipeline/Ir/DefiniteAssignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,8 @@ void CheckReads(IrNode? node, HashSet<int> assigned)
return;
case Conditional conditional:
{
CheckReads(conditional.Condition, assigned);
var trueSet = new HashSet<int>(assigned);
var (trueSet, falseSet) = CheckCondition(conditional.Condition, assigned);
CheckReads(conditional.WhenTrue, trueSet);
var falseSet = new HashSet<int>(assigned);
CheckReads(conditional.WhenFalse, falseSet);
trueSet.IntersectWith(falseSet);
assigned.Clear();
Expand All @@ -94,9 +92,9 @@ void CheckReads(IrNode? node, HashSet<int> assigned)
}
case LogicalBinary logical:
{
CheckReads(logical.Left, assigned);
var rightSet = new HashSet<int>(assigned);
CheckReads(logical.Right, rightSet);
var (trueSet, falseSet) = CheckCondition(logical, assigned);
trueSet.IntersectWith(falseSet);
assigned.UnionWith(trueSet);
return;
}
case Coalesce coalesce:
Expand Down Expand Up @@ -207,6 +205,63 @@ void CheckReads(IrNode? node, HashSet<int> assigned)
CheckReads(child, assigned);
}

// These sets describe the paths that actually yield each boolean
// outcome. The right operand sees only the outcome that evaluates it;
// the skipped path participates in the other outcome's intersection.
(HashSet<int> WhenTrue, HashSet<int> WhenFalse) CheckCondition(
IrExpression condition, HashSet<int> entry)
{
if (condition is LogicalNot not)
{
var (whenTrue, whenFalse) = CheckCondition(not.Operand, entry);
return (whenFalse, whenTrue);
}
if (condition is LogicalBinary logical)
{
// Condition spelling can expose a user-typed operand by stripping
// op_True/op_False. Its overloaded &&/|| does not establish C#'s
// primitive-boolean outcome facts, even though this IR result is
// bool. Keep the unconditional, pre-existing short-circuit proof.
if (ContainsUserDefinedTruthiness(logical))
{
var unconditional = new HashSet<int>(entry);
CheckReads(logical.Left, unconditional);
CheckReads(logical.Right, new HashSet<int>(unconditional));
return (unconditional, new HashSet<int>(unconditional));
}
var (leftTrue, leftFalse) = CheckCondition(logical.Left, entry);
if (logical.Kind == LogicalKind.And)
{
var (rightTrue, rightFalse) = CheckCondition(logical.Right, leftTrue);
leftFalse.IntersectWith(rightFalse);
return (rightTrue, leftFalse);
}
else
{
var (rightTrue, rightFalse) = CheckCondition(logical.Right, leftFalse);
leftTrue.IntersectWith(rightTrue);
return (leftTrue, rightFalse);
}
}

var assigned = new HashSet<int>(entry);
CheckReads(condition, assigned);
return (assigned, new HashSet<int>(assigned));
}

static bool ContainsUserDefinedTruthiness(IrNode node)
{
if (node is Lambda or LocalFunctionStatement)
return false;
if (node is IrExpression expression
&& ShortCircuitFidelity.IsUserDefinedTruthiness(expression))
return true;
foreach (var child in node.Children)
if (ContainsUserDefinedTruthiness(child))
return true;
return false;
}

void CheckCall(MethodRef callee, IReadOnlyList<IrExpression> arguments, int parameterStart, HashSet<int> assigned)
{
if (parameterStart == 1 && arguments.Count > 0)
Expand Down Expand Up @@ -686,13 +741,11 @@ when structuredGotoReaching is not null

DefiniteFlow If(IfStatement branch, HashSet<int> assigned)
{
CheckReads(branch.Condition, assigned);
var thenSet = new HashSet<int>(assigned);
var (thenSet, elseSet) = CheckCondition(branch.Condition, assigned);
var thenFlow = Statement(branch.Then, thenSet);
if (!branch.HasElse)
return DefiniteFlow.FallThrough; // the then-arm may be skipped
var elseSet = new HashSet<int>(assigned);
var elseFlow = Statement(branch.Else!, elseSet);
var elseFlow = branch.HasElse
? Statement(branch.Else!, elseSet)
: DefiniteFlow.FallThrough;
var reaching = new List<HashSet<int>>();
if (thenFlow == DefiniteFlow.FallThrough)
reaching.Add(thenSet);
Expand Down
Loading