File: SourceGeneration\CompilationCache.cs
Web Access
Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Caches the compilation produced by the pre-compilation phase, keyed on the inputs that
    /// determine its content. When a subsequent run produces the same inputs the cached
    /// <see cref="Compilation"/> reference is reused, which preserves reference-equality on
    /// <c>SharedInputNodes.Compilation</c> and therefore keeps every generator's
    /// <c>CompilationProvider</c>-derived caching valid across runs.
    /// </summary>
    /// <remarks>
    /// Always present on <see cref="GeneratorDriverState"/> (initially <see cref="Empty"/>).
    /// Driver code accumulates the run's inputs into a <see cref="Builder"/> obtained via
    /// <see cref="ToBuilder"/>, then calls <see cref="Builder.ToImmutableAndFree"/> to obtain
    /// either this same instance (cache hit) or a new one (cache miss / no caching needed).
    /// The result's <see cref="Compilation"/> is always the compilation the driver should pass
    /// to standard-phase generators.
    /// </remarks>
    internal sealed class CompilationCache
    {
        /// <summary>
        /// Initial sentinel used as the seed before any driver run has populated the cache.
        /// Its <see cref="Compilation"/> is <c>null</c> and is never read by the driver, since
        /// every run replaces the seed via <see cref="Builder.ToImmutableAndFree"/> before reading.
        /// </summary>
        public static readonly CompilationCache Empty = new();

        private readonly Compilation? _compilation;
        private readonly Compilation? _inputCompilation;
        private readonly ImmutableArray<SyntaxTree> _postInitTrees;
        private readonly ImmutableArray<PreCompCacheKey> _preCompKeys;

        private CompilationCache() { }

        private CompilationCache(
            Compilation compilation,
            Compilation inputCompilation,
            ImmutableArray<SyntaxTree> postInitTrees,
            ImmutableArray<PreCompCacheKey> preCompKeys)
        {
            _compilation = compilation;
            _inputCompilation = inputCompilation;
            _postInitTrees = postInitTrees;
            _preCompKeys = preCompKeys;
        }

        /// <summary>
        /// The compilation the driver should pass to standard-phase generators. Only valid to
        /// read after a <see cref="Builder.ToImmutableAndFree"/> call has produced this cache;
        /// reading it on the <see cref="Empty"/> sentinel will assert.
        /// </summary>
        public Compilation Compilation
        {
            get
            {
                Debug.Assert(_compilation is not null, $"{nameof(Compilation)} read before any builder produced this cache.");
                return _compilation;
            }
        }

        /// <summary>
        /// Starts accumulating inputs for the current run. The builder takes the input
        /// compilation reference (used for cache identity) and the compilation after post-init
        /// trees have been added (used as the base for any pre-compilation augmentation).
        /// </summary>
        public Builder ToBuilder(Compilation inputCompilation, Compilation compilationWithPostInit)
            => new Builder(this, inputCompilation, compilationWithPostInit);

        internal sealed class Builder
        {
            private readonly CompilationCache _previous;
            private readonly Compilation _inputCompilation;
            private readonly Compilation _compilationWithPostInit;
            private readonly ArrayBuilder<SyntaxTree> _postInitTrees = ArrayBuilder<SyntaxTree>.GetInstance();
            private readonly ArrayBuilder<PreCompCacheKey> _preCompKeys = ArrayBuilder<PreCompCacheKey>.GetInstance();
            private readonly ArrayBuilder<SyntaxTree> _preCompTreesToAdd = ArrayBuilder<SyntaxTree>.GetInstance();

            internal Builder(CompilationCache previous, Compilation inputCompilation, Compilation compilationWithPostInit)
            {
                _previous = previous;
                _inputCompilation = inputCompilation;
                _compilationWithPostInit = compilationWithPostInit;
            }

            /// <summary>
            /// Records a post-init <see cref="SyntaxTree"/> in driver order. The flat list of
            /// post-init trees participates in the cache key.
            /// </summary>
            public void AddPostInitTree(SyntaxTree tree) => _postInitTrees.Add(tree);

            /// <summary>
            /// Records a pre-compilation tree: contributes to the cache key (via generator
            /// index, hint name, text reference, and parse options) and to the set of trees
            /// appended to <c>compilationWithPostInit</c> on a cache miss.
            /// </summary>
            public void AddPreCompTree(int generatorIndex, GeneratedSyntaxTree tree)
            {
                _preCompKeys.Add(new PreCompCacheKey(generatorIndex, tree.HintName, tree.Text, tree.Tree.Options));
                _preCompTreesToAdd.Add(tree.Tree);
            }

            /// <summary>
            /// Decides between reusing the previous run's cache (when its inputs match) or
            /// producing a new entry. Always returns a cache whose <see cref="CompilationCache.Compilation"/>
            /// is the compilation the driver should use; reference equality with the source
            /// <see cref="CompilationCache"/> indicates the cache state is unchanged.
            /// </summary>
            public CompilationCache ToImmutableAndFree()
            {
                var preCompKeys = _preCompKeys.ToImmutableAndFree();
                var postInitTrees = _postInitTrees.ToImmutableAndFree();
                var preCompTreesToAdd = _preCompTreesToAdd.ToImmutableAndFree();

                if (preCompKeys.IsEmpty)
                {
                    // No pre-compilation contributions this run. Return a fresh cache holding
                    // just compilationWithPostInit; we deliberately don't attempt to reuse
                    // _previous here because that compilation reference is regenerated every
                    // run (post-init AddSyntaxTrees) and standard-phase consumers expect to
                    // see that fresh reference to preserve their own re-execution semantics.
                    return new CompilationCache(_compilationWithPostInit, _inputCompilation, ImmutableArray<SyntaxTree>.Empty, ImmutableArray<PreCompCacheKey>.Empty);
                }

                if (_previous._compilation is not null
                    && ReferenceEquals(_previous._inputCompilation, _inputCompilation)
                    && ReferenceSequenceEqual(_previous._postInitTrees, postInitTrees)
                    && _previous._preCompKeys.AsSpan().SequenceEqual(preCompKeys.AsSpan()))
                {
                    // Cache hit -- reuse the previous run's compilation reference. This makes
                    // SharedInputNodes.Compilation report Cached for every generator, which is
                    // the whole point of this cache.
                    return _previous;
                }

                var newCompilation = preCompTreesToAdd.IsEmpty
                    ? _compilationWithPostInit
                    : _compilationWithPostInit.AddSyntaxTrees(preCompTreesToAdd);
                return new CompilationCache(newCompilation, _inputCompilation, postInitTrees, preCompKeys);
            }

            // Reference-equality SequenceEqual for ImmutableArray<T> where T is a class.
            // ReferenceEqualityComparer is IEqualityComparer<object?>, so it can't be passed to
            // ImmutableArray<T>.SequenceEqual's IEqualityComparer<T> parameter directly; this
            // walks the underlying buffers via spans for an allocation-free comparison.
            private static bool ReferenceSequenceEqual<T>(ImmutableArray<T> a, ImmutableArray<T> b) where T : class
            {
                if (a.Length != b.Length)
                {
                    return false;
                }
                var aSpan = a.AsSpan();
                var bSpan = b.AsSpan();
                for (int i = 0; i < aSpan.Length; i++)
                {
                    if (!ReferenceEquals(aSpan[i], bSpan[i]))
                    {
                        return false;
                    }
                }
                return true;
            }
        }
    }

    /// <summary>
    /// Identity for a single pre-compilation source as fed into the compilation cache.
    /// Equality is reference-based on <see cref="Text"/> and <see cref="Options"/>; this matches
    /// what <c>AbstractSourceOutputNode</c> already does — when the pre-compilation output's
    /// inputs are unchanged it returns the previous <see cref="GeneratedSourceText"/> with the
    /// same <see cref="SourceText"/> reference, so reference equality is the right level here.
    /// </summary>
    internal readonly struct PreCompCacheKey : IEquatable<PreCompCacheKey>
    {
        public PreCompCacheKey(int generatorIndex, string hintName, SourceText text, ParseOptions options)
        {
            GeneratorIndex = generatorIndex;
            HintName = hintName;
            Text = text;
            Options = options;
        }

        public int GeneratorIndex { get; }
        public string HintName { get; }
        public SourceText Text { get; }
        public ParseOptions Options { get; }

        public bool Equals(PreCompCacheKey other) =>
            GeneratorIndex == other.GeneratorIndex
            && string.Equals(HintName, other.HintName, StringComparison.OrdinalIgnoreCase)
            && ReferenceEquals(Text, other.Text)
            && ReferenceEquals(Options, other.Options);

        public override bool Equals(object? obj) => obj is PreCompCacheKey k && Equals(k);

        public override int GetHashCode()
            => Roslyn.Utilities.Hash.Combine(
                GeneratorIndex,
                Roslyn.Utilities.Hash.Combine(
                    StringComparer.OrdinalIgnoreCase.GetHashCode(HintName),
                    Roslyn.Utilities.Hash.Combine(
                        System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(Text),
                        System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(Options))));
    }
}