-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DAM analyzer] Support exceptional control flow (#2481)
This includes a change to make the analyzer tests fail when an exception is thrown. This models exceptional control flow by maintaining an exception state for try/catch regions, which is updated to include all possible states encountered within that region. This exceptional state is propagated from try regions to the beginning of catch regions, and from try or catch regions to the beginning of finally regions. Finally regions are represented as additional information for each predecessor edge in the control flow graph, following the data model given by Roslyn's CFG. Each edge includes a list of finally regions through which control flows as part of the edge before reaching its destination. For now, this approach inefficiently propagates the old finally state along these chains of finally regions when it encounters them. This has the downside that long finally regions will cause re-analysis until each finally block sees a new state. The finally state is also unified along all such paths, so the warning state at the exit of a finally is a conservative approximation that accounts for all normal control flow leading to the finally region. Additionally, the finally regions are analyzed separately for the case when an exception causes a finally block to be reached. Inside the finally region, this will produce a superset of the warnings for normal control flow inside the same region. However this exceptional finally state does not flow out of the finally, to avoid spurious warnings in code that is unreachable when executing finally blocks due to exceptions. Also, implement a wrapper type where Set automatically meets the exception state. The Set operation is specific to the local dataflow analysis, so this introduces a state interface to track the current/exception state. Only the state type for the local dataflow analysis (LocalDataFlowState) implements this specialized behavior.
- Loading branch information
Showing
20 changed files
with
1,535 additions
and
159 deletions.
There are no files selected for viewing
126 changes: 126 additions & 0 deletions
126
src/ILLink.RoslynAnalyzer/DataFlow/ControlFlowGraphProxy.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using ILLink.Shared.DataFlow; | ||
using Microsoft.CodeAnalysis.FlowAnalysis; | ||
|
||
using Predecessor = ILLink.Shared.DataFlow.IControlFlowGraph< | ||
ILLink.RoslynAnalyzer.DataFlow.BlockProxy, | ||
ILLink.RoslynAnalyzer.DataFlow.RegionProxy | ||
>.Predecessor; | ||
|
||
namespace ILLink.RoslynAnalyzer.DataFlow | ||
{ | ||
// Blocks should be usable as keys of a dictionary. | ||
// The record equality implementation will check for reference equality | ||
// on the underlying BasicBlock, so uses of this class should not expect | ||
// any kind of value equality for different block instances. In practice | ||
// this should be fine as long as we consistently use block instances from | ||
// a single ControlFlowGraph. | ||
public readonly record struct BlockProxy (BasicBlock Block) | ||
{ | ||
public override string ToString () | ||
{ | ||
return base.ToString () + $"[{Block.Ordinal}]"; | ||
} | ||
} | ||
|
||
public readonly record struct RegionProxy (ControlFlowRegion Region) : IRegion<RegionProxy> | ||
{ | ||
public RegionKind Kind => Region.Kind switch { | ||
ControlFlowRegionKind.Try => RegionKind.Try, | ||
ControlFlowRegionKind.Catch => RegionKind.Catch, | ||
ControlFlowRegionKind.Finally => RegionKind.Finally, | ||
_ => throw new InvalidOperationException () | ||
}; | ||
} | ||
|
||
public readonly record struct ControlFlowGraphProxy (ControlFlowGraph ControlFlowGraph) : IControlFlowGraph<BlockProxy, RegionProxy> | ||
{ | ||
public IEnumerable<BlockProxy> Blocks { | ||
get { | ||
foreach (var block in ControlFlowGraph.Blocks) | ||
yield return new BlockProxy (block); | ||
} | ||
} | ||
|
||
public BlockProxy Entry => new BlockProxy (ControlFlowGraph.Blocks[0]); | ||
|
||
// This is implemented by getting predecessors of the underlying Roslyn BasicBlock. | ||
// This is fine as long as the blocks come from the correct control-flow graph. | ||
public IEnumerable<Predecessor> GetPredecessors (BlockProxy block) | ||
{ | ||
foreach (var predecessor in block.Block.Predecessors) { | ||
if (predecessor.FinallyRegions.IsEmpty) { | ||
yield return new Predecessor (new BlockProxy (predecessor.Source), ImmutableArray<RegionProxy>.Empty); | ||
continue; | ||
} | ||
var finallyRegions = ImmutableArray.CreateBuilder<RegionProxy> (); | ||
foreach (var region in predecessor.FinallyRegions) { | ||
if (region == null) | ||
throw new InvalidOperationException (); | ||
finallyRegions.Add (new RegionProxy (region)); | ||
} | ||
yield return new Predecessor (new BlockProxy (predecessor.Source), finallyRegions.ToImmutable ()); | ||
} | ||
} | ||
|
||
public bool TryGetEnclosingTryOrCatch (BlockProxy block, out RegionProxy tryOrCatchRegion) | ||
{ | ||
return TryGetTryOrCatch (block.Block.EnclosingRegion, out tryOrCatchRegion); | ||
} | ||
|
||
public bool TryGetEnclosingTryOrCatch (RegionProxy regionProxy, out RegionProxy tryOrCatchRegion) | ||
{ | ||
return TryGetTryOrCatch (regionProxy.Region.EnclosingRegion, out tryOrCatchRegion); | ||
} | ||
|
||
bool TryGetTryOrCatch (ControlFlowRegion? region, out RegionProxy tryOrCatchRegion) | ||
{ | ||
tryOrCatchRegion = default; | ||
while (region != null) { | ||
if (region.Kind is ControlFlowRegionKind.Try or ControlFlowRegionKind.Catch) { | ||
tryOrCatchRegion = new RegionProxy (region); | ||
return true; | ||
} | ||
region = region.EnclosingRegion; | ||
} | ||
return false; | ||
} | ||
|
||
public bool TryGetEnclosingFinally (BlockProxy block, out RegionProxy catchRegion) | ||
{ | ||
catchRegion = default; | ||
ControlFlowRegion? region = block.Block.EnclosingRegion; | ||
while (region != null) { | ||
if (region.Kind == ControlFlowRegionKind.Finally) { | ||
catchRegion = new RegionProxy (region); | ||
return true; | ||
} | ||
region = region.EnclosingRegion; | ||
} | ||
return false; | ||
} | ||
|
||
public RegionProxy GetCorrespondingTry (RegionProxy catchOrFinallyRegion) | ||
{ | ||
if (catchOrFinallyRegion.Region.Kind is not (ControlFlowRegionKind.Finally or ControlFlowRegionKind.Catch)) | ||
throw new ArgumentException (nameof (catchOrFinallyRegion)); | ||
|
||
foreach (var nested in catchOrFinallyRegion.Region.EnclosingRegion!.NestedRegions) { | ||
// Note that for try+catch+finally, the try corresponding to the finally will not be the same as | ||
// the try corresponding to the catch, because Roslyn represents this region hierarchy the same as | ||
// a try+catch nested inside the try block of a try+finally. | ||
if (nested.Kind == ControlFlowRegionKind.Try) | ||
return new (nested); | ||
} | ||
throw new InvalidOperationException (); | ||
} | ||
|
||
public BlockProxy FirstBlock (RegionProxy region) => | ||
new BlockProxy (ControlFlowGraph.Blocks[region.Region.FirstBlockOrdinal]); | ||
|
||
public BlockProxy LastBlock (RegionProxy region) => | ||
new BlockProxy (ControlFlowGraph.Blocks[region.Region.LastBlockOrdinal]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using ILLink.Shared.DataFlow; | ||
|
||
namespace ILLink.RoslynAnalyzer.DataFlow | ||
{ | ||
public class LocalDataFlowState<TValue, TValueLattice> | ||
: IDataFlowState<LocalState<TValue>, LocalStateLattice<TValue, TValueLattice>> | ||
where TValue : struct, IEquatable<TValue> | ||
where TValueLattice : ILattice<TValue> | ||
{ | ||
LocalState<TValue> current; | ||
public LocalState<TValue> Current { | ||
get => current; | ||
set => current = value; | ||
} | ||
|
||
public Box<LocalState<TValue>>? Exception { get; set; } | ||
|
||
public LocalStateLattice<TValue, TValueLattice> Lattice { get; init; } | ||
|
||
public void Set (LocalKey key, TValue value) | ||
{ | ||
current.Set (key, value); | ||
if (Exception != null) | ||
// TODO: optimize this to not meet the whole value, but just modify one value without copying. | ||
Exception.Value = Lattice.Meet (Exception.Value, current); | ||
} | ||
|
||
public TValue Get (LocalKey key) => current.Get (key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.