Skip to content

Commit e61f45a

Browse files
authored
[ILLink analyzer] Add branch analysis (#94123)
This adds an understanding of `RuntimeFeature.IsDynamicCodeSupported` to the ILLink Roslyn analyzer. This is done by maintaining an additional state, `FeatureContext`, alongside the tracking done for local variables. The `FeatureContext` maintains a set of features known to be enabled at a given point. The lattice meet operator is set intersection, so that we get features enabled along all paths, and the top value is the set of all features. At the beginning of the analysis, each basic block starts out in the "top" state, but the entry point gets initialized to the empty set of features. Feature checks (calls to `RuntimeFeature.IsDynamicCodeSupported`) modify the state in the outgoing branch to track the fact that a given feature is enabled. When a branch operation is seen at the end of a basic block, the analysis examines it to see if it is a supported feature check. If so, it returns an abstract representation of the checked condition (which can be a boolean expression involving various features): `FeatureCheckValue`. Because this may be assumed to be true or false (depending on the outgoing branch), it has to track included and excluded features, unlike `FeatureContext`. The analysis then separates the output state for the basic block into two, one for the conditional successor, and one for the fall-through successor. It applies the `FeatureCheckValue` to each state separately, once assuming that the check is true, and once assuming that it's false, depending on the branch semantics, possibly modifying the `FeatureContext` in each branch. To support this, the dataflow state tracking is now done per branch, instead of per basic block. Previously we tracked the state at the end of each block; now we track the state at the input to each edge in the control-flow graph. The supported feature checks are hard-coded in the analyzer (this change isn't introducing any kind of attribute-based model to replace the feature xml files). For now the only supported check is `RuntimeFeature.IsDynamicCodeSupported`, but it should be easy to add new feature checks. This change includes testing of feature checks for `RequiresUnreferencedCodeAttribute` and `RequiresAssemblyFilesAttribute`, by including code in the analyzer that looks for specific feature guards in the test namespace. Happy to change to another approach if we don't like this. There are still some pieces of the Requires analyzer logic (generic instantiations and dynamic objects, for example) that need to be moved over to the dataflow analysis, so that the feature checks can act as guards for all of the related patterns. Until that is done, feature checks won't silence those particular warnings. I'll continue working on moving the remaining logic over, but I don't think it needs to block this change. Fixes dotnet/linker#2715 This tests the ILLink/ILCompiler behavior by adding XML substitutions that cause TestFeatures.IsUnreferencedCodeSupported and TestFeatures.IsAssemblyFilesSupported to be treated as constants. Most of the tests check IsUnreferencedCodeSupported and RequiresUnreferencedCode, since that logic is shared by all three tools. There are some differences in the current ILLink/ILC test infra: - ILC only allows embedded substitutions (not separate global substitutions) - ILLink doesn't allow modifying CoreLib from test embedded substitutions Because of this, we don't substitute IsDynamicCodeSupported for ILLink (it is already substituted by default for NativeAot). This matches the product behavior for ILLink, and leads to a small difference (extra warning) in the tests. We also substitute IsAssemblyFilesSupported so that NativeAot treats it as a constant, to keep it close to the product behavior and the analyzer behavior. For simplicity, this is done for both NativeAot and ILLink, even though it should really be treated the same as IsDynamicCodeCompiled for ILLink. This makes no difference in the test behavior because we don't test how ILLink specifically behaves with IsAssemblyFilesSupported.
1 parent c4d5505 commit e61f45a

32 files changed

