File: System\Xml\Xsl\IlGen\IteratorDescriptor.cs
Web Access
Project: src\src\libraries\System.Private.Xml\src\System.Private.Xml.csproj (System.Private.Xml)
// 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 System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;
using System.Xml.Xsl.Runtime;
 
namespace System.Xml.Xsl.IlGen
{
    /// <summary>
    /// Type of location in which iterator items are stored.
    /// </summary>
    internal enum ItemLocation
    {
        None = 0,
        Stack,                              // Each value is stored as the top value on the IL stack
        Parameter,                          // Each value is stored as a parameter to the current method
        Local,                              // Each value is stored as a local variable in the current method
        Current,                            // Each value is stored as an iterator's Current property
        Global,                             // Each value is stored as a global variable
    };
 
 
    /// <summary>
    /// None--Not in a branching context
    /// True--Branch if boolean expression evaluates to true
    /// False--Branch if boolean expression evaluates to false
    /// </summary>
    internal enum BranchingContext
    {
        None,
        OnTrue,
        OnFalse
    };
 
    /// <summary>
    /// Describes the Clr type and location of items returned by an iterator.
    /// This struct is meant to be immutable.
    /// </summary>
    internal struct StorageDescriptor
    {
        private ItemLocation _location;
        private object _locationObject;
        private Type _itemStorageType;
        private bool _isCached;
 
 
        //-----------------------------------------------
        // Create Methods
        //-----------------------------------------------
 
