diff --git a/source/Cosmos.IL2CPU/AppAssembler.cs b/source/Cosmos.IL2CPU/AppAssembler.cs index e4c69b303..0911d9d84 100644 --- a/source/Cosmos.IL2CPU/AppAssembler.cs +++ b/source/Cosmos.IL2CPU/AppAssembler.cs @@ -1355,7 +1355,7 @@ public void EmitEntrypoint(MethodBase aEntrypoint, MethodBase[] aBootEntries = n if (ShouldOptimize) { - Optimizer.Optimize(Assembler); + } } diff --git a/source/Cosmos.IL2CPU/ILReader.cs b/source/Cosmos.IL2CPU/ILReader.cs index 4931dd341..303bf209d 100644 --- a/source/Cosmos.IL2CPU/ILReader.cs +++ b/source/Cosmos.IL2CPU/ILReader.cs @@ -6,10 +6,11 @@ using System.Reflection.Metadata; using Cosmos.IL2CPU.Extensions; +using Cosmos.IL2CPU.Optimization; namespace Cosmos.IL2CPU { - public class ILReader + public class ILReader : IOptimizerLookaheadProvider { // We split this into two arrays since we have to read // a byte at a time anways. In the future if we need to @@ -19,6 +20,14 @@ public class ILReader private static readonly OpCode[] mOpCodesLo = new OpCode[256]; private static readonly OpCode[] mOpCodesHi = new OpCode[256]; + private static readonly Dictionary> methodCache = new(); + private static readonly Dictionary> lookaheadCache = new(); + + /// + /// The optimizer to use. + /// + public Optimizer Optimizer { get; set; } + public ILReader() { LoadOpCodes(); @@ -50,10 +59,22 @@ protected void CheckBranch(int aTarget, int aMethodSize) } } - public List ProcessMethod(MethodBase aMethod) + public List ProcessMethodAhead(MethodBase aMethod) { - var xResult = new List(); + return ProcessMethod(aMethod, lookahead: true); + } + + public List ProcessMethod(MethodBase aMethod, bool lookahead = false) + { + if (methodCache.TryGetValue(aMethod, out var result)) { + return result; + } + if(lookahead && lookaheadCache.TryGetValue(aMethod, out var lookaheadResult)) { + return lookaheadResult; + } + + var xResult = new List(); var xBody = aMethod.GetMethodBody(); var xModule = aMethod.Module; @@ -128,7 +149,6 @@ public List ProcessMethod(MethodBase aMethod) #endregion - #region ByReference Intrinsic if (aMethod.DeclaringType.IsGenericType @@ -657,10 +677,27 @@ public List ProcessMethod(MethodBase aMethod) default: throw new Exception("Unknown OperandType"); } - xILOpCode.InitStackAnalysis(aMethod); + + //xILOpCode.InitStackAnalysis(aMethod); xResult.Add(xILOpCode); } + if (!lookahead) { + xResult = Optimizer.Optimize(xResult); + } + + foreach (var opCode in xResult) { + opCode.InitStackAnalysis(aMethod); + } + + if (lookahead) { + lookaheadCache[aMethod] = xResult; + } + else { + methodCache[aMethod] = xResult; + lookaheadCache.Remove(aMethod); + } + return xResult; } diff --git a/source/Cosmos.IL2CPU/ILScanner.cs b/source/Cosmos.IL2CPU/ILScanner.cs index ad9d15dc5..235c09c30 100644 --- a/source/Cosmos.IL2CPU/ILScanner.cs +++ b/source/Cosmos.IL2CPU/ILScanner.cs @@ -1,1006 +1,1014 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; - -using Cosmos.IL2CPU.Extensions; - -using IL2CPU.API; -using IL2CPU.API.Attribs; -using XSharp.Assembler; - -namespace Cosmos.IL2CPU -{ - public class ScannerQueueItem - { - public MemberInfo Item { get; } - public string QueueReason { get; } - public string SourceItem { get; } - - public ScannerQueueItem(MemberInfo aMemberInfo, string aQueueReason, string aSourceItem) - { - Item = aMemberInfo; - QueueReason = aQueueReason; - SourceItem = aSourceItem; - } - - public override string ToString() - { - return Item.MemberType + " " + Item.ToString(); - } - } - - internal class ILScanner : IDisposable - { - public Action LogException = null; - public Action LogWarning = null; - - protected ILReader mReader; - protected AppAssembler mAsmblr; - - // List of asssemblies found during scan. We cannot use the list of loaded - // assemblies because the loaded list includes compilers, etc, and also possibly - // other unused assemblies. So instead we collect a list of assemblies as we scan. - internal List mUsedAssemblies = new List(); - - protected HashSet mItems = new HashSet(new MemberInfoComparer()); - protected List mItemsList = new List(); - - // Contains items to be scanned, both types and methods - protected Queue mQueue = new Queue(); - - // Virtual methods are nasty and constantly need to be rescanned for - // overriding methods in new types, so we keep track of them separately. - // They are also in the main mItems and mQueue. - protected HashSet mVirtuals = new HashSet(); - - protected IDictionary mMethodUIDs = new Dictionary(); - protected IDictionary mTypeUIDs = new Dictionary(); - - protected PlugManager mPlugManager = null; - - // Logging - // Only use for debugging and profiling. - protected bool mLogEnabled = false; - - protected string mMapPathname; - protected TextWriter mLogWriter; - - protected struct LogItem - { - public string SrcType; - public object Item; - } - - protected Dictionary> mLogMap; - - public ILScanner(AppAssembler aAsmblr, TypeResolver typeResolver, Action aLogException, Action aLogWarning) - { - mAsmblr = aAsmblr; - mReader = new ILReader(); - - LogException = aLogException; - LogWarning = aLogWarning; - - mPlugManager = new PlugManager(LogException, LogWarning, typeResolver); - - VTablesImplRefs.GetTypeId = GetTypeUID; // we need this to figure out which ids object, valuetype and enum have in the vmt - } - - public bool EnableLogging(string aPathname) - { - mLogMap = new Dictionary>(); - mMapPathname = aPathname; - mLogEnabled = true; - - // be sure that file could be written, to prevent exception on Dispose call, cause we could not make Task log in it - try - { - File.CreateText(aPathname).Dispose(); - } - catch - { - return false; - } - return true; - } - - protected void Queue(MemberInfo aItem, object aSrc, string aSrcType, string sourceItem = null) - { - CompilerHelpers.Debug($"Enqueing: {aItem.DeclaringType?.Name ?? ""}.{aItem.Name} from {aSrc}"); - if (aItem == null) - { - throw new ArgumentNullException(nameof(aItem)); - } - - //TODO: fix this, as each label/symbol should also contain an assembly specifier. - - //if ((xMemInfo != null) && (xMemInfo.DeclaringType != null) - // && (xMemInfo.DeclaringType.FullName == "System.ThrowHelper") - // && (xMemInfo.DeclaringType.Assembly.GetName().Name != "mscorlib")) - //{ - // System.ThrowHelper exists in MS .NET twice... - // Its an internal class that exists in both mscorlib and system assemblies. - // They are separate types though, so normally the scanner scans both and - // then we get conflicting labels. MS included it twice to make exception - // throwing code smaller. They are internal though, so we cannot - // reference them directly and only via finding them as they come along. - // We find it here, not via QueueType so we only check it here. Later - // we might have to checkin QueueType also. - // So now we accept both types, but emit code for only one. This works - // with the current Yasm assembler as we resolve by name in the assembler. - // However with other assemblers this approach may not work. - // If AssemblerYASM adds assembly name to the label, this will allow - // both to exist as they do in BCL. - // So in the future we might be able to remove this hack, or change - // how it works. - // - // Do nothing - // - //} - /*else*/ - if (!mItems.Contains(aItem)) - { - if (mLogEnabled) - { - LogMapPoint(aSrc, aSrcType, aItem); - } - - mItems.Add(aItem); - mItemsList.Add(aItem); - - if (aSrc is MethodBase xMethodBaseSrc) - { - aSrc = xMethodBaseSrc.DeclaringType + "::" + aSrc; - } - - mQueue.Enqueue(new ScannerQueueItem(aItem, aSrcType, aSrc + Environment.NewLine + sourceItem)); - } - } - - public void Execute(MethodBase aStartMethod, IEnumerable plugsAssemblies) - { - if (aStartMethod == null) - { - throw new ArgumentNullException(nameof(aStartMethod)); - } - // TODO: Investigate using MS CCI - // Need to check license, as well as in profiler - // http://cciast.codeplex.com/ - - #region Description - - // Methodology - // - // Ok - we've done the scanner enough times to know it needs to be - // documented super well so that future changes won't inadvertently - // break undocumented and unseen requirements. - // - // We've tried many approaches including recursive and additive scanning. - // They typically end up being inefficient, overly complex, or both. - // - // -We would like to scan all types/methods so we can plug them. - // -But we can't scan them until we plug them, because we will scan things - // that plugs would remove/change the paths of. - // -Plugs may also call methods which are also plugged. - // -We cannot resolve plugs ahead of time but must do on the fly during - // scanning. - // -TODO: Because we do on the fly resolution, we need to add explicit - // checking of plug classes and err when public methods are found that - // do not resolve. Maybe we can make a list and mark, or rescan. Can be done - // later or as an optional auditing step. - // - // This why in the past we had repetitive scans. - // - // Now we focus on more passes, but simpler execution. In the end it should - // be eaiser to optmize and yield overall better performance. Most of the - // passes should be low overhead versus an integrated system which often - // would need to reiterate over items multiple times. So we do more loops on - // with less repetitive analysis, instead of fewer loops but more repetition. - // - // -Locate all plug classes - // -Scan from entry point collecting all types and methods while checking - // for and following plugs - // -For each type - // -Include all ancestors - // -Include all static constructors - // -For each virtual method - // -Scan overloads in descendants until IsFinal, IsSealed or end - // -Scan base in ancestors until top or IsAbstract - // -Go to scan types again, until no new ones found. - // -Because the virtual method scanning will add to the list as it goes, maintain - // 2 lists. - // -Known Types and Methods - // -Types and Methods in Queue - to be scanned - // -Finally, do compilation - - #endregion Description - - mPlugManager.FindPlugImpls(plugsAssemblies); - // Now that we found all plugs, scan them. - // We have to scan them after we find all plugs, because - // plugs can use other plugs - mPlugManager.ScanFoundPlugs(); - foreach (var xPlug in mPlugManager.PlugImpls) - { - CompilerHelpers.Debug($"Plug found: '{xPlug.Key.FullName}' in '{xPlug.Key.Assembly.FullName}'"); - } - - ILOp.PlugManager = mPlugManager; - - // Pull in extra implementations, GC etc. - Queue(VTablesImplRefs.IsInstanceRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.SetTypeInfoRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.SetInterfaceInfoRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.SetMethodInfoRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.SetInterfaceMethodInfoRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.GetMethodAddressForTypeRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.GetMethodAddressForInterfaceTypeRef, null, "Explicit Entry"); - Queue(VTablesImplRefs.GetDeclaringTypeOfMethodForTypeRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.InitRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.IncRootCountRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.IncRootCountsInStructRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.DecRootCountRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.DecRootCountsInStructRef, null, "Explicit Entry"); - Queue(GCImplementationRefs.AllocNewObjectRef, null, "Explicit Entry"); - // for now, to ease runtime exception throwing - Queue(typeof(ExceptionHelper).GetMethod("ThrowNotImplemented", new Type[] { typeof(string) }, null), null, "Explicit Entry"); - Queue(typeof(ExceptionHelper).GetMethod("ThrowOverflow", Type.EmptyTypes, null), null, "Explicit Entry"); - Queue(typeof(ExceptionHelper).GetMethod("ThrowInvalidOperation", new Type[] { typeof(string) }, null), null, "Explicit Entry"); - Queue(typeof(ExceptionHelper).GetMethod("ThrowArgumentOutOfRange", new Type[] { typeof(string) }, null), null, "Explicit Entry"); - - // register system types: - Queue(typeof(Array), null, "Explicit Entry"); - Queue(typeof(Array).Assembly.GetType("System.SZArrayHelper"), null, "Explicit Entry"); - Queue(typeof(Array).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(), null, "Explicit Entry"); - - Queue(typeof(MulticastDelegate).GetMethod("GetInvocationList"), null, "Explicit Entry"); - Queue(ExceptionHelperRefs.CurrentExceptionRef, null, "Explicit Entry"); - Queue(ExceptionHelperRefs.ThrowInvalidCastExceptionRef, null, "Explicit Entry"); - Queue(ExceptionHelperRefs.ThrowNotFiniteNumberExceptionRef, null, "Explicit Entry"); - Queue(ExceptionHelperRefs.ThrowDivideByZeroExceptionRef, null, "Explicit Entry"); - Queue(ExceptionHelperRefs.ThrowIndexOutOfRangeException, null, "Explicit Entry"); - - mAsmblr.ProcessField(typeof(string).GetField("Empty", BindingFlags.Static | BindingFlags.Public)); - - // Start from entry point of this program - Queue(aStartMethod, null, "Entry Point"); - - ScanQueue(); - UpdateAssemblies(); - Assemble(); - - mAsmblr.EmitEntrypoint(aStartMethod); - } - - public void QueueMethod(MethodBase method) - { - Queue(method, null, "Explicit entry via QueueMethod"); - } - - /// This method changes the opcodes. Changes are: - /// * inserting the ValueUID for method ops. - public void ProcessInstructions(List aOpCodes) // to remove ------- - { - foreach (var xOpCode in aOpCodes) - { - if (xOpCode is ILOpCodes.OpMethod xOpMethod) - { - mItems.TryGetValue(xOpMethod.Value, out MemberInfo value); - xOpMethod.Value = (MethodBase)(value ?? xOpMethod.Value); - xOpMethod.ValueUID = GetMethodUID(xOpMethod.Value); - } - } - } - - public void Dispose() - { - if (mLogEnabled) - { - // Create bookmarks, but also a dictionary that - // we can find the items in - var xBookmarks = new Dictionary(); - int xBookmark = 0; - foreach (var xList in mLogMap) - { - foreach (var xItem in xList.Value) - { - xBookmarks.Add(xItem.Item, xBookmark); - xBookmark++; - } - } - - using (mLogWriter = new StreamWriter(File.OpenWrite(mMapPathname))) - { - mLogWriter.WriteLine(""); - foreach (var xList in mLogMap) - { - var xLogItemText = LogItemText(xList.Key); - - mLogWriter.WriteLine("
"); - - // Emit bookmarks above source, so when clicking links user doesn't need - // to constantly scroll up. - foreach (var xItem in xList.Value) - { - mLogWriter.WriteLine(""); - } - - if (!xBookmarks.TryGetValue(xList.Key, out var xHref)) - { - xHref = -1; - } - mLogWriter.Write("

"); - if (xHref >= 0) - { - mLogWriter.WriteLine(""); - mLogWriter.WriteLine("", xHref); - } - if (xList.Key == null) - { - mLogWriter.WriteLine("Unspecified Source"); - } - else - { - mLogWriter.WriteLine(xLogItemText); - } - if (xHref >= 0) - { - mLogWriter.Write(""); - mLogWriter.Write(""); - } - mLogWriter.WriteLine("

"); - - mLogWriter.WriteLine("
    "); - foreach (var xItem in xList.Value) - { - mLogWriter.Write("
  • {0}
  • ", LogItemText(xItem.Item), xBookmarks[xItem.Item]); - - mLogWriter.WriteLine("
      "); - mLogWriter.WriteLine("
    • " + xItem.SrcType + "
    • "); - mLogWriter.WriteLine("
    "); - } - mLogWriter.WriteLine("
"); - } - mLogWriter.WriteLine(""); - } - } - } - - protected string LogItemText(object aItem) - { - if (aItem is MethodBase) - { - var x = (MethodBase)aItem; - return "Method: " + x.DeclaringType + "." + x.Name + "
" + x.GetFullName(); - } - if (aItem is Type) - { - var x = (Type)aItem; - return "Type: " + x.FullName; - } - return "Other: " + aItem; - } - - protected void ScanMethod(MethodBase aMethod, bool aIsPlug, string sourceItem) - { - CompilerHelpers.Debug($"ILScanner: ScanMethod"); - CompilerHelpers.Debug($"Method = '{aMethod}'"); - CompilerHelpers.Debug($"IsPlug = '{aIsPlug}'"); - CompilerHelpers.Debug($"Source = '{sourceItem}'"); - - var xParams = aMethod.GetParameters(); - var xParamTypes = new Type[xParams.Length]; - // Dont use foreach, enum generaly keeps order but - // isn't guaranteed. - //string xMethodFullName = LabelName.GetFullName(aMethod); - - for (int i = 0; i < xParams.Length; i++) - { - xParamTypes[i] = xParams[i].ParameterType; - Queue(xParamTypes[i], aMethod, "Parameter"); - } - var xIsDynamicMethod = aMethod.DeclaringType == null; - // Queue Types directly related to method - if (!aIsPlug) - { - // Don't queue declaring types of plugs - if (!xIsDynamicMethod) - { - // dont queue declaring types of dynamic methods either, those dont have a declaring type - Queue(aMethod.DeclaringType, aMethod, "Declaring Type"); - } - } - if (aMethod is MethodInfo) - { - Queue(((MethodInfo)aMethod).ReturnType, aMethod, "Return Type"); - } - // Scan virtuals - - #region Virtuals scan - - if (!xIsDynamicMethod && aMethod.IsVirtual) - { - // For virtuals we need to climb up the type tree - // and find the top base method. We then add that top - // node to the mVirtuals list. We don't need to add the - // types becuase adding DeclaringType will already cause - // all ancestor types to be added. - - var xVirtMethod = aMethod; - var xVirtType = aMethod.DeclaringType; - MethodBase xNewVirtMethod; - while (true) - { - xVirtType = xVirtType.BaseType; - if (xVirtType == null) - { - // We've reached object, can't go farther - xNewVirtMethod = null; - } - else - { - xNewVirtMethod = xVirtType.GetMethod(aMethod.Name, xParamTypes); - if (xNewVirtMethod != null) - { - if (!xNewVirtMethod.IsVirtual) - { - // This can happen if a virtual "replaces" a non virtual - // above it that is not virtual. - xNewVirtMethod = null; - } - } - } - // We dont bother to add these to Queue, because we have to do a - // full downlevel scan if its a new base virtual anyways. - if (xNewVirtMethod == null) - { - // If its already in the list, we mark it null - // so we dont do a full downlevel scan. - if (mVirtuals.Contains(xVirtMethod)) - { - xVirtMethod = null; - } - break; - } - xVirtMethod = xNewVirtMethod; - } - - // New virtual base found, we need to downscan it - // If it was already in mVirtuals, then ScanType will take - // care of new additions. - if (xVirtMethod != null) - { - Queue(xVirtMethod, aMethod, "Virtual Base"); - mVirtuals.Add(xVirtMethod); - - // List changes as we go, cant be foreach - for (int i = 0; i < mItemsList.Count; i++) - { - if (mItemsList[i] is Type xType && xType != xVirtMethod.DeclaringType && !xType.IsInterface) - { - if (xType.IsSubclassOf(xVirtMethod.DeclaringType)) - { - var enumerable = xType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(method => method.Name == aMethod.Name - && method.GetParameters().Select(param => param.ParameterType).SequenceEqual(xParamTypes)); - // We need to check IsVirtual, a non virtual could - // "replace" a virtual above it? - var xNewMethod = enumerable.FirstOrDefault(m => m.IsVirtual); - while (xNewMethod != null && (xNewMethod.Attributes & MethodAttributes.NewSlot) != 0) - { - xType = xType.BaseType; - xNewMethod = enumerable.Where(m => m.DeclaringType == xType).SingleOrDefault(); - } - if (xNewMethod != null) - { - Queue(xNewMethod, aMethod, "Virtual Downscan"); - } - } - else if (xVirtMethod.DeclaringType.IsInterface - && xType.GetInterfaces().Contains(xVirtMethod.DeclaringType) - && !(xType.BaseType == typeof(Array) && xVirtMethod.DeclaringType.IsGenericType)) - { - var xInterfaceMap = xType.GetInterfaceMap(xVirtMethod.DeclaringType); - var xMethodIndex = Array.IndexOf(xInterfaceMap.InterfaceMethods, xVirtMethod); - - if (xMethodIndex != -1) - { - var xMethod = xInterfaceMap.TargetMethods[xMethodIndex]; - - if (xMethod.DeclaringType == xType) - { - Queue(xInterfaceMap.TargetMethods[xMethodIndex], aMethod, "Virtual Downscan"); - } - } - } - } - } - } - } - - #endregion Virtuals scan - - MethodBase xPlug = null; - // Plugs may use plugs, but plugs won't be plugged over themself - var inl = aMethod.GetCustomAttribute(); - if (!aIsPlug && !xIsDynamicMethod) - { - // Check to see if method is plugged, if it is we don't scan body - xPlug = mPlugManager.ResolvePlug(aMethod, xParamTypes); - if (xPlug != null) - { - //ScanMethod(xPlug, true, "Plug method"); - if (inl == null) - { - Queue(xPlug, aMethod, "Plug method"); - } - } - } - - if (xPlug == null) - { - bool xNeedsPlug = false; - if ((aMethod.Attributes & MethodAttributes.PinvokeImpl) != 0) - { - // pinvoke methods dont have an embedded implementation - xNeedsPlug = true; - } - else - { - var xImplFlags = aMethod.GetMethodImplementationFlags(); - // todo: prob even more - if (xImplFlags.HasFlag(MethodImplAttributes.Native) || xImplFlags.HasFlag(MethodImplAttributes.InternalCall)) - { - // native implementations cannot be compiled - xNeedsPlug = true; - } - } - if (xNeedsPlug) - { - throw new Exception("Native code encountered, plug required. Check build output for more information." + Environment.NewLine - + " DO NOT REPORT THIS AS A BUG." + Environment.NewLine - + " Please see http://www.gocosmos.org/docs/plugs/missing/" + Environment.NewLine - + " Need plug for: " + LabelName.GetFullName(aMethod, false) + "(Plug Signature: " + DataMember.FilterStringForIncorrectChars(LabelName.GetFullName(aMethod, false)) + " ). " + Environment.NewLine - + " Static: " + aMethod.IsStatic + Environment.NewLine - + " Assembly: " + aMethod.DeclaringType.Assembly.FullName + Environment.NewLine - + " Called from:" + Environment.NewLine + sourceItem + Environment.NewLine); - } - - //TODO: As we scan each method, we could update or put in a new list - // that has the resolved plug so we don't have to reresolve it again - // later for compilation. - - // Scan the method body for more type and method refs - //TODO: Dont queue new items if they are plugged - // or do we need to queue them with a resolved ref in a new list? - - if (inl != null) - { - return; // cancel inline - } - - var xOpCodes = mReader.ProcessMethod(aMethod); - if (xOpCodes != null) - { - ProcessInstructions(xOpCodes); - foreach (var xOpCode in xOpCodes) - { - if (xOpCode is ILOpCodes.OpMethod) - { - Queue(((ILOpCodes.OpMethod)xOpCode).Value, aMethod, "Call", sourceItem); - } - else if (xOpCode is ILOpCodes.OpType xOpType) - { - Queue(((ILOpCodes.OpType)xOpCode).Value, aMethod, "OpCode Value"); - } - else if (xOpCode is ILOpCodes.OpField xOpField) - { - //TODO: Need to do this? Will we get a ILOpCodes.OpType as well? - Queue(xOpField.Value.DeclaringType, aMethod, "OpCode Value"); - if (xOpField.Value.IsStatic) - { - //TODO: Why do we add static fields, but not instance? - // AW: instance fields are "added" always, as part of a type, but for static fields, we need to emit a datamember - Queue(xOpField.Value, aMethod, "OpCode Value"); - } - } - else if (xOpCode is ILOpCodes.OpToken xOpToken) - { - if (xOpToken.ValueIsType) - { - Queue(xOpToken.ValueType, aMethod, "OpCode Value"); - } - if (xOpToken.ValueIsField) - { - Queue(xOpToken.ValueField.DeclaringType, aMethod, "OpCode Value"); - if (xOpToken.ValueField.IsStatic) - { - //TODO: Why do we add static fields, but not instance? - // AW: instance fields are "added" always, as part of a type, but for static fields, we need to emit a datamember - Queue(xOpToken.ValueField, aMethod, "OpCode Value"); - } - } - } - } - } - } - } - - protected void ScanType(Type aType) - { - CompilerHelpers.Debug($"ILScanner: ScanType"); - CompilerHelpers.Debug($"Type = '{aType}'"); - - // This is a bit overkill, most likely we dont need all these methods - // but I dont see a better way to do it easily - // so for generic interface methods on arrays, we just add all methods - if (aType.Name.Contains("SZArrayImpl")) - { - foreach (var xMethod in aType.GetMethods()) - { - Queue(xMethod, aType, "Generic Interface Method"); - } - } - - if (aType.IsGenericType && new string[] { "IList", "ICollection", "IEnumerable", "IReadOnlyList", "IReadOnlyCollection" } - .Any(i => aType.Name.Contains(i))) - { - Queue(aType.GenericTypeArguments[0].MakeArrayType(), aType, "CallVirt of Generic Interface for Array"); - } - - // Add immediate ancestor type - // We dont need to crawl up farther, when the BaseType is scanned - // it will add its BaseType, and so on. - if (aType.BaseType != null) - { - Queue(aType.BaseType, aType, "Base Type"); - } - // Queue static ctors - // We always need static ctors, else the type cannot - // be created. - foreach (var xCctor in aType.GetConstructors(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) - { - if (xCctor.DeclaringType == aType) - { - Queue(xCctor, aType, "Static Constructor"); - } - } - - if (aType.BaseType == typeof(Array) && !aType.GetElementType().IsPointer) - { - var szArrayHelper = typeof(Array).Assembly.GetType("System.SZArrayHelper"); // We manually add the link to the generic interfaces for an array - foreach (var xMethod in szArrayHelper.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - Queue(xMethod.MakeGenericMethod(new Type[] { aType.GetElementType() }), aType, "Virtual SzArrayHelper"); - } - - Queue(typeof(SZArrayImpl<>).MakeGenericType(aType.GetElementType()), aType, "Array"); - } - - - // Scam Fields so that we include those types - foreach (var field in aType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - Queue(field.FieldType, aType, "Field Type"); - } - - // For each new type, we need to scan for possible new virtuals - // in our new type if its a descendant of something in - // mVirtuals. - foreach (var xVirt in mVirtuals) - { - // See if our new type is a subclass of any virt's DeclaringTypes - // If so our new type might have some virtuals - if (aType.IsSubclassOf(xVirt.DeclaringType)) - { - var xParams = xVirt.GetParameters(); - var xParamTypes = new Type[xParams.Length]; - // Dont use foreach, enum generaly keeps order but - // isn't guaranteed. - for (int i = 0; i < xParams.Length; i++) - { - xParamTypes[i] = xParams[i].ParameterType; - } - var xMethod = aType.GetMethod(xVirt.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, xParamTypes, null); - if (xMethod != null) - { - // We need to check IsVirtual, a non virtual could - // "replace" a virtual above it? - if (xMethod.IsVirtual) - { - Queue(xMethod, aType, "Virtual"); - } - } - } - else if (!aType.IsGenericParameter && xVirt.DeclaringType.IsInterface && !(aType.BaseType == typeof(Array) && xVirt.DeclaringType.IsGenericType)) - { - if (!aType.IsInterface && aType.GetInterfaces().Contains(xVirt.DeclaringType) - && !(aType.BaseType == typeof(Array) && xVirt.DeclaringType.IsGenericType)) - { - var xIntfMapping = aType.GetInterfaceMap(xVirt.DeclaringType); - if (xIntfMapping.InterfaceMethods != null && xIntfMapping.TargetMethods != null) - { - var xIdx = Array.IndexOf(xIntfMapping.InterfaceMethods, xVirt); - if (xIdx != -1) - { - Queue(xIntfMapping.TargetMethods[xIdx], aType, "Virtual"); - } - } - } - } - } - - foreach (var xInterface in aType.GetInterfaces()) - { - Queue(xInterface, aType, "Implemented Interface"); - } - } - - protected void ScanQueue() - { - while (mQueue.Count > 0) - { - var xItem = mQueue.Dequeue(); - CompilerHelpers.Debug($"ILScanner: ScanQueue - '{xItem}'"); - // Check for MethodBase first, they are more numerous - // and will reduce compares - if (xItem.Item is MethodBase xMethod) - { - ScanMethod(xMethod, false, xItem.SourceItem); - } - else if (xItem.Item is Type xType) - { - ScanType(xType); - - // Methods and fields cant exist without types, so we only update - // mUsedAssemblies in type branch. - if (!mUsedAssemblies.Contains(xType.Assembly)) - { - mUsedAssemblies.Add(xType.Assembly); - } - } - else if (xItem.Item is FieldInfo) - { - // todo: static fields need more processing? - } - else - { - throw new Exception("Unknown item found in queue."); - } - } - } - - protected void LogMapPoint(object aSrc, string aSrcType, object aItem) - { - // Keys cant be null. If null, we just say ILScanner is the source - if (aSrc == null) - { - aSrc = typeof(ILScanner); - } - - var xLogItem = new LogItem - { - SrcType = aSrcType, - Item = aItem - }; - if (!mLogMap.TryGetValue(aSrc, out var xList)) - { - xList = new List(); - mLogMap.Add(aSrc, xList); - } - xList.Add(xLogItem); - } - - private MethodInfo GetUltimateBaseMethod(MethodInfo aMethod) - { - var xBaseMethod = aMethod; - - while (true) - { - var xBaseDefinition = xBaseMethod.GetBaseDefinition(); - - if (xBaseDefinition == xBaseMethod) - { - return xBaseMethod; - } - - xBaseMethod = xBaseDefinition; - } - } - - protected uint GetMethodUID(MethodBase aMethod) - { - if (mMethodUIDs.TryGetValue(aMethod, out var xMethodUID)) - { - return xMethodUID; - } - else - { - if (!aMethod.DeclaringType.IsInterface) - { - if (aMethod is MethodInfo xMethodInfo) - { - var xBaseMethod = GetUltimateBaseMethod(xMethodInfo); - - if (!mMethodUIDs.TryGetValue(xBaseMethod, out xMethodUID)) - { - xMethodUID = (uint)mMethodUIDs.Count; - mMethodUIDs.Add(xBaseMethod, xMethodUID); - } - - if (!new MethodBaseComparer().Equals(aMethod, xBaseMethod)) - { - mMethodUIDs.Add(aMethod, xMethodUID); - } - - return xMethodUID; - } - } - - xMethodUID = (uint)mMethodUIDs.Count; - mMethodUIDs.Add(aMethod, xMethodUID); - - return xMethodUID; - } - } - - protected uint GetTypeUID(Type aType) - { - if (!mItems.Contains(aType)) - { - throw new Exception($"Cannot get UID of types which are not queued! Type: {aType.Name}"); - } - if (!mTypeUIDs.ContainsKey(aType)) - { - var xId = (uint)mTypeUIDs.Count; - mTypeUIDs.Add(aType, xId); - return xId; - } - return mTypeUIDs[aType]; - } - - protected void UpdateAssemblies() - { - // It would be nice to keep DebugInfo output into assembler only but - // there is so much info that is available in scanner that is needed - // or can be used in a more efficient manner. So we output in both - // scanner and assembler as needed. - mAsmblr.DebugInfo.AddAssemblies(mUsedAssemblies); - } - - protected void Assemble() - { - foreach (var xItem in mItems) - { - if (xItem is MethodBase xMethod) - { - var xParams = xMethod.GetParameters(); - var xParamTypes = xParams.Select(q => q.ParameterType).ToArray(); - var xPlug = mPlugManager.ResolvePlug(xMethod, xParamTypes); - var xMethodType = Il2cpuMethodInfo.TypeEnum.Normal; - Type xPlugAssembler = null; - Il2cpuMethodInfo xPlugInfo = null; - var xMethodInline = xMethod.GetCustomAttribute(); - if (xMethodInline != null) - { - // inline assembler, shouldn't come here.. - continue; - } - var xMethodIdMethod = mItemsList.IndexOf(xMethod); - if (xMethodIdMethod == -1) - { - throw new Exception("Method not in scanner list!"); - } - PlugMethod xPlugAttrib = null; - if (xPlug != null) - { - xMethodType = Il2cpuMethodInfo.TypeEnum.NeedsPlug; - xPlugAttrib = xPlug.GetCustomAttribute(); - var xInlineAttrib = xPlug.GetCustomAttribute(); - var xMethodIdPlug = mItemsList.IndexOf(xPlug); - if (xMethodIdPlug == -1 && xInlineAttrib == null) - { - throw new Exception("Plug method not in scanner list!"); - } - if (xPlugAttrib != null && xInlineAttrib == null) - { - xPlugAssembler = xPlugAttrib.Assembler; - xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodIdPlug, Il2cpuMethodInfo.TypeEnum.Plug, null, xPlugAssembler); - - var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); - if (xPlugAttrib.IsWildcard) - { - xPlugInfo.IsWildcard = true; - xPlugInfo.PluggedMethod = xMethodInfo; - var xInstructions = mReader.ProcessMethod(xPlug); - if (xInstructions != null) - { - ProcessInstructions(xInstructions); - mAsmblr.ProcessMethod(xPlugInfo, xInstructions, mPlugManager); - } - } - mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); - } - else - { - if (xInlineAttrib != null) - { - var xMethodID = mItemsList.IndexOf(xItem); - if (xMethodID == -1) - { - throw new Exception("Method not in list!"); - } - xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodID, Il2cpuMethodInfo.TypeEnum.Plug, null, true); - - var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); - - xPlugInfo.PluggedMethod = xMethodInfo; - var xInstructions = mReader.ProcessMethod(xPlug); - if (xInstructions != null) - { - ProcessInstructions(xInstructions); - mAsmblr.ProcessMethod(xPlugInfo, xInstructions, mPlugManager); - } - mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); - } - else - { - xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodIdPlug, Il2cpuMethodInfo.TypeEnum.Plug, null, xPlugAssembler); - - var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); - mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); - } - } - } - else - { - xPlugAttrib = xMethod.GetCustomAttribute(); - - if (xPlugAttrib != null) - { - if (xPlugAttrib.IsWildcard) - { - continue; - } - if (xPlugAttrib.PlugRequired) - { - throw new Exception(String.Format("Method {0} requires a plug, but none is implemented", xMethod.Name)); - } - xPlugAssembler = xPlugAttrib.Assembler; - } - - var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo, xPlugAssembler); - var xInstructions = mReader.ProcessMethod(xMethod); - if (xInstructions != null) - { - ProcessInstructions(xInstructions); - mAsmblr.ProcessMethod(xMethodInfo, xInstructions, mPlugManager); - } - } - } - else if (xItem is FieldInfo) - { - mAsmblr.ProcessField((FieldInfo)xItem); - } - } - - var xTypes = new HashSet(); - var xMethods = new HashSet(new MethodBaseComparer()); - foreach (var xItem in mItems) - { - if (xItem is MethodBase) - { - xMethods.Add((MethodBase)xItem); - } - else if (xItem is Type) - { - xTypes.Add((Type)xItem); - } - } - - mAsmblr.GenerateVMTCode(xTypes, xMethods, mPlugManager, GetTypeUID, GetMethodUID); - } - } +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +using Cosmos.IL2CPU.Extensions; + +using IL2CPU.API; +using IL2CPU.API.Attribs; +using XSharp.Assembler; + +using Cosmos.IL2CPU.Optimization; +using Cosmos.IL2CPU.Optimization.Passes; + +namespace Cosmos.IL2CPU +{ + public class ScannerQueueItem + { + public MemberInfo Item { get; } + public string QueueReason { get; } + public string SourceItem { get; } + + public ScannerQueueItem(MemberInfo aMemberInfo, string aQueueReason, string aSourceItem) + { + Item = aMemberInfo; + QueueReason = aQueueReason; + SourceItem = aSourceItem; + } + + public override string ToString() + { + return Item.MemberType + " " + Item.ToString(); + } + } + + internal class ILScanner : IDisposable + { + public Action LogException = null; + public Action LogWarning = null; + + protected ILReader mReader; + protected AppAssembler mAsmblr; + + // List of asssemblies found during scan. We cannot use the list of loaded + // assemblies because the loaded list includes compilers, etc, and also possibly + // other unused assemblies. So instead we collect a list of assemblies as we scan. + internal List mUsedAssemblies = new List(); + + protected HashSet mItems = new HashSet(new MemberInfoComparer()); + protected List mItemsList = new List(); + + // Contains items to be scanned, both types and methods + protected Queue mQueue = new Queue(); + + // Virtual methods are nasty and constantly need to be rescanned for + // overriding methods in new types, so we keep track of them separately. + // They are also in the main mItems and mQueue. + protected HashSet mVirtuals = new HashSet(); + + protected IDictionary mMethodUIDs = new Dictionary(); + protected IDictionary mTypeUIDs = new Dictionary(); + + protected PlugManager mPlugManager = null; + + // Logging + // Only use for debugging and profiling. + protected bool mLogEnabled = false; + + protected string mMapPathname; + protected TextWriter mLogWriter; + + protected struct LogItem + { + public string SrcType; + public object Item; + } + + protected Dictionary> mLogMap; + + public ILScanner(AppAssembler aAsmblr, TypeResolver typeResolver, Action aLogException, Action aLogWarning) + { + mAsmblr = aAsmblr; + mReader = new ILReader(); + + var optimizer = new Optimizer(mReader) + .WithPass(new InlineDirectPropertiesPass()); + + mReader.Optimizer = optimizer; + + LogException = aLogException; + LogWarning = aLogWarning; + + mPlugManager = new PlugManager(LogException, LogWarning, typeResolver); + + VTablesImplRefs.GetTypeId = GetTypeUID; // we need this to figure out which ids object, valuetype and enum have in the vmt + } + + public bool EnableLogging(string aPathname) + { + mLogMap = new Dictionary>(); + mMapPathname = aPathname; + mLogEnabled = true; + + // be sure that file could be written, to prevent exception on Dispose call, cause we could not make Task log in it + try + { + File.CreateText(aPathname).Dispose(); + } + catch + { + return false; + } + return true; + } + + protected void Queue(MemberInfo aItem, object aSrc, string aSrcType, string sourceItem = null) + { + CompilerHelpers.Debug($"Enqueing: {aItem.DeclaringType?.Name ?? ""}.{aItem.Name} from {aSrc}"); + if (aItem == null) + { + throw new ArgumentNullException(nameof(aItem)); + } + + //TODO: fix this, as each label/symbol should also contain an assembly specifier. + + //if ((xMemInfo != null) && (xMemInfo.DeclaringType != null) + // && (xMemInfo.DeclaringType.FullName == "System.ThrowHelper") + // && (xMemInfo.DeclaringType.Assembly.GetName().Name != "mscorlib")) + //{ + // System.ThrowHelper exists in MS .NET twice... + // Its an internal class that exists in both mscorlib and system assemblies. + // They are separate types though, so normally the scanner scans both and + // then we get conflicting labels. MS included it twice to make exception + // throwing code smaller. They are internal though, so we cannot + // reference them directly and only via finding them as they come along. + // We find it here, not via QueueType so we only check it here. Later + // we might have to checkin QueueType also. + // So now we accept both types, but emit code for only one. This works + // with the current Yasm assembler as we resolve by name in the assembler. + // However with other assemblers this approach may not work. + // If AssemblerYASM adds assembly name to the label, this will allow + // both to exist as they do in BCL. + // So in the future we might be able to remove this hack, or change + // how it works. + // + // Do nothing + // + //} + /*else*/ + if (!mItems.Contains(aItem)) + { + if (mLogEnabled) + { + LogMapPoint(aSrc, aSrcType, aItem); + } + + mItems.Add(aItem); + mItemsList.Add(aItem); + + if (aSrc is MethodBase xMethodBaseSrc) + { + aSrc = xMethodBaseSrc.DeclaringType + "::" + aSrc; + } + + mQueue.Enqueue(new ScannerQueueItem(aItem, aSrcType, aSrc + Environment.NewLine + sourceItem)); + } + } + + public void Execute(MethodBase aStartMethod, IEnumerable plugsAssemblies) + { + if (aStartMethod == null) + { + throw new ArgumentNullException(nameof(aStartMethod)); + } + // TODO: Investigate using MS CCI + // Need to check license, as well as in profiler + // http://cciast.codeplex.com/ + + #region Description + + // Methodology + // + // Ok - we've done the scanner enough times to know it needs to be + // documented super well so that future changes won't inadvertently + // break undocumented and unseen requirements. + // + // We've tried many approaches including recursive and additive scanning. + // They typically end up being inefficient, overly complex, or both. + // + // -We would like to scan all types/methods so we can plug them. + // -But we can't scan them until we plug them, because we will scan things + // that plugs would remove/change the paths of. + // -Plugs may also call methods which are also plugged. + // -We cannot resolve plugs ahead of time but must do on the fly during + // scanning. + // -TODO: Because we do on the fly resolution, we need to add explicit + // checking of plug classes and err when public methods are found that + // do not resolve. Maybe we can make a list and mark, or rescan. Can be done + // later or as an optional auditing step. + // + // This why in the past we had repetitive scans. + // + // Now we focus on more passes, but simpler execution. In the end it should + // be eaiser to optmize and yield overall better performance. Most of the + // passes should be low overhead versus an integrated system which often + // would need to reiterate over items multiple times. So we do more loops on + // with less repetitive analysis, instead of fewer loops but more repetition. + // + // -Locate all plug classes + // -Scan from entry point collecting all types and methods while checking + // for and following plugs + // -For each type + // -Include all ancestors + // -Include all static constructors + // -For each virtual method + // -Scan overloads in descendants until IsFinal, IsSealed or end + // -Scan base in ancestors until top or IsAbstract + // -Go to scan types again, until no new ones found. + // -Because the virtual method scanning will add to the list as it goes, maintain + // 2 lists. + // -Known Types and Methods + // -Types and Methods in Queue - to be scanned + // -Finally, do compilation + + #endregion Description + + mPlugManager.FindPlugImpls(plugsAssemblies); + // Now that we found all plugs, scan them. + // We have to scan them after we find all plugs, because + // plugs can use other plugs + mPlugManager.ScanFoundPlugs(); + foreach (var xPlug in mPlugManager.PlugImpls) + { + CompilerHelpers.Debug($"Plug found: '{xPlug.Key.FullName}' in '{xPlug.Key.Assembly.FullName}'"); + } + + ILOp.PlugManager = mPlugManager; + + // Pull in extra implementations, GC etc. + Queue(VTablesImplRefs.IsInstanceRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.SetTypeInfoRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.SetInterfaceInfoRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.SetMethodInfoRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.SetInterfaceMethodInfoRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.GetMethodAddressForTypeRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.GetMethodAddressForInterfaceTypeRef, null, "Explicit Entry"); + Queue(VTablesImplRefs.GetDeclaringTypeOfMethodForTypeRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.InitRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.IncRootCountRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.IncRootCountsInStructRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.DecRootCountRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.DecRootCountsInStructRef, null, "Explicit Entry"); + Queue(GCImplementationRefs.AllocNewObjectRef, null, "Explicit Entry"); + // for now, to ease runtime exception throwing + Queue(typeof(ExceptionHelper).GetMethod("ThrowNotImplemented", new Type[] { typeof(string) }, null), null, "Explicit Entry"); + Queue(typeof(ExceptionHelper).GetMethod("ThrowOverflow", Type.EmptyTypes, null), null, "Explicit Entry"); + Queue(typeof(ExceptionHelper).GetMethod("ThrowInvalidOperation", new Type[] { typeof(string) }, null), null, "Explicit Entry"); + Queue(typeof(ExceptionHelper).GetMethod("ThrowArgumentOutOfRange", new Type[] { typeof(string) }, null), null, "Explicit Entry"); + + // register system types: + Queue(typeof(Array), null, "Explicit Entry"); + Queue(typeof(Array).Assembly.GetType("System.SZArrayHelper"), null, "Explicit Entry"); + Queue(typeof(Array).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(), null, "Explicit Entry"); + + Queue(typeof(MulticastDelegate).GetMethod("GetInvocationList"), null, "Explicit Entry"); + Queue(ExceptionHelperRefs.CurrentExceptionRef, null, "Explicit Entry"); + Queue(ExceptionHelperRefs.ThrowInvalidCastExceptionRef, null, "Explicit Entry"); + Queue(ExceptionHelperRefs.ThrowNotFiniteNumberExceptionRef, null, "Explicit Entry"); + Queue(ExceptionHelperRefs.ThrowDivideByZeroExceptionRef, null, "Explicit Entry"); + Queue(ExceptionHelperRefs.ThrowIndexOutOfRangeException, null, "Explicit Entry"); + + mAsmblr.ProcessField(typeof(string).GetField("Empty", BindingFlags.Static | BindingFlags.Public)); + + // Start from entry point of this program + Queue(aStartMethod, null, "Entry Point"); + + ScanQueue(); + UpdateAssemblies(); + Assemble(); + + mAsmblr.EmitEntrypoint(aStartMethod); + } + + public void QueueMethod(MethodBase method) + { + Queue(method, null, "Explicit entry via QueueMethod"); + } + + /// This method changes the opcodes. Changes are: + /// * inserting the ValueUID for method ops. + public void ProcessInstructions(List aOpCodes) // to remove ------- + { + foreach (var xOpCode in aOpCodes) + { + if (xOpCode is ILOpCodes.OpMethod xOpMethod) + { + mItems.TryGetValue(xOpMethod.Value, out MemberInfo value); + xOpMethod.Value = (MethodBase)(value ?? xOpMethod.Value); + xOpMethod.ValueUID = GetMethodUID(xOpMethod.Value); + } + } + } + + public void Dispose() + { + if (mLogEnabled) + { + // Create bookmarks, but also a dictionary that + // we can find the items in + var xBookmarks = new Dictionary(); + int xBookmark = 0; + foreach (var xList in mLogMap) + { + foreach (var xItem in xList.Value) + { + xBookmarks.Add(xItem.Item, xBookmark); + xBookmark++; + } + } + + using (mLogWriter = new StreamWriter(File.OpenWrite(mMapPathname))) + { + mLogWriter.WriteLine(""); + foreach (var xList in mLogMap) + { + var xLogItemText = LogItemText(xList.Key); + + mLogWriter.WriteLine("
"); + + // Emit bookmarks above source, so when clicking links user doesn't need + // to constantly scroll up. + foreach (var xItem in xList.Value) + { + mLogWriter.WriteLine(""); + } + + if (!xBookmarks.TryGetValue(xList.Key, out var xHref)) + { + xHref = -1; + } + mLogWriter.Write("

"); + if (xHref >= 0) + { + mLogWriter.WriteLine(""); + mLogWriter.WriteLine("", xHref); + } + if (xList.Key == null) + { + mLogWriter.WriteLine("Unspecified Source"); + } + else + { + mLogWriter.WriteLine(xLogItemText); + } + if (xHref >= 0) + { + mLogWriter.Write(""); + mLogWriter.Write(""); + } + mLogWriter.WriteLine("

"); + + mLogWriter.WriteLine("
    "); + foreach (var xItem in xList.Value) + { + mLogWriter.Write("
  • {0}
  • ", LogItemText(xItem.Item), xBookmarks[xItem.Item]); + + mLogWriter.WriteLine("
      "); + mLogWriter.WriteLine("
    • " + xItem.SrcType + "
    • "); + mLogWriter.WriteLine("
    "); + } + mLogWriter.WriteLine("
"); + } + mLogWriter.WriteLine(""); + } + } + } + + protected string LogItemText(object aItem) + { + if (aItem is MethodBase) + { + var x = (MethodBase)aItem; + return "Method: " + x.DeclaringType + "." + x.Name + "
" + x.GetFullName(); + } + if (aItem is Type) + { + var x = (Type)aItem; + return "Type: " + x.FullName; + } + return "Other: " + aItem; + } + + protected void ScanMethod(MethodBase aMethod, bool aIsPlug, string sourceItem) + { + CompilerHelpers.Debug($"ILScanner: ScanMethod"); + CompilerHelpers.Debug($"Method = '{aMethod}'"); + CompilerHelpers.Debug($"IsPlug = '{aIsPlug}'"); + CompilerHelpers.Debug($"Source = '{sourceItem}'"); + + var xParams = aMethod.GetParameters(); + var xParamTypes = new Type[xParams.Length]; + // Dont use foreach, enum generaly keeps order but + // isn't guaranteed. + //string xMethodFullName = LabelName.GetFullName(aMethod); + + for (int i = 0; i < xParams.Length; i++) + { + xParamTypes[i] = xParams[i].ParameterType; + Queue(xParamTypes[i], aMethod, "Parameter"); + } + var xIsDynamicMethod = aMethod.DeclaringType == null; + // Queue Types directly related to method + if (!aIsPlug) + { + // Don't queue declaring types of plugs + if (!xIsDynamicMethod) + { + // dont queue declaring types of dynamic methods either, those dont have a declaring type + Queue(aMethod.DeclaringType, aMethod, "Declaring Type"); + } + } + if (aMethod is MethodInfo) + { + Queue(((MethodInfo)aMethod).ReturnType, aMethod, "Return Type"); + } + // Scan virtuals + + #region Virtuals scan + + if (!xIsDynamicMethod && aMethod.IsVirtual) + { + // For virtuals we need to climb up the type tree + // and find the top base method. We then add that top + // node to the mVirtuals list. We don't need to add the + // types becuase adding DeclaringType will already cause + // all ancestor types to be added. + + var xVirtMethod = aMethod; + var xVirtType = aMethod.DeclaringType; + MethodBase xNewVirtMethod; + while (true) + { + xVirtType = xVirtType.BaseType; + if (xVirtType == null) + { + // We've reached object, can't go farther + xNewVirtMethod = null; + } + else + { + xNewVirtMethod = xVirtType.GetMethod(aMethod.Name, xParamTypes); + if (xNewVirtMethod != null) + { + if (!xNewVirtMethod.IsVirtual) + { + // This can happen if a virtual "replaces" a non virtual + // above it that is not virtual. + xNewVirtMethod = null; + } + } + } + // We dont bother to add these to Queue, because we have to do a + // full downlevel scan if its a new base virtual anyways. + if (xNewVirtMethod == null) + { + // If its already in the list, we mark it null + // so we dont do a full downlevel scan. + if (mVirtuals.Contains(xVirtMethod)) + { + xVirtMethod = null; + } + break; + } + xVirtMethod = xNewVirtMethod; + } + + // New virtual base found, we need to downscan it + // If it was already in mVirtuals, then ScanType will take + // care of new additions. + if (xVirtMethod != null) + { + Queue(xVirtMethod, aMethod, "Virtual Base"); + mVirtuals.Add(xVirtMethod); + + // List changes as we go, cant be foreach + for (int i = 0; i < mItemsList.Count; i++) + { + if (mItemsList[i] is Type xType && xType != xVirtMethod.DeclaringType && !xType.IsInterface) + { + if (xType.IsSubclassOf(xVirtMethod.DeclaringType)) + { + var enumerable = xType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(method => method.Name == aMethod.Name + && method.GetParameters().Select(param => param.ParameterType).SequenceEqual(xParamTypes)); + // We need to check IsVirtual, a non virtual could + // "replace" a virtual above it? + var xNewMethod = enumerable.FirstOrDefault(m => m.IsVirtual); + while (xNewMethod != null && (xNewMethod.Attributes & MethodAttributes.NewSlot) != 0) + { + xType = xType.BaseType; + xNewMethod = enumerable.Where(m => m.DeclaringType == xType).SingleOrDefault(); + } + if (xNewMethod != null) + { + Queue(xNewMethod, aMethod, "Virtual Downscan"); + } + } + else if (xVirtMethod.DeclaringType.IsInterface + && xType.GetInterfaces().Contains(xVirtMethod.DeclaringType) + && !(xType.BaseType == typeof(Array) && xVirtMethod.DeclaringType.IsGenericType)) + { + var xInterfaceMap = xType.GetInterfaceMap(xVirtMethod.DeclaringType); + var xMethodIndex = Array.IndexOf(xInterfaceMap.InterfaceMethods, xVirtMethod); + + if (xMethodIndex != -1) + { + var xMethod = xInterfaceMap.TargetMethods[xMethodIndex]; + + if (xMethod.DeclaringType == xType) + { + Queue(xInterfaceMap.TargetMethods[xMethodIndex], aMethod, "Virtual Downscan"); + } + } + } + } + } + } + } + + #endregion Virtuals scan + + MethodBase xPlug = null; + // Plugs may use plugs, but plugs won't be plugged over themself + var inl = aMethod.GetCustomAttribute(); + if (!aIsPlug && !xIsDynamicMethod) + { + // Check to see if method is plugged, if it is we don't scan body + xPlug = mPlugManager.ResolvePlug(aMethod, xParamTypes); + if (xPlug != null) + { + //ScanMethod(xPlug, true, "Plug method"); + if (inl == null) + { + Queue(xPlug, aMethod, "Plug method"); + } + } + } + + if (xPlug == null) + { + bool xNeedsPlug = false; + if ((aMethod.Attributes & MethodAttributes.PinvokeImpl) != 0) + { + // pinvoke methods dont have an embedded implementation + xNeedsPlug = true; + } + else + { + var xImplFlags = aMethod.GetMethodImplementationFlags(); + // todo: prob even more + if (xImplFlags.HasFlag(MethodImplAttributes.Native) || xImplFlags.HasFlag(MethodImplAttributes.InternalCall)) + { + // native implementations cannot be compiled + xNeedsPlug = true; + } + } + if (xNeedsPlug) + { + throw new Exception("Native code encountered, plug required. Check build output for more information." + Environment.NewLine + + " DO NOT REPORT THIS AS A BUG." + Environment.NewLine + + " Please see http://www.gocosmos.org/docs/plugs/missing/" + Environment.NewLine + + " Need plug for: " + LabelName.GetFullName(aMethod, false) + "(Plug Signature: " + DataMember.FilterStringForIncorrectChars(LabelName.GetFullName(aMethod, false)) + " ). " + Environment.NewLine + + " Static: " + aMethod.IsStatic + Environment.NewLine + + " Assembly: " + aMethod.DeclaringType.Assembly.FullName + Environment.NewLine + + " Called from:" + Environment.NewLine + sourceItem + Environment.NewLine); + } + + //TODO: As we scan each method, we could update or put in a new list + // that has the resolved plug so we don't have to reresolve it again + // later for compilation. + + // Scan the method body for more type and method refs + //TODO: Dont queue new items if they are plugged + // or do we need to queue them with a resolved ref in a new list? + + if (inl != null) + { + return; // cancel inline + } + + var xOpCodes = mReader.ProcessMethod(aMethod); + if (xOpCodes != null) + { + ProcessInstructions(xOpCodes); + foreach (var xOpCode in xOpCodes) + { + if (xOpCode is ILOpCodes.OpMethod) + { + Queue(((ILOpCodes.OpMethod)xOpCode).Value, aMethod, "Call", sourceItem); + } + else if (xOpCode is ILOpCodes.OpType xOpType) + { + Queue(((ILOpCodes.OpType)xOpCode).Value, aMethod, "OpCode Value"); + } + else if (xOpCode is ILOpCodes.OpField xOpField) + { + //TODO: Need to do this? Will we get a ILOpCodes.OpType as well? + Queue(xOpField.Value.DeclaringType, aMethod, "OpCode Value"); + if (xOpField.Value.IsStatic) + { + //TODO: Why do we add static fields, but not instance? + // AW: instance fields are "added" always, as part of a type, but for static fields, we need to emit a datamember + Queue(xOpField.Value, aMethod, "OpCode Value"); + } + } + else if (xOpCode is ILOpCodes.OpToken xOpToken) + { + if (xOpToken.ValueIsType) + { + Queue(xOpToken.ValueType, aMethod, "OpCode Value"); + } + if (xOpToken.ValueIsField) + { + Queue(xOpToken.ValueField.DeclaringType, aMethod, "OpCode Value"); + if (xOpToken.ValueField.IsStatic) + { + //TODO: Why do we add static fields, but not instance? + // AW: instance fields are "added" always, as part of a type, but for static fields, we need to emit a datamember + Queue(xOpToken.ValueField, aMethod, "OpCode Value"); + } + } + } + } + } + } + } + + protected void ScanType(Type aType) + { + CompilerHelpers.Debug($"ILScanner: ScanType"); + CompilerHelpers.Debug($"Type = '{aType}'"); + + // This is a bit overkill, most likely we dont need all these methods + // but I dont see a better way to do it easily + // so for generic interface methods on arrays, we just add all methods + if (aType.Name.Contains("SZArrayImpl")) + { + foreach (var xMethod in aType.GetMethods()) + { + Queue(xMethod, aType, "Generic Interface Method"); + } + } + + if (aType.IsGenericType && new string[] { "IList", "ICollection", "IEnumerable", "IReadOnlyList", "IReadOnlyCollection" } + .Any(i => aType.Name.Contains(i))) + { + Queue(aType.GenericTypeArguments[0].MakeArrayType(), aType, "CallVirt of Generic Interface for Array"); + } + + // Add immediate ancestor type + // We dont need to crawl up farther, when the BaseType is scanned + // it will add its BaseType, and so on. + if (aType.BaseType != null) + { + Queue(aType.BaseType, aType, "Base Type"); + } + // Queue static ctors + // We always need static ctors, else the type cannot + // be created. + foreach (var xCctor in aType.GetConstructors(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + { + if (xCctor.DeclaringType == aType) + { + Queue(xCctor, aType, "Static Constructor"); + } + } + + if (aType.BaseType == typeof(Array) && !aType.GetElementType().IsPointer) + { + var szArrayHelper = typeof(Array).Assembly.GetType("System.SZArrayHelper"); // We manually add the link to the generic interfaces for an array + foreach (var xMethod in szArrayHelper.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + Queue(xMethod.MakeGenericMethod(new Type[] { aType.GetElementType() }), aType, "Virtual SzArrayHelper"); + } + + Queue(typeof(SZArrayImpl<>).MakeGenericType(aType.GetElementType()), aType, "Array"); + } + + + // Scam Fields so that we include those types + foreach (var field in aType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + Queue(field.FieldType, aType, "Field Type"); + } + + // For each new type, we need to scan for possible new virtuals + // in our new type if its a descendant of something in + // mVirtuals. + foreach (var xVirt in mVirtuals) + { + // See if our new type is a subclass of any virt's DeclaringTypes + // If so our new type might have some virtuals + if (aType.IsSubclassOf(xVirt.DeclaringType)) + { + var xParams = xVirt.GetParameters(); + var xParamTypes = new Type[xParams.Length]; + // Dont use foreach, enum generaly keeps order but + // isn't guaranteed. + for (int i = 0; i < xParams.Length; i++) + { + xParamTypes[i] = xParams[i].ParameterType; + } + var xMethod = aType.GetMethod(xVirt.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, xParamTypes, null); + if (xMethod != null) + { + // We need to check IsVirtual, a non virtual could + // "replace" a virtual above it? + if (xMethod.IsVirtual) + { + Queue(xMethod, aType, "Virtual"); + } + } + } + else if (!aType.IsGenericParameter && xVirt.DeclaringType.IsInterface && !(aType.BaseType == typeof(Array) && xVirt.DeclaringType.IsGenericType)) + { + if (!aType.IsInterface && aType.GetInterfaces().Contains(xVirt.DeclaringType) + && !(aType.BaseType == typeof(Array) && xVirt.DeclaringType.IsGenericType)) + { + var xIntfMapping = aType.GetInterfaceMap(xVirt.DeclaringType); + if (xIntfMapping.InterfaceMethods != null && xIntfMapping.TargetMethods != null) + { + var xIdx = Array.IndexOf(xIntfMapping.InterfaceMethods, xVirt); + if (xIdx != -1) + { + Queue(xIntfMapping.TargetMethods[xIdx], aType, "Virtual"); + } + } + } + } + } + + foreach (var xInterface in aType.GetInterfaces()) + { + Queue(xInterface, aType, "Implemented Interface"); + } + } + + protected void ScanQueue() + { + while (mQueue.Count > 0) + { + var xItem = mQueue.Dequeue(); + CompilerHelpers.Debug($"ILScanner: ScanQueue - '{xItem}'"); + // Check for MethodBase first, they are more numerous + // and will reduce compares + if (xItem.Item is MethodBase xMethod) + { + ScanMethod(xMethod, false, xItem.SourceItem); + } + else if (xItem.Item is Type xType) + { + ScanType(xType); + + // Methods and fields cant exist without types, so we only update + // mUsedAssemblies in type branch. + if (!mUsedAssemblies.Contains(xType.Assembly)) + { + mUsedAssemblies.Add(xType.Assembly); + } + } + else if (xItem.Item is FieldInfo) + { + // todo: static fields need more processing? + } + else + { + throw new Exception("Unknown item found in queue."); + } + } + } + + protected void LogMapPoint(object aSrc, string aSrcType, object aItem) + { + // Keys cant be null. If null, we just say ILScanner is the source + if (aSrc == null) + { + aSrc = typeof(ILScanner); + } + + var xLogItem = new LogItem + { + SrcType = aSrcType, + Item = aItem + }; + if (!mLogMap.TryGetValue(aSrc, out var xList)) + { + xList = new List(); + mLogMap.Add(aSrc, xList); + } + xList.Add(xLogItem); + } + + private MethodInfo GetUltimateBaseMethod(MethodInfo aMethod) + { + var xBaseMethod = aMethod; + + while (true) + { + var xBaseDefinition = xBaseMethod.GetBaseDefinition(); + + if (xBaseDefinition == xBaseMethod) + { + return xBaseMethod; + } + + xBaseMethod = xBaseDefinition; + } + } + + protected uint GetMethodUID(MethodBase aMethod) + { + if (mMethodUIDs.TryGetValue(aMethod, out var xMethodUID)) + { + return xMethodUID; + } + else + { + if (!aMethod.DeclaringType.IsInterface) + { + if (aMethod is MethodInfo xMethodInfo) + { + var xBaseMethod = GetUltimateBaseMethod(xMethodInfo); + + if (!mMethodUIDs.TryGetValue(xBaseMethod, out xMethodUID)) + { + xMethodUID = (uint)mMethodUIDs.Count; + mMethodUIDs.Add(xBaseMethod, xMethodUID); + } + + if (!new MethodBaseComparer().Equals(aMethod, xBaseMethod)) + { + mMethodUIDs.Add(aMethod, xMethodUID); + } + + return xMethodUID; + } + } + + xMethodUID = (uint)mMethodUIDs.Count; + mMethodUIDs.Add(aMethod, xMethodUID); + + return xMethodUID; + } + } + + protected uint GetTypeUID(Type aType) + { + if (!mItems.Contains(aType)) + { + throw new Exception($"Cannot get UID of types which are not queued! Type: {aType.Name}"); + } + if (!mTypeUIDs.ContainsKey(aType)) + { + var xId = (uint)mTypeUIDs.Count; + mTypeUIDs.Add(aType, xId); + return xId; + } + return mTypeUIDs[aType]; + } + + protected void UpdateAssemblies() + { + // It would be nice to keep DebugInfo output into assembler only but + // there is so much info that is available in scanner that is needed + // or can be used in a more efficient manner. So we output in both + // scanner and assembler as needed. + mAsmblr.DebugInfo.AddAssemblies(mUsedAssemblies); + } + + protected void Assemble() + { + foreach (var xItem in mItems) + { + if (xItem is MethodBase xMethod) + { + var xParams = xMethod.GetParameters(); + var xParamTypes = xParams.Select(q => q.ParameterType).ToArray(); + var xPlug = mPlugManager.ResolvePlug(xMethod, xParamTypes); + var xMethodType = Il2cpuMethodInfo.TypeEnum.Normal; + Type xPlugAssembler = null; + Il2cpuMethodInfo xPlugInfo = null; + var xMethodInline = xMethod.GetCustomAttribute(); + if (xMethodInline != null) + { + // inline assembler, shouldn't come here.. + continue; + } + var xMethodIdMethod = mItemsList.IndexOf(xMethod); + if (xMethodIdMethod == -1) + { + throw new Exception("Method not in scanner list!"); + } + PlugMethod xPlugAttrib = null; + if (xPlug != null) + { + xMethodType = Il2cpuMethodInfo.TypeEnum.NeedsPlug; + xPlugAttrib = xPlug.GetCustomAttribute(); + var xInlineAttrib = xPlug.GetCustomAttribute(); + var xMethodIdPlug = mItemsList.IndexOf(xPlug); + if (xMethodIdPlug == -1 && xInlineAttrib == null) + { + throw new Exception("Plug method not in scanner list!"); + } + if (xPlugAttrib != null && xInlineAttrib == null) + { + xPlugAssembler = xPlugAttrib.Assembler; + xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodIdPlug, Il2cpuMethodInfo.TypeEnum.Plug, null, xPlugAssembler); + + var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); + if (xPlugAttrib.IsWildcard) + { + xPlugInfo.IsWildcard = true; + xPlugInfo.PluggedMethod = xMethodInfo; + var xInstructions = mReader.ProcessMethod(xPlug); + if (xInstructions != null) + { + ProcessInstructions(xInstructions); + mAsmblr.ProcessMethod(xPlugInfo, xInstructions, mPlugManager); + } + } + mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); + } + else + { + if (xInlineAttrib != null) + { + var xMethodID = mItemsList.IndexOf(xItem); + if (xMethodID == -1) + { + throw new Exception("Method not in list!"); + } + xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodID, Il2cpuMethodInfo.TypeEnum.Plug, null, true); + + var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); + + xPlugInfo.PluggedMethod = xMethodInfo; + var xInstructions = mReader.ProcessMethod(xPlug); + if (xInstructions != null) + { + ProcessInstructions(xInstructions); + mAsmblr.ProcessMethod(xPlugInfo, xInstructions, mPlugManager); + } + mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); + } + else + { + xPlugInfo = new Il2cpuMethodInfo(xPlug, (uint)xMethodIdPlug, Il2cpuMethodInfo.TypeEnum.Plug, null, xPlugAssembler); + + var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo); + mAsmblr.GenerateMethodForward(xMethodInfo, xPlugInfo); + } + } + } + else + { + xPlugAttrib = xMethod.GetCustomAttribute(); + + if (xPlugAttrib != null) + { + if (xPlugAttrib.IsWildcard) + { + continue; + } + if (xPlugAttrib.PlugRequired) + { + throw new Exception(String.Format("Method {0} requires a plug, but none is implemented", xMethod.Name)); + } + xPlugAssembler = xPlugAttrib.Assembler; + } + + var xMethodInfo = new Il2cpuMethodInfo(xMethod, (uint)xMethodIdMethod, xMethodType, xPlugInfo, xPlugAssembler); + var xInstructions = mReader.ProcessMethod(xMethod); + if (xInstructions != null) + { + ProcessInstructions(xInstructions); + mAsmblr.ProcessMethod(xMethodInfo, xInstructions, mPlugManager); + } + } + } + else if (xItem is FieldInfo) + { + mAsmblr.ProcessField((FieldInfo)xItem); + } + } + + var xTypes = new HashSet(); + var xMethods = new HashSet(new MethodBaseComparer()); + foreach (var xItem in mItems) + { + if (xItem is MethodBase) + { + xMethods.Add((MethodBase)xItem); + } + else if (xItem is Type) + { + xTypes.Add((Type)xItem); + } + } + + mAsmblr.GenerateVMTCode(xTypes, xMethods, mPlugManager, GetTypeUID, GetMethodUID); + } + } } \ No newline at end of file diff --git a/source/Cosmos.IL2CPU/Optimization/IOptimizerLookaheadProvider.cs b/source/Cosmos.IL2CPU/Optimization/IOptimizerLookaheadProvider.cs new file mode 100644 index 000000000..7aa1dff02 --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/IOptimizerLookaheadProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Cosmos.IL2CPU.Optimization +{ + /// + /// Represents an object that can provide methods that can be used in + /// look-ahead operations by an . + /// + /// + /// A look-ahead is used when an requires to + /// compute the method body of a method it hasn't been provided with before. + /// + public interface IOptimizerLookaheadProvider + { + /// + /// Provides the method body for the given method, using a look-ahead lookup. + /// When using a optimization look-ahead, no optimizations are applied, and + /// a different cache is used. + /// + /// The method to compute the body of. + public List ProcessMethodAhead(MethodBase aMethod); + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/Optimizer.cs b/source/Cosmos.IL2CPU/Optimization/Optimizer.cs new file mode 100644 index 000000000..a059ffa4f --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/Optimizer.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Cosmos.IL2CPU.Optimization +{ + /// + /// Optimizes IL methods. + /// + public class Optimizer + { + /// + /// The method look-ahead provider that can be used to compute the method body of a method it hasn't been provided with before. + /// + public IOptimizerLookaheadProvider LookaheadProvider { get; set; } + + /// + /// The passes that the optimizer will use. + /// + public List Passes { get; init; } = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The look-ahead provider to use. + public Optimizer(IOptimizerLookaheadProvider aLookaheadProvider) + { + LookaheadProvider = aLookaheadProvider ?? throw new ArgumentNullException(nameof(aLookaheadProvider)); + } + + /// + /// Exclude the given type of optimizer passes in this optimizer. + /// + /// The type to exclude. + /// Thrown when the given type is not a subclass of . + public void ExcludePassType(Type passType) + { + if(!passType.IsSubclassOf(typeof(OptimizerPass))) { + throw new InvalidOperationException("The given type is not a subclass of OptimizerPass."); + } + + for (int i = Passes.Count - 1; i >= 0; i--) { + if (Passes[i].GetType() == passType) { + Passes.RemoveAt(i); + } + } + } + + /// + /// Optimizes the given method. + /// + /// The body of the method, in IL instructions. + /// The optimized method body. + public List Optimize(List instructions) + { + foreach (var pass in Passes) { + instructions = pass.Process(instructions); + } + + return instructions; + } + + /// + /// Adds a pass to this and returns the object. + /// + /// The pass to add to the optimizer. + public Optimizer WithPass(OptimizerPass aPass) + { + aPass.Owner = this; + Passes.Add(aPass); + return this; + } + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs b/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs new file mode 100644 index 000000000..ff3146531 --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/OptimizerPass.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cosmos.IL2CPU.Optimization +{ + /// + /// Represents a single optimization pass. + /// + public abstract class OptimizerPass + { + /// + /// The owner of this . + /// + public Optimizer Owner { get; internal set; } + + /// + /// Optimizes the given method. + /// + /// The body of the target method. + /// The optimized method body. + public abstract List Process(List instructions); + } +} diff --git a/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs b/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs new file mode 100644 index 000000000..b8c5615ec --- /dev/null +++ b/source/Cosmos.IL2CPU/Optimization/Passes/InlineDirectPropertiesPass.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Cosmos.IL2CPU.ILOpCodes; + +namespace Cosmos.IL2CPU.Optimization.Passes +{ + /// + /// Inlines direct property accesses. A direct property access is a call + /// to a getter/setter method that directly gets or sets an underlying field, + /// which can be simplified to a single instruction. + /// + internal class InlineDirectPropertiesPass : OptimizerPass + { + // Control value to cancel optimization by look-ahead provider + bool processing = false; + + public override List Process(List il) + { + if (processing) { + return il; + } + + processing = true; + + var callsToInline = Enumerable.Range(0, il.Count) + // Select both the index and the instruction under that index. + .Select(i => (idx: i, inst: il[i])) + // Filter to only call instructions from which we can get a MethodBase. + .Where(x => x.inst.OpCode == ILOpCode.Code.Call && x.inst is OpMethod) + // Transform to (int, OpMethod). + .Select(x => (x.idx, inst: (OpMethod)x.inst)) + // Ensure that inst.Value is not null. + .Where(x => x.inst.Value != null) + // Apply a look-ahead to see what method this instruction wants to call. + .Select(x => (x.idx, x.inst, body: Owner.LookaheadProvider.ProcessMethodAhead(x.inst.Value))) + // Some methods return null bodies; filter them out. + .Where(x => x.body != null) + // Find the inline instruction replacements for the method calls, now that we have the body of the method its trying to call. + .Select(x => (x.idx, x.inst, x.body, inline: GetPropertyInlineReplacement(x.inst.Value, x.body))) + // If there is no inline replacement, don't include it in the list. + .Where(x => x.inline != null); + + foreach (var x in callsToInline) { + // Create a new OpField that fits into our sequence + // OpField and OpMethod should have the same length, so we can just replace Position and NextPosition + // with the OpMethod instructions we are replacing + var inline = new OpField( + x.inline.OpCode, + il[x.idx].Position, il[x.idx].NextPosition, + x.inline.Value, + x.inline.CurrentExceptionRegion + ); + + il[x.idx] = inline; + } + + processing = false; + return il; + } + + + /// + /// Determines whether the provided method body is a direct property accessor + /// and returns the field target instruction. + /// + private static OpField GetPropertyInlineReplacement(MethodBase info, List il) + { + var filtered = il.Where(x => x.OpCode != ILOpCode.Code.Nop) + .ToArray(); + + if (info is not MethodInfo moreInfo) { + // The method information isn't MethodInfo... this shouldn't happen! + return null; + } + + bool setter = false; + if(moreInfo.ReturnType == typeof(void) && info.GetParameters().Length == 1) { + setter = true; + } else if(moreInfo.GetParameters().Length != 0){ + // Not a setter, and accepts parameters; not a property + return null; + } + + if (info.IsStatic) { + if(setter) { + var isDirectAccess = + filtered.Length >= 3 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Stsfld && + filtered[2].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property loads argument 0 (value), that is in turn taken from the stack. + // The field then is changed with the value on the stack and returns; replace the call + // with the stsfld instruction (that sets the field). + return (OpField)filtered[1]; + } + } else { + var isDirectAccess = + filtered.Length >= 2 && + filtered[0].OpCode == ILOpCode.Code.Ldsfld && + filtered[1].OpCode == ILOpCode.Code.Ret; + + if (isDirectAccess) { + // The property pushes the field on the stack and returns; replace the call + // with the ldsfld instruction (that pushes the field). + return (OpField)filtered[0]; + } + } + } + else { + if(setter) { + var isDirectAccess = + filtered.Length >= 4 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[1]).Value == 1 && + filtered[2].OpCode == ILOpCode.Code.Stfld && + filtered[3].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property pushes two arguments onto the stack; the instance pointer + // and the value, and calls stfld. In order to call this method, the caller + // must also push the target instance pointer (even if the caller is the object + // that the property belongs to; in that case, it would do "ldarg.0") and the + // value. The stack layout does not change, meaning we can directly replace the call. + return (OpField)filtered[2]; + } + } else { + var isDirectAccess = + filtered.Length >= 3 && + filtered[0].OpCode == ILOpCode.Code.Ldarg && ((OpVar)filtered[0]).Value == 0 && + filtered[1].OpCode == ILOpCode.Code.Ldfld && + filtered[2].OpCode == ILOpCode.Code.Ret; + + if(isDirectAccess) { + // The property loads argument 0 (self pointer), executes the instruction + // ldfld, and returns. In order to call the property, we already push argument 0, + // meaning we can direclty replace the call. + return (OpField)filtered[1]; + } + } + } + + // No instruction that can be used to directly replace the call + return null; + } + } +} diff --git a/source/Cosmos.IL2CPU/Optimizer.cs b/source/Cosmos.IL2CPU/Optimizer.cs deleted file mode 100644 index ff6df9cbb..000000000 --- a/source/Cosmos.IL2CPU/Optimizer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using XSharp.Assembler; - -namespace Cosmos.IL2CPU -{ - internal static class Optimizer - { - public static Assembler Optimize(Assembler asmb) - { - return asmb; - //Assembler asmblr = asmb; - //List instr = asmb.Instructions; - ////List dmbrs = asmb.DataMembers; - - //SortedDictionary labels = new SortedDictionary(); - //List comments = new List(); - //List usedLabels = new List(); - //usedLabels.Add("KernelStart"); - //foreach (Instruction ins in instr) { - // if (ins is Label) { - // if (((Label)ins).IsGlobal) { - // usedLabels.Add(((Label)ins).QualifiedName); - // } - // labels.Add(((Label)ins).QualifiedName, ins); - // } else if (ins is x86.JumpToSegment) { - // if (((x86.JumpToSegment)ins).DestinationRef != null) { - // usedLabels.Add(((x86.JumpToSegment)ins).DestinationRef.Name); - // } else { - // usedLabels.Add(((x86.JumpToSegment)ins).DestinationLabel); - // } - // } else if (ins is x86.JumpBase) { - // usedLabels.Add(((x86.JumpBase)ins).DestinationLabel); - // } else if (ins is x86.Call) { - // usedLabels.Add(((x86.Call)ins).DestinationLabel); - // } else if (ins is x86.Push) { - // if (((x86.Push)ins).DestinationRef != null) { - // usedLabels.Add(((x86.Push)ins).DestinationRef.Name); - // } - // } else if (ins is x86.Mov) { - // if (((x86.Mov)ins).SourceRef != null) { - // usedLabels.Add(((x86.Mov)ins).SourceRef.Name); - // } - // } - //} - //foreach (string s in usedLabels) { - // labels.Remove(s); - //} - //usedLabels = null; - //instr.RemoveAll( - // delegate(Instruction inst) { - // if (inst is Comment) - // return true; - // else if (inst is Label) { - // if (labels.ContainsKey(((Label)inst).QualifiedName)) - // return true; - // return false; - // } - // return false; - // } - //); - //labels = null; - //comments = null; - - - - - //asmblr.Instructions = instr; - ////asmblr.DataMembers = dmbrs; - //return asmblr; - - } - - } -} \ No newline at end of file diff --git a/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs b/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs index 296009cee..6aa8eda75 100644 --- a/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs +++ b/source/Cosmos.IL2CPU/_ExceptionRegionInfo.cs @@ -4,49 +4,100 @@ namespace Cosmos.IL2CPU { + /// + /// Represents the information about a clause in a structured exception-handling block. + /// public class _ExceptionRegionInfo { + /// + /// The exception handling clause that this object describes. + /// public ExceptionHandlingClause ExceptionClause { get; } + + /// + /// Gets the offset within the method body, in bytes, of this exception-handling clause. + /// public int HandlerOffset { get; } + + /// + /// Gets the length, in bytes, of the body of this exception-handling clause. + /// public int HandlerLength { get; } + + /// + /// The offset within the method, in bytes, of the try block that includes this exception-handling clause. + /// public int TryOffset { get; } + + /// + /// The total length, in bytes, of the try block that includes this exception-handling clause. + /// public int TryLength { get; } + + /// + /// Gets the offset within the method body, in bytes, of the user-supplied filter code. + /// public int FilterOffset { get; } + + /// + /// Gets the type of this exception region. + /// public ExceptionRegionKind Kind { get; } + + /// + /// Gets the type of exception handled by this clause. + /// public Type CatchType { get; } + /// + /// Creates a new instance of the class, describing + /// the given exception handling clause. + /// + /// The exception handling clause that the newly created object should describe. public _ExceptionRegionInfo(ExceptionHandlingClause aExceptionClause) + : this( + aExceptionClause, + aExceptionClause.HandlerOffset, + aExceptionClause.HandlerLength, + aExceptionClause.TryOffset, + aExceptionClause.TryLength, + aExceptionClause.Flags == ExceptionHandlingClauseOptions.Filter ? aExceptionClause.FilterOffset : 0 + ) + { + + } + + /// + /// Creates a new instance of the class, describing + /// the given exception handling clause with the given region offsets and lengths. + /// + /// The exception handling clause that the newly created object should describe. + internal _ExceptionRegionInfo(ExceptionHandlingClause aExceptionClause, int aHandlerOffset, int aHandlerLength, int aTryOffset, int aTryLength, int aFilterOffset) { - try - { + try { ExceptionClause = aExceptionClause; - HandlerOffset = aExceptionClause.HandlerOffset; - HandlerLength = aExceptionClause.HandlerLength; - TryOffset = aExceptionClause.TryOffset; - TryLength = aExceptionClause.TryLength; + HandlerOffset = aHandlerOffset; + HandlerLength = aHandlerLength; + TryOffset = aTryOffset; + TryLength = aTryLength; - if (aExceptionClause.Flags == ExceptionHandlingClauseOptions.Clause) - { + if (aExceptionClause.Flags == ExceptionHandlingClauseOptions.Clause) { Kind = ExceptionRegionKind.Catch; CatchType = aExceptionClause.CatchType; } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Fault)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Fault)) { Kind = ExceptionRegionKind.Fault; } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Filter)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Filter)) { Kind = ExceptionRegionKind.Filter; - FilterOffset = aExceptionClause.FilterOffset; + FilterOffset = aFilterOffset; CatchType = typeof(System.Exception); //TODO: Confirm that this is correct. } - else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Finally)) - { + else if (aExceptionClause.Flags.HasFlag(ExceptionHandlingClauseOptions.Finally)) { Kind = ExceptionRegionKind.Finally; } } - catch - { + catch { // ignored } }