File: ScriptState.cs
Web Access
Project: src\src\Scripting\Core\Microsoft.CodeAnalysis.Scripting.csproj (Microsoft.CodeAnalysis.Scripting)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
using System.Collections.Generic;
using Microsoft.CodeAnalysis.PooledObjects;
 
namespace Microsoft.CodeAnalysis.Scripting
{
    /// <summary>
    /// The result of running a script.
    /// </summary>
    public abstract class ScriptState
    {
        /// <summary>
        /// The script that ran to produce this result.
        /// </summary>
        public Script Script { get; }
 
        /// <summary>
        /// Caught exception originating from the script top-level code.
        /// </summary>
        /// <remarks>
        /// Exceptions are only caught and stored here if the API returning the <see cref="ScriptState"/> is instructed to do so. 
        /// By default they are propagated to the caller of the API.
        /// </remarks>
        public Exception Exception { get; }
 
        internal ScriptExecutionState ExecutionState { get; }
 
        private ImmutableArray<ScriptVariable> _lazyVariables;
        private IReadOnlyDictionary<string, int> _lazyVariableMap;
 
        internal ScriptState(ScriptExecutionState executionState, Script script, Exception exceptionOpt)
        {
            Debug.Assert(executionState != null);
            Debug.Assert(script != null);
 
            ExecutionState = executionState;
            Script = script;
            Exception = exceptionOpt;
        }
 
        /// <summary>
        /// The final value produced by running the script.
        /// </summary>
        public object ReturnValue => GetReturnValue();
        internal abstract object GetReturnValue();
 
        /// <summary>
        /// Returns variables defined by the scripts in the declaration order.
        /// </summary>
        public ImmutableArray<ScriptVariable> Variables
        {
            get
            {
                if (_lazyVariables == null)
                {
                    ImmutableInterlocked.InterlockedInitialize(ref _lazyVariables, CreateVariables());
                }
 
                return _lazyVariables;
            }
        }
 
        /// <summary>
        /// Returns a script variable of the specified name. 
        /// </summary> 
        /// <remarks>
        /// If multiple script variables are defined in the script (in distinct submissions) returns the last one.
        /// Name lookup is case sensitive in C# scripts and case insensitive in VB scripts.
        /// </remarks>
        /// <returns><see cref="ScriptVariable"/> or null, if no variable of the specified <paramref name="name"/> is defined in the script.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
        public ScriptVariable GetVariable(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }
 
            int index;
            return GetVariableMap().TryGetValue(name, out index) ? Variables[index] : null;
        }
 
        private ImmutableArray<ScriptVariable> CreateVariables()
        {
            var result = ArrayBuilder<ScriptVariable>.GetInstance();
 
            var executionState = ExecutionState;
 
            // Don't include the globals object (slot #0)
            for (int i = 1; i < executionState.SubmissionStateCount; i++)
            {
                var state = executionState.GetSubmissionState(i);
                Debug.Assert(state != null);
 
                foreach (var field in state.GetType().GetTypeInfo().DeclaredFields)
                {
                    // TODO: synthesized fields of submissions shouldn't be public
                    if (field.IsPublic && field.Name.Length > 0 && (char.IsLetterOrDigit(field.Name[0]) || field.Name[0] == '_'))
                    {
                        result.Add(new ScriptVariable(state, field));
                    }
                }
            }
 
            return result.ToImmutableAndFree();
        }
 
        private IReadOnlyDictionary<string, int> GetVariableMap()
        {
            if (_lazyVariableMap == null)
            {
                var map = new Dictionary<string, int>(Script.Compiler.IdentifierComparer);
                for (int i = 0; i < Variables.Length; i++)
                {
                    map[Variables[i].Name] = i;
                }
 
                _lazyVariableMap = map;
            }
 
            return _lazyVariableMap;
        }
 