+2167
-234
lines changed

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/MethodBodyScanner.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,32 +1194,32 @@ internal MultiValue DereferenceValue(
11941194
switch (value)
11951195
{
11961196
case FieldReferenceValue fieldReferenceValue:
1197-
dereferencedValue = MultiValue.Meet(
1197+
dereferencedValue = MultiValue.Union(
11981198
dereferencedValue,
11991199
CompilerGeneratedState.IsHoistedLocal(fieldReferenceValue.FieldDefinition)
12001200
? interproceduralState.GetHoistedLocal(new HoistedLocalKey(fieldReferenceValue.FieldDefinition))
12011201
: HandleGetField(methodBody, offset, fieldReferenceValue.FieldDefinition));
12021202
break;
12031203
case ParameterReferenceValue parameterReferenceValue:
1204-
dereferencedValue = MultiValue.Meet(
1204+
dereferencedValue = MultiValue.Union(
12051205
dereferencedValue,
12061206
GetMethodParameterValue(parameterReferenceValue.Parameter));
12071207
break;
12081208
case LocalVariableReferenceValue localVariableReferenceValue:
12091209
var valueBasicBlockPair = locals[localVariableReferenceValue.LocalIndex];
12101210
if (valueBasicBlockPair.HasValue)
1211-
dereferencedValue = MultiValue.Meet(dereferencedValue, valueBasicBlockPair.Value.Value);
1211+
dereferencedValue = MultiValue.Union(dereferencedValue, valueBasicBlockPair.Value.Value);
12121212
else
1213-
dereferencedValue = MultiValue.Meet(dereferencedValue, UnknownValue.Instance);
1213+
dereferencedValue = MultiValue.Union(dereferencedValue, UnknownValue.Instance);
12141214
break;
12151215
case ReferenceValue referenceValue:
12161216
throw new NotImplementedException($"Unhandled dereference of ReferenceValue of type {referenceValue.GetType().FullName}");
12171217
// Incomplete handling for ref values
12181218
case FieldValue fieldValue:
1219-
dereferencedValue = MultiValue.Meet(dereferencedValue, fieldValue);
1219+
dereferencedValue = MultiValue.Union(dereferencedValue, fieldValue);
12201220
break;
12211221
default:
1222-
dereferencedValue = MultiValue.Meet(dereferencedValue, value);
1222+
dereferencedValue = MultiValue.Union(dereferencedValue, value);
12231223
break;
12241224
}
12251225
}

src/tools/illink/src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
using System.Collections.Generic;
66
using System.Collections.Immutable;
77
using System.Diagnostics;
8+
using System.Diagnostics.CodeAnalysis;
89
using ILLink.Shared.DataFlow;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
911
using Microsoft.CodeAnalysis.FlowAnalysis;
1012

11-
using Predecessor = ILLink.Shared.DataFlow.IControlFlowGraph<
13+
using ControlFlowBranch = ILLink.Shared.DataFlow.IControlFlowGraph<
1214
ILLink.RoslynAnalyzer.DataFlow.BlockProxy,
1315
ILLink.RoslynAnalyzer.DataFlow.RegionProxy
14-
>.Predecessor;
16+
>.ControlFlowBranch;
1517

1618
namespace ILLink.RoslynAnalyzer.DataFlow
1719
{
@@ -21,12 +23,14 @@ namespace ILLink.RoslynAnalyzer.DataFlow
2123
// any kind of value equality for different block instances. In practice
2224
// this should be fine as long as we consistently use block instances from
2325
// a single ControlFlowGraph.
24-
public readonly record struct BlockProxy (BasicBlock Block)
26+
public readonly record struct BlockProxy (BasicBlock Block) : IBlock<BlockProxy>
2527
{
2628
public override string ToString ()
2729
{
2830
return base.ToString () + $"[{Block.Ordinal}]";
2931
}
32+
33+
public ConditionKind ConditionKind => (ConditionKind) Block.ConditionKind;
3034
}
3135

3236
public readonly record struct RegionProxy (ControlFlowRegion Region) : IRegion<RegionProxy>
@@ -51,25 +55,41 @@ public IEnumerable<BlockProxy> Blocks {
5155

5256
public BlockProxy Entry => new BlockProxy (ControlFlowGraph.EntryBlock ());
5357

58+
public static ControlFlowBranch? CreateProxyBranch (Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowBranch? branch)
59+
{
60+
if (branch == null)
61+
return null;
62+
63+
var finallyRegions = ImmutableArray.CreateBuilder<RegionProxy> ();
64+
foreach (var region in branch.FinallyRegions) {
65+
Debug.Assert (region != null);
66+
if (region == null)
67+
continue;
68+
finallyRegions.Add (new RegionProxy (region));
69+
}
70+
71+
// Destination might be null in a 'throw' branch.
72+
return new ControlFlowBranch (
73+
new BlockProxy (branch.Source),
74+
branch.Destination == null ? null : new BlockProxy (branch.Destination),
75+
finallyRegions.ToImmutable (),
76+
branch.IsConditionalSuccessor);
77+
}
78+
5479
// This is implemented by getting predecessors of the underlying Roslyn BasicBlock.
5580
// This is fine as long as the blocks come from the correct control-flow graph.
56-
public IEnumerable<Predecessor> GetPredecessors (BlockProxy block)
81+
public IEnumerable<ControlFlowBranch> GetPredecessors (BlockProxy block)
5782
{
5883
foreach (var predecessor in block.Block.Predecessors) {
59-
if (predecessor.FinallyRegions.IsEmpty) {
60-
yield return new Predecessor (new BlockProxy (predecessor.Source), ImmutableArray<RegionProxy>.Empty);
61-
continue;
62-
}
63-
var finallyRegions = ImmutableArray.CreateBuilder<RegionProxy> ();
64-
foreach (var region in predecessor.FinallyRegions) {
65-
if (region == null)
66-
throw new InvalidOperationException ();
67-
finallyRegions.Add (new RegionProxy (region));
68-
}
69-
yield return new Predecessor (new BlockProxy (predecessor.Source), finallyRegions.ToImmutable ());
84+
if (CreateProxyBranch (predecessor) is ControlFlowBranch branch)
85+
yield return branch;
7086
}
7187
}
7288

89+
public ControlFlowBranch? GetConditionalSuccessor (BlockProxy block) => CreateProxyBranch (block.Block.ConditionalSuccessor);
90+
91+
public ControlFlowBranch? GetFallThroughSuccessor (BlockProxy block) => CreateProxyBranch (block.Block.FallThroughSuccessor);
92+
7393
public bool TryGetEnclosingTryOrCatchOrFilter (BlockProxy block, out RegionProxy tryOrCatchOrFilterRegion)
7494
{
7595
return TryGetTryOrCatchOrFilter (block.Block.EnclosingRegion, out tryOrCatchOrFilterRegion);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
8+
using ILLink.Shared.DataFlow;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.FlowAnalysis;
11+
using Microsoft.CodeAnalysis.Operations;
12+
using ILLink.Shared.TypeSystemProxy;
13+
14+
using StateValue = ILLink.RoslynAnalyzer.DataFlow.LocalDataFlowState<
15+
ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>,
16+
ILLink.RoslynAnalyzer.DataFlow.FeatureContext,
17+
ILLink.Shared.DataFlow.ValueSetLattice<ILLink.Shared.DataFlow.SingleValue>,
18+
ILLink.RoslynAnalyzer.DataFlow.FeatureContextLattice
19+
>;
20+
21+
namespace ILLink.RoslynAnalyzer.DataFlow
22+
{
23+
// Visits a conditional expression to optionally produce a 'FeatureChecksValue'
24+
// (a set features that are checked to be enabled or disabled).
25+
// The visitor takes a LocalDataFlowState as an argument, allowing for checks that
26+
// depend on the current dataflow state.
27+
public class FeatureChecksVisitor : OperationVisitor<StateValue, FeatureChecksValue?>
28+
{
29+
DataFlowAnalyzerContext _dataFlowAnalyzerContext;
30+
31+
public FeatureChecksVisitor (DataFlowAnalyzerContext dataFlowAnalyzerContext)
32+
{
33+
_dataFlowAnalyzerContext = dataFlowAnalyzerContext;
34+
}
35+
36+
public override FeatureChecksValue? VisitArgument (IArgumentOperation operation, StateValue state)
37+
{
38+
return Visit (operation.Value, state);
39+
}
40+
41+
public override FeatureChecksValue? VisitPropertyReference (IPropertyReferenceOperation operation, StateValue state)
42+
{
43+
foreach (var analyzer in _dataFlowAnalyzerContext.EnabledRequiresAnalyzers) {
44+
if (analyzer.IsRequiresCheck (_dataFlowAnalyzerContext.Compilation, operation.Property)) {
45+
return new FeatureChecksValue (analyzer.FeatureName);
46+
}
47+
}
48+
49+
return null;
50+
}
51+
52+
public override FeatureChecksValue? VisitUnaryOperator (IUnaryOperation operation, StateValue state)
53+
{
54+
if (operation.OperatorKind is not UnaryOperatorKind.Not)
55+
return null;
56+
57+
FeatureChecksValue? context = Visit (operation.Operand, state);
58+
if (context == null)
59+
return null;
60+
61+
return context.Value.Negate ();
62+
}
63+
64+
public bool? GetLiteralBool (IOperation operation)
65+
{
66+
if (operation is not ILiteralOperation literal)
67+
return null;
68+
69+
return GetConstantBool (literal.ConstantValue);
70+
}
71+
72+
static bool? GetConstantBool (Optional<object?> constantValue)
73+
{
74+
if (!constantValue.HasValue || constantValue.Value is not bool value)
75+
return null;
76+
77+
return value;
78+
}
79+
80+
public override FeatureChecksValue? VisitBinaryOperator (IBinaryOperation operation, StateValue state)
81+
{
82+
bool expectEqual;
83+
switch (operation.OperatorKind) {
84+
case BinaryOperatorKind.Equals:
85+
expectEqual = true;
86+
break;
87+
case BinaryOperatorKind.NotEquals:
88+
expectEqual = false;
89+
break;
90+
default:
91+
return null;
92+
}
93+
94+
if (GetLiteralBool (operation.LeftOperand) is bool leftBool) {
95+
if (Visit (operation.RightOperand, state) is not FeatureChecksValue rightValue)
96+
return null;
97+
return leftBool == expectEqual
98+
? rightValue
99+
: rightValue.Negate ();
100+
}
101+
102+
if (GetLiteralBool (operation.RightOperand) is bool rightBool) {
103+
if (Visit (operation.LeftOperand, state) is not FeatureChecksValue leftValue)
104+
return null;
105+
return rightBool == expectEqual
106+
? leftValue
107+
: leftValue.Negate ();
108+
}
109+
110+
return null;
111+
}
112+
113+
public override FeatureChecksValue? VisitIsPattern (IIsPatternOperation operation, StateValue state)
114+
{
115+
if (GetExpectedValueFromPattern (operation.Pattern) is not bool patternValue)
116+
return null;
117+
118+
if (Visit (operation.Value, state) is not FeatureChecksValue value)
119+
return null;
120+
121+
return patternValue
122+
? value
123+
: value.Negate ();
124+
125+
126+
static bool? GetExpectedValueFromPattern (IPatternOperation pattern)
127+
{
128+
switch (pattern) {
129+
case IConstantPatternOperation constantPattern:
130+
return GetConstantBool (constantPattern.Value.ConstantValue);
131+
case INegatedPatternOperation negatedPattern:
132+
return !GetExpectedValueFromPattern (negatedPattern.Pattern);
133+
default:
134+
return null;
135+
}
136+
}
137+
}
138+
}
139+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using ILLink.Shared.DataFlow;
5+
6+
namespace ILLink.RoslynAnalyzer.DataFlow
7+
{
8+
// Represents the feature conditions checked in a conditional expression,
9+
// such as
10+
// Debug.Assert (RuntimeFeatures.IsDynamicCodeSupported)
11+
// or
12+
// if (!RuntimeFeatures.IsDynamicCodeSupported)
13+
// For now, this is only designed to track the built-in "features"/"capabilities"
14+
// like RuntimeFeatures.IsDynamicCodeSupported, where a true return value
15+
// indicates that a feature/capability is available.
16+
public record struct FeatureChecksValue : INegate<FeatureChecksValue>
17+
{
18+
public ValueSet<string> EnabledFeatures;
19+
public ValueSet<string> DisabledFeatures;
20+
21+
public FeatureChecksValue (string enabledFeature)
22+
{
23+
EnabledFeatures = new ValueSet<string> (enabledFeature);
24+
DisabledFeatures = ValueSet<string>.Empty;
25+
}
26+
27+
private FeatureChecksValue (ValueSet<string> enabled, ValueSet<string> disabled)
28+
{
29+
EnabledFeatures = enabled;
30+
DisabledFeatures = disabled;
31+
}
32+
33+
public FeatureChecksValue And (FeatureChecksValue other)
34+
{
35+
return new FeatureChecksValue (
36+
ValueSet<string>.Union (EnabledFeatures.DeepCopy (), other.EnabledFeatures.DeepCopy ()),
37+
ValueSet<string>.Union (DisabledFeatures.DeepCopy (), other.DisabledFeatures.DeepCopy ()));
38+
}
39+
40+
public FeatureChecksValue Or (FeatureChecksValue other)
41+
{
42+
return new FeatureChecksValue (
43+
ValueSet<string>.Intersection (EnabledFeatures.DeepCopy (), other.EnabledFeatures.DeepCopy ()),
44+
ValueSet<string>.Intersection (DisabledFeatures.DeepCopy (), other.DisabledFeatures.DeepCopy ()));
45+
}
46+
47+
public FeatureChecksValue Negate ()
48+
{
49+
return new FeatureChecksValue (DisabledFeatures.DeepCopy (), EnabledFeatures.DeepCopy ());
50+
}
51+
}
52+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Runtime.InteropServices;
8+
using ILLink.Shared;
9+
using ILLink.Shared.DataFlow;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
namespace ILLink.RoslynAnalyzer.DataFlow
13+
{
14+
public struct FeatureContext : IEquatable<FeatureContext>, IDeepCopyValue<FeatureContext>
15+
{
16+
// The set of features known to be enabled in this context.
17+
// Null represents "all possible features".
18+
public ValueSet<string>? EnabledFeatures;
19+
20+
public static readonly FeatureContext All = new FeatureContext (null);
21+
22+
public static readonly FeatureContext None = new FeatureContext (ValueSet<string>.Empty);
23+
24+
public FeatureContext (ValueSet<string>? enabled)
25+
{
26+
EnabledFeatures = enabled;
27+
}
28+
29+
public bool IsEnabled (string feature)
30+
{
31+
return EnabledFeatures == null || EnabledFeatures.Value.Contains (feature);
32+
}
33+
34+
public bool Equals (FeatureContext other) => EnabledFeatures == other.EnabledFeatures;
35+
public override bool Equals (object? obj) => obj is FeatureContext other && Equals (other);
36+
public override int GetHashCode () => EnabledFeatures?.GetHashCode () ?? typeof (FeatureContext).GetHashCode ();
37+
38+
public static bool operator == (FeatureContext left, FeatureContext right) => left.Equals (right);
39+
public static bool operator != (FeatureContext left, FeatureContext right) => !left.Equals (right);
40+
41+
public FeatureContext DeepCopy ()
42+
{
43+
return new FeatureContext (EnabledFeatures?.DeepCopy ());
44+
}
45+
46+
public FeatureContext Intersection (FeatureContext other)
47+
{
48+
if (EnabledFeatures == null)
49+
return other.DeepCopy ();
50+
if (other.EnabledFeatures == null)
51+
return this.DeepCopy ();
52+
return new FeatureContext (ValueSet<string>.Intersection (EnabledFeatures.Value, other.EnabledFeatures.Value));
53+
}
54+
55+
public FeatureContext Union (FeatureContext other)
56+
{
57+
if (EnabledFeatures == null)
58+
return this.DeepCopy ();
59+
if (other.EnabledFeatures == null)
60+
return other.DeepCopy ();
61+
return new FeatureContext (ValueSet<string>.Union (EnabledFeatures.Value, other.EnabledFeatures.Value));
62+
}
63+
}
64+
65+
public readonly struct FeatureContextLattice : ILattice<FeatureContext>
66+
{
67+
public FeatureContextLattice () { }
68+
69+
// The top value is the identity of meet (intersection), the set of all features.
70+
public FeatureContext Top { get; } = FeatureContext.All;
71+
72+
// We are interested in features which are known to be enabled for all paths through the CFG,
73+
// so the meet operator is set intersection.
74+
public FeatureContext Meet (FeatureContext left, FeatureContext right) => left.Intersection (right);
75+
}
76+
}

0 commit comments

Comments
 (0)