        /// <summary>
        /// Create default, empty StorageDescriptor.
        /// </summary>
        public static StorageDescriptor None()
        {
            return default;
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item located on the stack.
        /// </summary>
        public static StorageDescriptor Stack(Type itemStorageType, bool isCached)
        {
            StorageDescriptor storage = default;
            storage._location = ItemLocation.Stack;
            storage._itemStorageType = itemStorageType;
            storage._isCached = isCached;
            return storage;
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item which is a parameter to the current method.
        /// </summary>
        public static StorageDescriptor Parameter(int paramIndex, Type itemStorageType, bool isCached)
        {
            StorageDescriptor storage = default;
            storage._location = ItemLocation.Parameter;
            storage._locationObject = paramIndex;
            storage._itemStorageType = itemStorageType;
            storage._isCached = isCached;
            return storage;
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item located in a local variable.
        /// </summary>
        [RequiresDynamicCode("Calls System.Type.MakeGenericType")]
        public static StorageDescriptor Local(LocalBuilder loc, Type itemStorageType, bool isCached)
        {
            Debug.Assert(loc.LocalType == itemStorageType ||
                         typeof(IList<>).MakeGenericType(itemStorageType).IsAssignableFrom(loc.LocalType),
                $"Type {itemStorageType} does not match the local variable's type");
 
            StorageDescriptor storage = default;
            storage._location = ItemLocation.Local;
            storage._locationObject = loc;
            storage._itemStorageType = itemStorageType;
            storage._isCached = isCached;
            return storage;
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item which is the Current item in an iterator.
        /// </summary>
        public static StorageDescriptor Current(LocalBuilder locIter, MethodInfo currentMethod, Type itemStorageType)
        {
            Debug.Assert(currentMethod.ReturnType == itemStorageType,
                         $"Type {itemStorageType} does not match type of Current property.");
 
            StorageDescriptor storage = default;
            storage._location = ItemLocation.Current;
            storage._locationObject = new CurrentContext(locIter, currentMethod);
            storage._itemStorageType = itemStorageType;
            return storage;
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item located in a global variable.
        /// </summary>
        [RequiresDynamicCode("Calls System.Type.MakeGenericType")]
        public static StorageDescriptor Global(MethodInfo methGlobal, Type itemStorageType, bool isCached)
        {
            Debug.Assert(methGlobal.ReturnType == itemStorageType ||
                         typeof(IList<>).MakeGenericType(itemStorageType).IsAssignableFrom(methGlobal.ReturnType),
                $"Type {itemStorageType} does not match the global method's return type");
 
            StorageDescriptor storage = default;
            storage._location = ItemLocation.Global;
            storage._locationObject = methGlobal;
            storage._itemStorageType = itemStorageType;
            storage._isCached = isCached;
            return storage;
        }
 
 
        //-----------------------------------------------
        // Accessor Methods
        //-----------------------------------------------
 
        /// <summary>
        /// Return copy of current descriptor, but change item's location to the stack.
        /// </summary>
        public StorageDescriptor ToStack()
        {
            return Stack(_itemStorageType, _isCached);
        }
 
        /// <summary>
        /// Create a StorageDescriptor for an item located in a local variable.
        /// </summary>
        [RequiresDynamicCode("Calls StorageDescriptor.Local")]
        public StorageDescriptor ToLocal(LocalBuilder loc)
        {
            return Local(loc, _itemStorageType, _isCached);
        }
 
        /// <summary>
        /// Create a StorageDescriptor which is the same as this one, except for the item storage type.
        /// </summary>
        public StorageDescriptor ToStorageType(Type itemStorageType)
        {
            StorageDescriptor storage = this;
            storage._itemStorageType = itemStorageType;
            return storage;
        }
 
        /// <summary>
        /// Return an enumeration specifying where the value is located.
        /// </summary>
        public ItemLocation Location
        {
            get { return _location; }
        }
 
        /// <summary>
        /// Return the index of the parameter that stores this iterator's values.
        /// </summary>
        public int ParameterLocation
        {
            get { return (int)_locationObject; }
        }
 
        /// <summary>
        /// Return the LocalBuilder that stores this iterator's values.
        /// </summary>
        public LocalBuilder? LocalLocation
        {
            get { return _locationObject as LocalBuilder; }
        }
 
        /// <summary>
        /// Return the "Current" location information (LocalBuilder and Current MethodInfo) that will store
        /// this iterator's helper class. The Current property on this iterator can be accessed to get the CurrentMethod.
        /// </summary>
        public CurrentContext? CurrentLocation
        {
            get { return _locationObject as CurrentContext; }
        }
 
        /// <summary>
        /// Return the MethodInfo for the method that computes this global value.
        /// </summary>
        public MethodInfo? GlobalLocation
        {
            get { return _locationObject as MethodInfo; }
        }
 
        /// <summary>
        /// Return true if this iterator's values are cached.
        /// </summary>
        public bool IsCached
        {
            get { return _isCached; }
        }
 
        /// <summary>
        /// Return the Clr type of an individual item in the storage location (never an IList{T} type).
        /// </summary>
        public Type ItemStorageType
        {
            get { return _itemStorageType; }
        }
    }
 
    /// <summary>
    /// A data class to hold information for a "Current" StorageLocation.
    /// </summary>
    internal sealed class CurrentContext
    {
        public CurrentContext(LocalBuilder local, MethodInfo currentMethod)
        {
            Local = local;
            CurrentMethod = currentMethod;
        }
 
        public readonly LocalBuilder Local;
        public readonly MethodInfo CurrentMethod;
    }
 
    /// <summary>
    /// Iterators are joined together, are nested within each other, and reference each other.  This internal class
    /// contains detailed information about iteration next labels, caching, iterator item location, etc.
    /// </summary>
    [RequiresDynamicCode("Creates DynamicMethods")]
    internal sealed class IteratorDescriptor
    {
        private GenerateHelper _helper;
 
        // Related iterators
        private IteratorDescriptor? _iterParent;
 
        // Iteration
        private Label _lblNext;
        private bool _hasNext;
        private LocalBuilder? _locPos;
 
        // Branching
        private BranchingContext _brctxt;
        private Label _lblBranch;
 
        // Storage
        private StorageDescriptor _storage;
 
 
        //-----------------------------------------------
        // Initialize
        //-----------------------------------------------
 
        /// <summary>
        /// Create a "root" IteratorDescriptor which has no parent iterator.
        /// </summary>
        public IteratorDescriptor(GenerateHelper helper)
        {
            Init(null, helper);
        }
 
        /// <summary>
        /// Create an IteratorDescriptor that is nested in a parent iterator.
        /// </summary>
        public IteratorDescriptor(IteratorDescriptor iterParent)
        {
            Init(iterParent, iterParent._helper);
        }
 
        /// <summary>
        /// Internal helper initializor.
        /// </summary>
        [MemberNotNull(nameof(_helper))]
        private void Init(IteratorDescriptor? iterParent, GenerateHelper helper)
        {
            _helper = helper;
            _iterParent = iterParent;
        }
 
 
        //-----------------------------------------------
        // Related Iterators
        //-----------------------------------------------
 
        /// <summary>
        /// Return the iterator in which this iterator is nested.
        /// </summary>
        public IteratorDescriptor? ParentIterator
        {
            get { return _iterParent; }
        }
 
 
        //-----------------------------------------------
        // Iteration
        //-----------------------------------------------
 
        /// <summary>
        /// Returns true if LabelNext is currently defined.  If not, then this iterator will return
        /// exactly one result (iterator is cardinality one).
        /// </summary>
        public bool HasLabelNext
        {
            get { return _hasNext; }
        }
 
        /// <summary>
        /// Return the label that is anchored to this code iterator's MoveNext code.
        /// </summary>
        public Label GetLabelNext()
        {
            Debug.Assert(_hasNext);
            return _lblNext;
        }
 
        /// <summary>
        /// Set this iterator's next label and storage.  This iterator will range over a set of values located in
        /// "storage".  To get the next value, jump to "lblNext".
        /// </summary>
        public void SetIterator(Label lblNext, StorageDescriptor storage)
        {
            _lblNext = lblNext;
            _hasNext = true;
            _storage = storage;
        }
 
        /// <summary>
        /// Set this iterator to be the same as the specified iterator.
        /// </summary>
        public void SetIterator(IteratorDescriptor iterInfo)
        {
            if (iterInfo.HasLabelNext)
            {
                _lblNext = iterInfo.GetLabelNext();
                _hasNext = true;
            }
 
            _storage = iterInfo.Storage;
        }
 
        /// <summary>
        /// Continue iteration until it is complete.  Branch to "lblOnEnd" when iteration is complete.
        /// </summary>
        /// <remarks>
        /// goto LabelNextCtxt;
        /// LabelOnEnd:
        /// </remarks>
        public void LoopToEnd(Label lblOnEnd)
        {
            if (_hasNext)
            {
                _helper.BranchAndMark(_lblNext, lblOnEnd);
                _hasNext = false;
            }
 
            // After looping is finished, storage is N/A
            _storage = StorageDescriptor.None();
        }
 
        /// <summary>
        /// Storage location containing the position of the current item as an integer.
        /// This location is only defined on iterators, and then only if they might be
        /// referenced by a PositionOf operator.
        /// </summary>
        public LocalBuilder? LocalPosition
        {
            get { return _locPos; }
            set { _locPos = value; }
        }
 
 
        //-----------------------------------------------
        // Caching
        //-----------------------------------------------
 
        /// <summary>
        /// Push the count of items in the cache onto the stack.
        /// </summary>
        public void CacheCount()
        {
            Debug.Assert(_storage.IsCached);
            PushValue();
            _helper.CallCacheCount(_storage.ItemStorageType);
        }
 
        /// <summary>
        /// If the iterator has been fully cached, then iterate the values one-by-one.
        /// </summary>
        public void EnsureNoCache()
        {
            if (_storage.IsCached)
            {
                if (!HasLabelNext)
                {
                    // If no Next label, this must be a singleton cache
                    EnsureStack();
                    _helper.LoadInteger(0);
                    _helper.CallCacheItem(_storage.ItemStorageType);
 
                    _storage = StorageDescriptor.Stack(_storage.ItemStorageType, false);
                }
                else
                {
                    // int idx;
                    LocalBuilder locIdx = _helper.DeclareLocal("$$$idx", typeof(int));
                    Label lblNext;
 
                    // Make sure cache is not on the stack
                    EnsureNoStack("$$$cache");
 
                    // idx = -1;
                    _helper.LoadInteger(-1);
                    _helper.Emit(OpCodes.Stloc, locIdx);
 
                    // LabelNext:
                    lblNext = _helper.DefineLabel();
                    _helper.MarkLabel(lblNext);
 
                    // idx++;
                    _helper.Emit(OpCodes.Ldloc, locIdx);
                    _helper.LoadInteger(1);
                    _helper.Emit(OpCodes.Add);
                    _helper.Emit(OpCodes.Stloc, locIdx);
 
                    // if (idx >= cache.Count) goto LabelNextCtxt;
                    _helper.Emit(OpCodes.Ldloc, locIdx);
                    CacheCount();
                    _helper.Emit(OpCodes.Bge, GetLabelNext());
 
                    // item = cache[idx];
                    PushValue();
                    _helper.Emit(OpCodes.Ldloc, locIdx);
                    _helper.CallCacheItem(_storage.ItemStorageType);
 
                    SetIterator(lblNext, StorageDescriptor.Stack(_storage.ItemStorageType, false));
                }
            }
        }
 
 
        //-----------------------------------------------
        // If-then-else branching
        //-----------------------------------------------
 
        /// <summary>
        /// Setup a branching context.  All nested iterators compiled in this context must evaluate
        /// to a single boolean value.  However, these expressions must not push the result as a boolean
        /// onto the stack.  Instead, if brctxt is BranchType.True, then the expression should
        /// jump to lblBranch if it evaluates to true.  If brctxt is BranchType.False, then the
        /// branch should happen if the evaluation result is false.
        /// </summary>
        public void SetBranching(BranchingContext brctxt, Label lblBranch)
        {
            Debug.Assert(brctxt != BranchingContext.None);
            _brctxt = brctxt;
            _lblBranch = lblBranch;
        }
 
        /// <summary>
        /// True if within a branching context.
        /// </summary>
        public bool IsBranching
        {
            get { return _brctxt != BranchingContext.None; }
        }
 
        /// <summary>
        /// Returns the label to which conditionals should branch.
        /// </summary>
        public Label LabelBranch
        {
            get { return _lblBranch; }
        }
 
        /// <summary>
        /// If BranchingContext.OnTrue, branch on true.  Otherwise, branch on false.
        /// </summary>
        public BranchingContext CurrentBranchingContext
        {
            get { return _brctxt; }
        }
 
 
        //-----------------------------------------------
        // Storage
        //-----------------------------------------------
 
        /// <summary>
        /// Returns information about how and where iterator values are stored.
        /// </summary>
        public StorageDescriptor Storage
        {
            get { return _storage; }
            set { _storage = value; }
        }
 
        /// <summary>
        /// Push current item onto the stack without affecting Location.
        /// </summary>
        public void PushValue()
        {
            switch (_storage.Location)
            {
                case ItemLocation.Stack:
                    _helper.Emit(OpCodes.Dup);
                    break;
 
                case ItemLocation.Parameter:
                    _helper.LoadParameter(_storage.ParameterLocation);
                    break;
 
                case ItemLocation.Local:
                    _helper.Emit(OpCodes.Ldloc, _storage.LocalLocation!);
                    break;
 
                case ItemLocation.Current:
                    CurrentContext currentContext = _storage.CurrentLocation!;
                    _helper.Emit(OpCodes.Ldloca, currentContext.Local);
                    _helper.Call(currentContext.CurrentMethod);
                    break;
 
                default:
                    Debug.Fail($"Invalid location: {_storage.Location}");
                    break;
            }
        }
 
        /// <summary>
        /// Ensure that the current item is pushed onto the stack.
        /// </summary>
        public void EnsureStack()
        {
            switch (_storage.Location)
            {
                case ItemLocation.Stack:
                    // Already on the stack
                    return;
 
                case ItemLocation.Parameter:
                case ItemLocation.Local:
                case ItemLocation.Current:
                    PushValue();
                    break;
 
                case ItemLocation.Global:
                    // Call method that computes the value of this global value
                    _helper.LoadQueryRuntime();
                    _helper.Call(_storage.GlobalLocation!);
                    break;
 
                default:
                    Debug.Fail($"Invalid location: {_storage.Location}");
                    break;
            }
 
            _storage = _storage.ToStack();
        }
 
        /// <summary>
        /// If the current item is on the stack, move it to a local variable.
        /// </summary>
        public void EnsureNoStack(string locName)
        {
            if (_storage.Location == ItemLocation.Stack)
                EnsureLocal(locName);
        }
 
        /// <summary>
        /// If current item is not already in a local variable, then move it to a local variable of the specified name.
        /// </summary>
        public void EnsureLocal(string locName)
        {
            if (_storage.Location != ItemLocation.Local)
            {
                if (_storage.IsCached)
                    EnsureLocal(_helper.DeclareLocal(locName, typeof(IList<>).MakeGenericType(_storage.ItemStorageType)));
                else
                    EnsureLocal(_helper.DeclareLocal(locName, _storage.ItemStorageType));
            }
        }
 
        /// <summary>
        /// Ensure that current item is saved to the specified local variable.
        /// </summary>
        public void EnsureLocal(LocalBuilder bldr)
        {
            if (_storage.LocalLocation != bldr)
            {
                // Push value onto stack and then save to bldr
                EnsureStack();
                _helper.Emit(OpCodes.Stloc, bldr);
                _storage = _storage.ToLocal(bldr);
            }
        }
 
        /// <summary>
        /// Discard the current item if it is pushed onto the stack.
        /// </summary>
        public void DiscardStack()
        {
            if (_storage.Location == ItemLocation.Stack)
            {
                _helper.Emit(OpCodes.Pop);
                _storage = StorageDescriptor.None();
            }
        }
 
        /// <summary>
        /// Ensure that the iterator's items are not cached, and that the current item is pushed onto the stack.
        /// </summary>
        public void EnsureStackNoCache()
        {
            EnsureNoCache();
            EnsureStack();
        }
 
        /// <summary>
        /// Ensure that the iterator's items are not cached, and that if the current item is pushed onto the stack,
        /// that it is moved to a local variable.
        /// </summary>
        public void EnsureNoStackNoCache(string locName)
        {
            EnsureNoCache();
            EnsureNoStack(locName);
        }
 
        /// <summary>
        /// Ensure that the iterator's items are not cached, and that if the current item is not already in a local,
        /// variable, that it is moved to a local variable of the specified name.
        /// </summary>
        public void EnsureLocalNoCache(string locName)
        {
            EnsureNoCache();
            EnsureLocal(locName);
        }
 
        /// <summary>
        /// Ensure that the iterator's items are not cached and that the current item is saved to the specified local variable.
        /// </summary>
        public void EnsureLocalNoCache(LocalBuilder bldr)
        {
            EnsureNoCache();
            EnsureLocal(bldr);
        }
 
        /// <summary>
        /// Each XmlQueryType has multiple legal CLR representations.  Ensure that all items returned by this iterator are in
        /// the Clr representation specified by "storageTypeDest".
        /// </summary>
        public void EnsureItemStorageType(XmlQueryType xmlType, Type storageTypeDest)
        {
            // If source type = destination type, then done
            if (_storage.ItemStorageType == storageTypeDest)
                goto SetStorageType;
 
            Debug.Assert(_storage.ItemStorageType == typeof(XPathItem) || storageTypeDest == typeof(XPathItem),
                         "EnsureItemStorageType must convert to or from Item");
 
            // If items are cached,
            if (_storage.IsCached)
            {
                // Check for special case of IList<XPathNavigator> -> IList<XPathItem>
                if (_storage.ItemStorageType == typeof(XPathNavigator))
                {
                    EnsureStack();
                    _helper.Call(XmlILMethods.NavsToItems);
                    goto SetStorageType;
                }
 
                // Check for special case of IList<XPathItem> -> IList<XPathNavigator>
                if (storageTypeDest == typeof(XPathNavigator))
                {
                    EnsureStack();
                    _helper.Call(XmlILMethods.ItemsToNavs);
                    goto SetStorageType;
                }
            }
 
            // Iterate over each item, and convert each to the destination type
            EnsureStackNoCache();
 
            // If source type is Item,
            if (_storage.ItemStorageType == typeof(XPathItem))
            {
                // Then downcast to Navigator
                if (storageTypeDest == typeof(XPathNavigator))
                {
                    _helper.Emit(OpCodes.Castclass, typeof(XPathNavigator));
                }
                else
                {
                    // Call ValueAs methods for atomic types
                    _helper.CallValueAs(storageTypeDest);
                }
                goto SetStorageType;
            }
            else if (_storage.ItemStorageType == typeof(XPathNavigator))
            {
                // No-op if converting from XPathNavigator to XPathItem
                Debug.Assert(storageTypeDest == typeof(XPathItem), "Must be converting from XPathNavigator to XPathItem");
                goto SetStorageType;
            }
 
            // Destination type must be item, so generate code to create an XmlAtomicValue
            _helper.LoadInteger(_helper.StaticData.DeclareXmlType(xmlType));
            _helper.LoadQueryRuntime();
            _helper.Call(XmlILMethods.StorageMethods[_storage.ItemStorageType].ToAtomicValue!);
 
        SetStorageType:
            _storage = _storage.ToStorageType(storageTypeDest);
        }
    }
}