Skip to content

Commit 92786fd

Browse files
authored
Support assignment to multiple refs in trim analyzer (#90287)
Fixes dotnet/linker#3046 by adding analyzer support for assignments to multiple refs, for example: ```csharp (b ? ref f1 : ref f2) = v; ``` This also includes support for assignments by reference to array elements: ```csharp (b ? ref a1[0] : ref a2[0]) = v; ``` ILLink and ILCompiler produce a different warning in this case (`stdin.ref` results in the array values being replaced with `UnknownValue`). I attempted to quirk this in the analyzer, but this caused more problems in existing tests because it was difficult to make the quirk specific enough. Also fixes assignment to multiple arrays on the LHS (this appears to be an analysis hole in ILLink and ILCompiler): ```csharp (b ? a1 : a2)[0] = v; ```
1 parent 55a83d6 commit 92786fd

File tree

9 files changed

+293
-73
lines changed

9 files changed

+293
-73
lines changed

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

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace ILLink.RoslynAnalyzer.DataFlow
1010
{
1111
public readonly struct CapturedReferenceValue : IEquatable<CapturedReferenceValue>
1212
{
13-
public readonly IOperation? Reference;
13+
public readonly IOperation Reference;
1414

1515
public CapturedReferenceValue (IOperation operation)
1616
{
@@ -48,23 +48,4 @@ public override bool Equals (object obj)
4848
public override int GetHashCode ()
4949
=> Reference?.GetHashCode () ?? 0;
5050
}
51-
52-
53-
public struct CapturedReferenceLattice : ILattice<CapturedReferenceValue>
54-
{
55-
public CapturedReferenceValue Top => default;
56-
57-
public CapturedReferenceValue Meet (CapturedReferenceValue left, CapturedReferenceValue right)
58-
{
59-
if (left.Equals (right))
60-
return left;
61-
if (left.Reference == null)
62-
return right;
63-
if (right.Reference == null)
64-
return left;
65-
// Both non-null and different shouldn't happen.
66-
// We assume that a flow capture can capture only a single property.
67-
throw new InvalidOperationException ();
68-
}
69-
}
7051
}

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
#nullable disable
4+
#nullable enable
55

66
using System.Collections.Generic;
77
using System.Collections.Immutable;
@@ -15,7 +15,7 @@
1515

1616
namespace ILLink.RoslynAnalyzer.DataFlow
1717
{
18-
// Copied from https://github.com/dotnet/roslyn/blob/c8ebc8682889b395fcb84c85bf4ff54577377d26/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/LValueFlowCaptureProvider.cs
18+
// Adapted from https://github.com/dotnet/roslyn/blob/c8ebc8682889b395fcb84c85bf4ff54577377d26/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/LValueFlowCaptureProvider.cs
1919
/// <summary>
2020
/// Helper class to detect <see cref="IFlowCaptureOperation"/>s that are l-value captures.
2121
/// L-value captures are essentially captures of a symbol's location/address.
@@ -38,6 +38,22 @@ namespace ILLink.RoslynAnalyzer.DataFlow
3838
/// </remarks>
3939
internal static class LValueFlowCapturesProvider
4040
{
41+
static bool IsLValueFlowCapture (IFlowCaptureReferenceOperation flowCaptureReference, out IAssignmentOperation? assignment)
42+
{
43+
assignment = flowCaptureReference.Parent as IAssignmentOperation;
44+
if (assignment?.Target == flowCaptureReference)
45+
return true;
46+
47+
if (flowCaptureReference.Parent is IArrayElementReferenceOperation arrayAlementRef) {
48+
assignment = arrayAlementRef.Parent as IAssignmentOperation;
49+
if (assignment?.Target == arrayAlementRef)
50+
return true;
51+
}
52+
53+
assignment = null;
54+
return flowCaptureReference.IsInLeftOfDeconstructionAssignment (out _);
55+
}
56+
4157
public static ImmutableDictionary<CaptureId, FlowCaptureKind> CreateLValueFlowCaptures (ControlFlowGraph cfg)
4258
{
4359
// This method identifies flow capture reference operations that are target of an assignment
@@ -47,15 +63,13 @@ public static ImmutableDictionary<CaptureId, FlowCaptureKind> CreateLValueFlowCa
4763
// the flow graph. Specifically, for an ICoalesceOperation a flow capture acts
4864
// as both an r-value and l-value flow capture.
4965

50-
ImmutableDictionary<CaptureId, FlowCaptureKind>.Builder lvalueFlowCaptureIdBuilder = null;
66+
ImmutableDictionary<CaptureId, FlowCaptureKind>.Builder? lvalueFlowCaptureIdBuilder = null;
5167
var rvalueFlowCaptureIds = new HashSet<CaptureId> ();
5268

5369
foreach (var flowCaptureReference in cfg.DescendantOperations<IFlowCaptureReferenceOperation> (OperationKind.FlowCaptureReference)) {
54-
if (flowCaptureReference.Parent is IAssignmentOperation assignment &&
55-
assignment.Target == flowCaptureReference ||
56-
flowCaptureReference.IsInLeftOfDeconstructionAssignment (out _)) {
70+
if (IsLValueFlowCapture (flowCaptureReference, out IAssignmentOperation? assignment)) {
5771
lvalueFlowCaptureIdBuilder ??= ImmutableDictionary.CreateBuilder<CaptureId, FlowCaptureKind> ();
58-
var captureKind = flowCaptureReference.Parent.IsAnyCompoundAssignment () || rvalueFlowCaptureIds.Contains (flowCaptureReference.Id)
72+
var captureKind = assignment?.IsAnyCompoundAssignment () == true || rvalueFlowCaptureIds.Contains (flowCaptureReference.Id)
5973
? FlowCaptureKind.LValueAndRValueCapture
6074
: FlowCaptureKind.LValueCapture;
6175
lvalueFlowCaptureIdBuilder.Add (flowCaptureReference.Id, captureKind);

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

Lines changed: 103 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void Transfer (BlockProxy block, LocalDataFlowState<TValue, TValueLattice
9696

9797
public abstract TValue HandleArrayElementRead (TValue arrayValue, TValue indexValue, IOperation operation);
9898

99-
public abstract void HandleArrayElementWrite (TValue arrayValue, TValue indexValue, TValue valueToWrite, IOperation operation);
99+
public abstract void HandleArrayElementWrite (TValue arrayValue, TValue indexValue, TValue valueToWrite, IOperation operation, bool merge);
100100

101101
// This takes an IOperation rather than an IReturnOperation because the return value
102102
// may (must?) come from BranchValue of an operation whose FallThroughSuccessor is the exit block.
@@ -139,7 +139,7 @@ TValue GetLocal (ILocalReferenceOperation operation, LocalDataFlowState<TValue,
139139
return state.Get (local);
140140
}
141141

142-
void SetLocal (ILocalReferenceOperation operation, TValue value, LocalDataFlowState<TValue, TValueLattice> state)
142+
void SetLocal (ILocalReferenceOperation operation, TValue value, LocalDataFlowState<TValue, TValueLattice> state, bool merge = false)
143143
{
144144
var local = new LocalKey (operation.Local);
145145
if (IsReferenceToCapturedVariable (operation))
@@ -149,27 +149,14 @@ void SetLocal (ILocalReferenceOperation operation, TValue value, LocalDataFlowSt
149149
if (InterproceduralState.TrySetHoistedLocal (local, value))
150150
return;
151151

152-
state.Set (local, value);
152+
var newValue = merge
153+
? state.Lattice.Lattice.ValueLattice.Meet (state.Get (local), value)
154+
: value;
155+
state.Set (local, newValue);
153156
}
154157

155-
public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
158+
TValue ProcessSingleTargetAssignment (IOperation targetOperation, ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state, bool merge)
156159
{
157-
var targetOperation = operation.Target;
158-
if (targetOperation is IFlowCaptureReferenceOperation flowCaptureReference) {
159-
Debug.Assert (IsLValueFlowCapture (flowCaptureReference.Id));
160-
Debug.Assert (!flowCaptureReference.GetValueUsageInfo (Method).HasFlag (ValueUsageInfo.Read));
161-
var capturedReference = state.Current.CapturedReferences.Get (flowCaptureReference.Id).Reference;
162-
targetOperation = capturedReference;
163-
if (targetOperation == null)
164-
throw new InvalidOperationException ();
165-
166-
// Note: technically we should avoid visiting the target operation below when assigning to a flow capture reference,
167-
// because this should be done when the capture is created. For example, a flow capture used as both an LValue and a RValue
168-
// should only evaluate the expression that computes the object instance of a property reference once.
169-
// However, we just visit the instance again below for simplicity. This could be generalized if we encounter a dataflow
170-
// behavior where this makes a difference.
171-
}
172-
173160
switch (targetOperation) {
174161
case IFieldReferenceOperation:
175162
case IParameterReferenceOperation: {
@@ -237,17 +224,52 @@ public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operati
237224
// TODO: when setting a property in an attribute, target is an IPropertyReference.
238225
case ILocalReferenceOperation localRef: {
239226
TValue value = Visit (operation.Value, state);
240-
SetLocal (localRef, value, state);
227+
SetLocal (localRef, value, state, merge);
241228
return value;
242229
}
243230
case IArrayElementReferenceOperation arrayElementRef: {
244231
if (arrayElementRef.Indices.Length != 1)
245232
break;
246233

247-
TValue arrayRef = Visit (arrayElementRef.ArrayReference, state);
248-
TValue index = Visit (arrayElementRef.Indices[0], state);
249-
TValue value = Visit (operation.Value, state);
250-
HandleArrayElementWrite (arrayRef, index, value, operation);
234+
// Similarly to VisitSimpleAssignment, this needs to handle cases where the array reference
235+
// is a captured variable, even if the target of the assignment (the array element reference) is not.
236+
237+
TValue arrayRef;
238+
TValue index;
239+
TValue value;
240+
if (arrayElementRef.ArrayReference is not IFlowCaptureReferenceOperation captureReference) {
241+
arrayRef = Visit (arrayElementRef.ArrayReference, state);
242+
index = Visit (arrayElementRef.Indices[0], state);
243+
value = Visit (operation.Value, state);
244+
HandleArrayElementWrite (arrayRef, index, value, operation, merge: merge);
245+
return value;
246+
}
247+
248+
index = Visit (arrayElementRef.Indices[0], state);
249+
value = Visit (operation.Value, state);
250+
251+
var capturedReferences = state.Current.CapturedReferences.Get (captureReference.Id);
252+
if (!capturedReferences.HasMultipleValues) {
253+
// Single captured reference. Treat this as an overwriting assignment,
254+
// unless the caller already told us to merge values because this is an
255+
// assignment to one of multiple captured array element references.
256+
var enumerator = capturedReferences.GetEnumerator ();
257+
enumerator.MoveNext ();
258+
var capture = enumerator.Current;
259+
arrayRef = Visit (capture.Reference, state);
260+
HandleArrayElementWrite (arrayRef, index, value, operation, merge: merge);
261+
return value;
262+
}
263+
264+
// The capture id may have captured multiple references, as in:
265+
// (b ? arr1 : arr2)[0] = value;
266+
// We treat this as possible write to each of the captured references,
267+
// which requires merging with the previous values of each.
268+
269+
foreach (var capture in state.Current.CapturedReferences.Get (captureReference.Id)) {
270+
arrayRef = Visit (capture.Reference, state);
271+
HandleArrayElementWrite (arrayRef, index, value, operation, merge: true);
272+
}
251273
return value;
252274
}
253275
case IDiscardOperation:
@@ -293,8 +315,50 @@ public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operati
293315
return Visit (operation.Value, state);
294316
}
295317

296-
// Similar to VisitLocalReference
297-
public override TValue VisitFlowCaptureReference (IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
318+
public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
319+
{
320+
var targetOperation = operation.Target;
321+
if (targetOperation is not IFlowCaptureReferenceOperation flowCaptureReference)
322+
return ProcessSingleTargetAssignment (targetOperation, operation, state, merge: false);
323+
324+
// Note: technically we should avoid visiting the target operation in ProcessNonCapturedAssignment when assigning
325+
// to a flow capture reference, because this should be done when the capture is created.
326+
// For example, a flow capture used as both an LValue and a RValue should only evaluate the expression that
327+
// computes the object instance of a property reference once. However, we just visit the instance again below
328+
// for simplicity. This could be generalized if we encounter a dataflow behavior where this makes a difference.
329+
330+
Debug.Assert (IsLValueFlowCapture (flowCaptureReference.Id));
331+
Debug.Assert (!flowCaptureReference.GetValueUsageInfo (Method).HasFlag (ValueUsageInfo.Read));
332+
var capturedReferences = state.Current.CapturedReferences.Get (flowCaptureReference.Id);
333+
if (!capturedReferences.HasMultipleValues) {
334+
// Single captured reference. Treat this as an overwriting assignment.
335+
var enumerator = capturedReferences.GetEnumerator ();
336+
enumerator.MoveNext ();
337+
targetOperation = enumerator.Current.Reference;
338+
return ProcessSingleTargetAssignment (targetOperation, operation, state, merge: false);
339+
}
340+
341+
// The capture id may have captured multiple references, as in:
342+
// (b ? ref v1 : ref v2) = value;
343+
// We treat this as a possible write to each of the captured references,
344+
// which requires merging with the previous values of each.
345+
346+
// Note: technically this should only visit the RHS of the assignment once.
347+
// For now we visit the RHS in ProcessSingleTargetAssignment for simplicity, and
348+
// rely on the warning deduplication to prevent this from producing multiple warnings
349+
// if the RHS has dataflow warnings.
350+
351+
TValue value = TopValue;
352+
foreach (var capturedReference in capturedReferences) {
353+
targetOperation = capturedReference.Reference;
354+
var singleValue = ProcessSingleTargetAssignment (targetOperation, operation, state, merge: true);
355+
value = LocalStateLattice.Lattice.ValueLattice.Meet (value, singleValue);
356+
}
357+
358+
return value;
359+
}
360+
361+
TValue GetFlowCaptureValue (IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
298362
{
299363
if (!operation.GetValueUsageInfo (Method).HasFlag (ValueUsageInfo.Read)) {
300364
// There are known cases where this assert doesn't hold, because LValueFlowCaptureProvider
@@ -304,10 +368,21 @@ public override TValue VisitFlowCaptureReference (IFlowCaptureReferenceOperation
304368
return TopValue;
305369
}
306370

307-
Debug.Assert (IsRValueFlowCapture (operation.Id));
371+
// This assert is incorrect for cases like (b ? arr1 : arr2)[0] = v;
372+
// Here the ValueUsageInfo shows that the value usage is for reading (this is probably wrong!)
373+
// but the value is actually an LValueFlowCapture.
374+
// Let's just disable the assert for now.
375+
// Debug.Assert (IsRValueFlowCapture (operation.Id));
376+
308377
return state.Get (new LocalKey (operation.Id));
309378
}
310379

380+
// Similar to VisitLocalReference
381+
public override TValue VisitFlowCaptureReference (IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
382+
{
383+
return GetFlowCaptureValue (operation, state);
384+
}
385+
311386
// Similar to VisitSimpleAssignment when assigning to a local, but for values which are captured without a
312387
// corresponding local variable. The "flow capture" is like a local assignment, and the "flow capture reference"
313388
// is like a local reference.

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,22 @@ public struct LocalState<TValue> : IEquatable<LocalState<TValue>>
4343
// Stores any operations which are captured by reference in a FlowCaptureOperation.
4444
// Only stores captures which are assigned through. Captures of the values of operations
4545
// are tracked as part of the dictionary of values, keyed by LocalKey.
46-
public DefaultValueDictionary<CaptureId, CapturedReferenceValue> CapturedReferences;
46+
public DefaultValueDictionary<CaptureId, ValueSet<CapturedReferenceValue>> CapturedReferences;
4747

4848
public LocalState (TValue defaultValue)
4949
: this (new DefaultValueDictionary<LocalKey, TValue> (defaultValue),
50-
new DefaultValueDictionary<CaptureId, CapturedReferenceValue> (default (CapturedReferenceValue)))
50+
new DefaultValueDictionary<CaptureId, ValueSet<CapturedReferenceValue>> (default (ValueSet<CapturedReferenceValue>)))
5151
{
5252
}
5353

54-
public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary, DefaultValueDictionary<CaptureId, CapturedReferenceValue> capturedReferences)
54+
public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary, DefaultValueDictionary<CaptureId, ValueSet<CapturedReferenceValue>> capturedReferences)
5555
{
5656
Dictionary = dictionary;
5757
CapturedReferences = capturedReferences;
5858
}
5959

6060
public LocalState (DefaultValueDictionary<LocalKey, TValue> dictionary)
61-
: this (dictionary, new DefaultValueDictionary<CaptureId, CapturedReferenceValue> (default (CapturedReferenceValue)))
61+
: this (dictionary, new DefaultValueDictionary<CaptureId, ValueSet<CapturedReferenceValue>> (default (ValueSet<CapturedReferenceValue>)))
6262
{
6363
}
6464

@@ -83,12 +83,12 @@ public override int GetHashCode ()
8383
where TValueLattice : ILattice<TValue>
8484
{
8585
public readonly DictionaryLattice<LocalKey, TValue, TValueLattice> Lattice;
86-
public readonly DictionaryLattice<CaptureId, CapturedReferenceValue, CapturedReferenceLattice> CapturedReferenceLattice;
86+
public readonly DictionaryLattice<CaptureId, ValueSet<CapturedReferenceValue>, ValueSetLattice<CapturedReferenceValue>> CapturedReferenceLattice;
8787

8888
public LocalStateLattice (TValueLattice valueLattice)
8989
{
9090
Lattice = new DictionaryLattice<LocalKey, TValue, TValueLattice> (valueLattice);
91-
CapturedReferenceLattice = new DictionaryLattice<CaptureId, CapturedReferenceValue, CapturedReferenceLattice> (default (CapturedReferenceLattice));
91+
CapturedReferenceLattice = new DictionaryLattice<CaptureId, ValueSet<CapturedReferenceValue>, ValueSetLattice<CapturedReferenceValue>> (default (ValueSetLattice<CapturedReferenceValue>));
9292
Top = new (Lattice.Top);
9393
}
9494

src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/TrimAnalysisVisitor.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,20 +219,19 @@ public override MultiValue HandleArrayElementRead (MultiValue arrayValue, MultiV
219219
return result.Equals (TopValue) ? UnknownValue.Instance : result;
220220
}
221221

222-
public override void HandleArrayElementWrite (MultiValue arrayValue, MultiValue indexValue, MultiValue valueToWrite, IOperation operation)
222+
public override void HandleArrayElementWrite (MultiValue arrayValue, MultiValue indexValue, MultiValue valueToWrite, IOperation operation, bool merge)
223223
{
224224
int? index = indexValue.AsConstInt ();
225225
foreach (var arraySingleValue in arrayValue) {
226226
if (arraySingleValue is ArrayValue arr) {
227227
if (index == null) {
228228
// Reset the array to all unknowns - since we don't know which index is being assigned
229229
arr.IndexValues.Clear ();
230-
} else {
231-
if (arr.IndexValues.TryGetValue (index.Value, out _)) {
232-
arr.IndexValues[index.Value] = ArrayValue.SanitizeArrayElementValue(valueToWrite);
233-
} else if (arr.IndexValues.Count < MaxTrackedArrayValues) {
234-
arr.IndexValues[index.Value] = ArrayValue.SanitizeArrayElementValue(valueToWrite);
235-
}
230+
} else if (arr.IndexValues.TryGetValue (index.Value, out _) || arr.IndexValues.Count < MaxTrackedArrayValues) {
231+
var sanitizedValue = ArrayValue.SanitizeArrayElementValue(valueToWrite);
232+
arr.IndexValues[index.Value] = merge
233+
? _multiValueLattice.Meet (arr.IndexValues[index.Value], sanitizedValue)
234+
: sanitizedValue;
236235
}
237236
}
238237
}

src/tools/illink/src/ILLink.Shared/DataFlow/ValueSet.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ public void Reset ()
132132

133133
public static implicit operator ValueSet<TValue> (TValue value) => new (value);
134134

135+
public bool HasMultipleValues => _values is EnumerableValues;
136+
135137
public override bool Equals (object? obj) => obj is ValueSet<TValue> other && Equals (other);
136138

137139
public bool Equals (ValueSet<TValue> other)

src/tools/illink/test/Mono.Linker.Tests.Cases/Attributes/OnlyKeepUsed/MethodWithUnmanagedConstraint.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
namespace Mono.Linker.Tests.Cases.Attributes.OnlyKeepUsed
55
{
66
[SetupCSharpCompilerToUse ("csc")]
7-
[SetupCompileArgument ("/langversion:7.3")]
87
[SetupLinkerArgument ("--used-attrs-only", "true")]
98
public class MethodWithUnmanagedConstraint
109
{

0 commit comments

Comments
 (0)