        /// <summary>
        /// Continues script execution from the state represented by this instance by running the specified code snippet.
        /// </summary>
        /// <param name="code">The code to be executed.</param>
        /// <param name="options">Options.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A <see cref="ScriptState"/> that represents the state after running <paramref name="code"/>, including all declared variables and return value.</returns>
        public Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options, CancellationToken cancellationToken)
            => ContinueWithAsync<object>(code, options, null, cancellationToken);
 
        /// <summary>
        /// Continues script execution from the state represented by this instance by running the specified code snippet.
        /// </summary>
        /// <param name="code">The code to be executed.</param>
        /// <param name="options">Options.</param>
        /// <param name="catchException">
        /// If specified, any exception thrown by the script top-level code is passed to <paramref name="catchException"/>.
        /// If it returns true the exception is caught and stored on the resulting <see cref="ScriptState"/>, otherwise the exception is propagated to the caller.
        /// </param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A <see cref="ScriptState"/> that represents the state after running <paramref name="code"/>, including all declared variables, return value and caught exception (if applicable).</returns>
        public Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
            => Script.ContinueWith<object>(code, options).RunFromAsync(this, catchException, cancellationToken);
 
        /// <summary>
        /// Continues script execution from the state represented by this instance by running the specified code snippet.
        /// </summary>
        /// <param name="code">The code to be executed.</param>
        /// <param name="options">Options.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A <see cref="ScriptState"/> that represents the state after running <paramref name="code"/>, including all declared variables and return value.</returns>
        public Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options, CancellationToken cancellationToken)
            => ContinueWithAsync<TResult>(code, options, null, cancellationToken);
 
        /// <summary>
        /// Continues script execution from the state represented by this instance by running the specified code snippet.
        /// </summary>
        /// <param name="code">The code to be executed.</param>
        /// <param name="options">Options.</param>
        /// <param name="catchException">
        /// If specified, any exception thrown by the script top-level code is passed to <paramref name="catchException"/>.
        /// If it returns true the exception is caught and stored on the resulting <see cref="ScriptState"/>, otherwise the exception is propagated to the caller.
        /// </param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>A <see cref="ScriptState"/> that represents the state after running <paramref name="code"/>, including all declared variables, return value and caught exception (if applicable).</returns>
        public Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
            => Script.ContinueWith<TResult>(code, options).RunFromAsync(this, catchException, cancellationToken);
 
        // How do we resolve overloads? We should use the language semantics.
        // https://github.com/dotnet/roslyn/issues/3720
#if TODO
        /// <summary>
        /// Invoke a method declared by the script.
        /// </summary>
        public object Invoke(string name, params object[] args)
        {
            var func = this.FindMethod(name, args != null ? args.Length : 0);
            if (func != null)
            {
                return func(args);
            }
 
            return null;
        }
 
        private Func<object[], object> FindMethod(string name, int argCount)
        {
            for (int i = _executionState.Count - 1; i >= 0; i--)
            {
                var sub = _executionState[i];
                if (sub != null)
                {
                    var type = sub.GetType();
                    var method = FindMethod(type, name, argCount);
                    if (method != null)
                    {
                        return (args) => method.Invoke(sub, args);
                    }
                }
            }
 
            return null;
        }
 
        private MethodInfo FindMethod(Type type, string name, int argCount)
        {
            return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        }
 
        /// <summary>
        /// Create a delegate to a method declared by the script.
        /// </summary>
        public TDelegate CreateDelegate<TDelegate>(string name)
        {
            var delegateInvokeMethod = typeof(TDelegate).GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public);
 
            for (int i = _executionState.Count - 1; i >= 0; i--)
            {
                var sub = _executionState[i];
                if (sub != null)
                {
                    var type = sub.GetType();
                    var method = FindMatchingMethod(type, name, delegateInvokeMethod);
                    if (method != null)
                    {
                        return (TDelegate)(object)method.CreateDelegate(typeof(TDelegate), sub);
                    }
                }
            }
 
            return default(TDelegate);
        }
 
        private MethodInfo FindMatchingMethod(Type instanceType, string name, MethodInfo delegateInvokeMethod)
        {
            var dprms = delegateInvokeMethod.GetParameters();
 
            foreach (var mi in instanceType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (mi.Name == name)
                {
                    var prms = mi.GetParameters();
                    if (prms.Length == dprms.Length)
                    {
                        // TODO: better matching..
                        return mi;
                    }
                }
            }
 
            return null;
        }
#endif
    }
 
    public sealed class ScriptState<T> : ScriptState
    {
        public new T ReturnValue { get; }
        internal override object GetReturnValue() => ReturnValue;
 
        internal ScriptState(ScriptExecutionState executionState, Script script, T value, Exception exceptionOpt)
            : base(executionState, script, exceptionOpt)
        {
            ReturnValue = value;
        }
    }
}