File: src\tools\illink\src\ILLink.Shared\DataFlow\ValueSet.cs
Web Access
Project: src\src\tools\illink\src\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj (ILLink.RoslynAnalyzer)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
 
// This is needed due to NativeAOT which doesn't enable nullable globally yet
#nullable enable
 
namespace ILLink.Shared.DataFlow
{
    public readonly struct ValueSet<TValue> : IEquatable<ValueSet<TValue>>, IDeepCopyValue<ValueSet<TValue>>
        where TValue : notnull
    {
        private const int MaxValuesInSet = 256;
 
        public static readonly ValueSet<TValue> Empty;
 
        private sealed class ValueSetSentinel
        {
        }
 
        private static readonly ValueSetSentinel UnknownSentinel = new();
 
        public static readonly ValueSet<TValue> Unknown = new(UnknownSentinel);
 
        // Since we're going to do lot of type checks for this class a lot, it is much more efficient
        // if the class is sealed (as then the runtime can do a simple method table pointer comparison)
        private sealed class EnumerableValues : HashSet<TValue>
        {
            public EnumerableValues(IEnumerable<TValue> values) : base(values) { }
 
            public override int GetHashCode()
            {
                int hashCode = 0;
                foreach (var item in this)
                    hashCode = HashUtils.Combine(hashCode, item);
                return hashCode;
            }
 
            public bool Equals(EnumerableValues other)
            {
                // Unfortunately if some of the values are ArrayValues then they can mutate
                // after being added to the set, in which case their "hashing" is broken
                // The set will self-heal on every Meet since we recreate the HashSet
                // but equality is not guaranteed in the interim state.
                // So to workaround this for now, iterate over both sets and check
                // that the item can be found in the other set.
                foreach (TValue item in this)
                    if (!other.Contains(item))
                        return false;
 
                foreach (TValue item in other)
                    if (!Contains(item))
                        return false;
 
                return true;
            }
 
            public bool Equals(TValue other)
            {
                // As described above, it's possible to end up with a hashset which has multiple
                // values which are equal (due to mutability). So we can't rely on item count.
                bool found = false;
                foreach (TValue item in this)
                {
                    if (!item.Equals(other))
                        return false;
                    found = true;
                }
 
                return found;
            }
        }
 
        public struct Enumerator : IEnumerator<TValue>, IDisposable, IEnumerator
        {
            private readonly object? _value;
            private int _state;  // 0 before beginning, 1 at item, 2 after end
            private readonly IEnumerator<TValue>? _enumerator;
 
            internal Enumerator(object? values)
            {
                _state = 0;
                if (values is EnumerableValues valuesSet)
                {
                    _enumerator = valuesSet.GetEnumerator();
                    _value = null;
                }
                else
                {
                    _enumerator = null;
                    _value = values;
                }
            }
 
            public TValue Current => _enumerator is not null
                ? _enumerator.Current
                : (_state == 1 ? (TValue)_value! : default!);
 
            object? IEnumerator.Current => Current;
 
            public void Dispose()
            {
            }
 
            public bool MoveNext()
            {
                if (_enumerator is not null)
                    return _enumerator.MoveNext();
 
                if (_value is null)
                    return false;
 
                if (_state > 1)
                    return false;
 
                _state++;
                return _state == 1;
            }
 
            public void Reset()
            {
                if (_enumerator is not null)
                    _enumerator.Reset();
                else
                    _state = 0;
            }
        }
 
        public readonly struct Enumerable : IEnumerable<TValue>
        {
            private readonly object? _values;
 
            public Enumerable(object? values) => _values = values;
 
            public Enumerator GetEnumerator() => new(_values);
 
            IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
 
            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
        }
 
        // This stores the values. By far the most common case will be either no values, or a single value.
        // Cases where there are multiple values stored are relatively very rare.
        //   null - no values (empty set)
        //   TValue - single value itself
        //   EnumerableValues typed object - multiple values, stored in the hashset
        //   ValueSetSentinel.Unknown - unknown value, or "any possible value"
        private readonly object? _values;
 
        public ValueSet(TValue value) => _values = value;
 
        public ValueSet(IEnumerable<TValue> values) => _values = new EnumerableValues(values);
 
        private ValueSet(EnumerableValues values) => _values = values;
 
        private ValueSet(ValueSetSentinel sentinel) => _values = sentinel;
 
        public static implicit operator ValueSet<TValue>(TValue value) => new(value);
 
        // Note: returns false for Unknown
        public bool HasMultipleValues => _values is EnumerableValues;
 
        public override bool Equals(object? obj) => obj is ValueSet<TValue> other && Equals(other);
 
        public bool Equals(ValueSet<TValue> other)
        {
            if (_values == null)
                return other._values == null;
            if (other._values == null)
                return false;
 
            if (_values is EnumerableValues enumerableValues)
            {
                if (other._values is EnumerableValues otherValuesSet)
                {
                    return enumerableValues.Equals(otherValuesSet);
                }
                else if (other._values is TValue otherValue)
                {
                    return enumerableValues.Equals(otherValue);
                }
                else
                {
                    Debug.Assert(other._values == UnknownSentinel);
                    return false;
                }
            }
            else if (_values is TValue value)
            {
                if (other._values is EnumerableValues otherEnumerableValues)
                {
                    return otherEnumerableValues.Equals(value);
                }
                else if (other._values is TValue otherValue)
                {
                    return EqualityComparer<TValue>.Default.Equals(value, otherValue);
                }
                else
                {
                    Debug.Assert(other._values == UnknownSentinel);
                    return false;
                }
            }
            else
            {
                Debug.Assert(_values == UnknownSentinel);
                return other._values == UnknownSentinel;
            }
        }
 
        public static bool operator ==(ValueSet<TValue> left, ValueSet<TValue> right) => left.Equals(right);
        public static bool operator !=(ValueSet<TValue> left, ValueSet<TValue> right) => !(left == right);
 
        public override int GetHashCode()
        {
            if (_values == null)
                return typeof(ValueSet<TValue>).GetHashCode();
 
            if (_values is EnumerableValues enumerableValues)
                return enumerableValues.GetHashCode();
 
            return _values.GetHashCode();
        }
 
        public Enumerable GetKnownValues() => new Enumerable(_values == UnknownSentinel ? null : _values);
 
        // Note: returns false for Unknown
        public bool Contains(TValue value)
        {
            if (_values is null)
                return false;
            if (_values is EnumerableValues valuesSet)
                return valuesSet.Contains(value);
            if (_values is TValue thisValue)
                return EqualityComparer<TValue>.Default.Equals(value, thisValue);
            Debug.Assert(_values == UnknownSentinel);
            return false;
        }
 
        internal static ValueSet<TValue> Union(ValueSet<TValue> left, ValueSet<TValue> right)
        {
            if (left._values == null)
                return right.DeepCopy();
            if (right._values == null)
                return left.DeepCopy();
 
            if (left._values == UnknownSentinel || right._values == UnknownSentinel)
                return Unknown;
 
            if (left._values is not EnumerableValues && right.Contains((TValue)left._values))
                return right.DeepCopy();
 
            if (right._values is not EnumerableValues && left.Contains((TValue)right._values))
                return left.DeepCopy();
 
            var values = new EnumerableValues(left.DeepCopy().GetKnownValues());
            values.UnionWith(right.DeepCopy().GetKnownValues());
            // Limit the number of values we track, to prevent hangs in case of patterns that
            // create exponentially many possible values.
            if (values.Count > MaxValuesInSet)
                return Unknown;
            return new ValueSet<TValue>(values);
        }
 
        internal static ValueSet<TValue> Intersection(ValueSet<TValue> left, ValueSet<TValue> right)
        {
            if (left._values == null || right._values == null)
                return Empty;
 
            if (left._values == UnknownSentinel)
                return right.DeepCopy();
 
            if (right._values == UnknownSentinel)
                return left.DeepCopy();
 
            if (left._values is not EnumerableValues)
                return right.Contains((TValue)left._values) ? left.DeepCopy() : Empty;
 
            if (right._values is not EnumerableValues)
                return left.Contains((TValue)right._values) ? right.DeepCopy() : Empty;
 
            var values = new EnumerableValues(left.DeepCopy().GetKnownValues());
            values.IntersectWith(right.GetKnownValues());
            return new ValueSet<TValue>(values);
        }
 
        public bool IsEmpty() => _values == null;
 
        public bool IsUnknown() => _values == UnknownSentinel;
 
        public override string ToString()
        {
            if (IsUnknown())
                return "Unknown";
            StringBuilder sb = new();
            sb.Append('{');
            sb.Append(string.Join(",", GetKnownValues().Select(v => v.ToString())));
            sb.Append('}');
            return sb.ToString();
        }
 
        // Meet should copy the values, but most SingleValues are immutable.
        // Clone returns `this` if there are no mutable SingleValues (SingleValues that implement IDeepCopyValue), otherwise creates a new ValueSet with copies of the copiable Values
        public ValueSet<TValue> DeepCopy()
        {
            if (_values is null)
                return this;
 
            if (_values == UnknownSentinel)
                return this;
 
            // Optimize for the most common case with only a single value
            if (_values is not EnumerableValues)
            {
                if (_values is IDeepCopyValue<TValue> copyValue)
                    return new ValueSet<TValue>(copyValue.DeepCopy());
                else
                    return this;
            }
 
            return new ValueSet<TValue>(GetKnownValues().Select(value => value is IDeepCopyValue<TValue> copyValue ? copyValue.DeepCopy() : value));
        }
    }
}