diff --git a/src/Simulation/Common/Microsoft.Quantum.Simulation.Common.csproj b/src/Simulation/Common/Microsoft.Quantum.Simulation.Common.csproj index 369209ad295..54886046904 100644 --- a/src/Simulation/Common/Microsoft.Quantum.Simulation.Common.csproj +++ b/src/Simulation/Common/Microsoft.Quantum.Simulation.Common.csproj @@ -8,6 +8,10 @@ x64 + + + + diff --git a/src/Simulation/Common/PortablePDBReader.cs b/src/Simulation/Common/PortablePDBReader.cs new file mode 100644 index 00000000000..6db1b53b108 --- /dev/null +++ b/src/Simulation/Common/PortablePDBReader.cs @@ -0,0 +1,396 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Quantum.Simulation.Common +{ + using System.Collections.Generic; + using System.Reflection.Metadata; + using System; + using System.IO; + using System.IO.Compression; + using System.Text; + using Newtonsoft.Json.Linq; + using System.Text.RegularExpressions; + using Microsoft.Quantum.Simulation.Core; + using System.Linq; + + public class CompressedSourceFile + { + private readonly byte[] compressedSource = null; + private string decompressedSource = null; + + public CompressedSourceFile(byte[] compressedSource) + { + this.compressedSource = compressedSource; + } + + public override string ToString() + { + if (decompressedSource == null) + { + using MemoryStream memoryStream = new MemoryStream(compressedSource, sizeof(Int32), compressedSource.Length - sizeof(Int32)); + using DeflateStream decompressionStream = new DeflateStream(memoryStream, CompressionMode.Decompress); + Int32 uncompressedSourceFileSize = BitConverter.ToInt32(compressedSource, 0); + MemoryStream decompressed = new MemoryStream(new byte[uncompressedSourceFileSize], true); + decompressionStream.CopyTo(decompressed); + decompressedSource = Encoding.UTF8.GetString(decompressed.ToArray()); + } + + return decompressedSource; + } + } + + /// + /// Class for source link information corresponding to SourceLink schema + /// https://github.com/dotnet/designs/blob/master/accepted/diagnostics/source-link.md#source-link-json-schema + /// + [Serializable] + public class SourceLinkInfo + { + public Dictionary documents; + + /// + /// Collection of patterns present within documents + /// + /// + /// For example, documents can contain patterns with *, like shown below + /// + /// { "documents": { "C:\\src\\CodeFormatter\\*": "https://raw.githubusercontent.com/dotnet/codeformatter/bcc51178e1a82fb2edaf47285f6e577989a7333f/*" },} + /// + /// + [Newtonsoft.Json.JsonIgnore] + private ValueTuple[] patterns; + + // Collection of patterns present within documents + [Newtonsoft.Json.JsonIgnore] + public ValueTuple[] Patterns + { + get + { + if (this.patterns == null) + { + List> patterns = new List>(); + foreach (KeyValuePair keyValuePair in documents) + { + if (keyValuePair.Key.EndsWith("*")) + { + patterns.Add(new ValueTuple(keyValuePair.Key.Replace("*", ""), keyValuePair.Value.Replace("*", ""))); + } + } + this.patterns = patterns.ToArray(); + } + return this.patterns; + } + } + } + + /// + /// Utility class for extracting source file text and source location from PortablePDB meta-data. + /// + /// + /// Based on https://github.com/microsoft/BPerf/blob/master/WebViewer/Microsoft.BPerf.SymbolicInformation.ProgramDatabase/PortablePdbSymbolReader.cs + /// + public class PortablePdbSymbolReader + { + /// SourceLink GUID is a part of PortablePDB meta-data specification https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PortablePdb-Metadata.md#SourceLink + private static readonly Guid SourceLink = new Guid("CC110556-A091-4D38-9FEC-25AB9A351A6A"); + + /// EmbeddedSource GUID is a part of PortablePDB meta-data specification https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PortablePdb-Metadata.md#embedded-source-c-and-vb-compilers + private static readonly Guid EmbeddedSource = new Guid("0E8A571B-6926-466E-B4AD-8AB04611F5FE"); + + /// + /// Unpacks all files stored in a PortablePDB meta-data. The key in the dictionary is the location of a source file + /// on the build machine. The value is the content of the source file itself. + /// The function will throw an exception if PortablePDB file is not found or anything else went wrong. + /// + /// Path to PortablePDB file to load source files from. + public static Dictionary GetEmbeddedFiles(string pdbFilePath) + { + Dictionary embeddedFiles = new Dictionary(); + + using (FileStream stream = File.OpenRead(pdbFilePath)) + using (MetadataReaderProvider metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(stream)) + { + MetadataReader metadataReader = metadataReaderProvider.GetMetadataReader(); + CustomDebugInformationHandleCollection customDebugInformationHandles = metadataReader.CustomDebugInformation; + foreach (var customDebugInformationHandle in customDebugInformationHandles) + { + CustomDebugInformation customDebugInformation = metadataReader.GetCustomDebugInformation(customDebugInformationHandle); + if (metadataReader.GetGuid(customDebugInformation.Kind) == EmbeddedSource) + { + byte[] embeddedSource = metadataReader.GetBlobBytes(customDebugInformation.Value); + Int32 uncompressedSourceFileSize = BitConverter.ToInt32(embeddedSource, 0); + if (uncompressedSourceFileSize != 0) + { + Document document = metadataReader.GetDocument((DocumentHandle)customDebugInformation.Parent); + string sourceFileName = System.IO.Path.GetFullPath(metadataReader.GetString(document.Name)); + embeddedFiles.Add(sourceFileName, new CompressedSourceFile(embeddedSource)); + } + } + } + } + return embeddedFiles; + } + + /// + /// Returns SourceLink information, that is the source link information as described at https://github.com/dotnet/designs/blob/master/accepted/diagnostics/source-link.md#source-link-json-schema + /// stored in PortablePDB. + /// The function will throw an exception if PortablePDB file is not found or anything else went wrong. + /// + /// Path to PortablePDB file + public static SourceLinkInfo GetSourceLinkInfo(string pdbFilePath) + { + using (FileStream stream = File.OpenRead(pdbFilePath)) + { + using (MetadataReaderProvider metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(stream)) + { + MetadataReader metadataReader = metadataReaderProvider.GetMetadataReader(); + CustomDebugInformationHandleCollection customDebugInformationHandles = metadataReader.CustomDebugInformation; + foreach (var customDebugInformationHandle in customDebugInformationHandles) + { + CustomDebugInformation customDebugInformation = metadataReader.GetCustomDebugInformation(customDebugInformationHandle); + + if (metadataReader.GetGuid(customDebugInformation.Kind) == SourceLink) + { + string jsonString = Encoding.UTF8.GetString(metadataReader.GetBlobBytes(customDebugInformation.Value)); + return Newtonsoft.Json.JsonConvert.DeserializeObject(jsonString); + } + } + } + } + return null; + } + + /// + /// Includes line number into GitHub URL with source location + /// + /// Tuple of baseURL describing the repository and commit and relative path to the file + /// Line number to include into URL + /// GitHub URL for the file with the line number + public static string TryFormatGitHubUrl(string rawUrl, int lineNumber) + { + if (rawUrl == null) + return null; + + string result = rawUrl; + if (rawUrl.StartsWith(@"https://raw.githubusercontent.com")) + { + // Our goal is to replace raw GitHub URL, for example something like: + // https://raw.githubusercontent.com/microsoft/qsharp-runtime/af6262c05522d645d0a0952272443e84eeab677a/src/Xunit/TestCaseDiscoverer.cs + // By a permanent link GitHub URL that includes line number + // https://github.com/microsoft/qsharp-runtime/blob/af6262c05522d645d0a0952272443e84eeab677a/src/Xunit/TestCaseDiscoverer.cs#L13 + // To make sure that when a user clicks the link to GitHub the line number is highlighted + + MatchCollection sha1StringMatches = Regex.Matches(rawUrl, Regex.Escape("/") + "[a-f0-9]{40}" + Regex.Escape("/")); // SHA1 part of the URL is 40 symbols long + Match sha1StringMatch = null; + + if (sha1StringMatches.Count == 1) + { + sha1StringMatch = sha1StringMatches[0]; + } + else // there are several sub-strings of URL that can potentially be a sha1 hash, we fall-back to original URL in this case + { + return rawUrl; + } + + if (sha1StringMatch.Success) + { + int startPosition = sha1StringMatch.Index; + string sha1String = sha1StringMatch.Value; //should be "/af6262c05522d645d0a0952272443e84eeab677a/" + string urlAndRepositoryPath = rawUrl.Substring(0, startPosition); // should be "https://raw.githubusercontent.com/microsoft/qsharp-runtime" + string relativePath = rawUrl.Substring(startPosition + sha1String.Length); // should be "src/Xunit/TestCaseDiscoverer.cs" + return $"{urlAndRepositoryPath.Replace(@"https://raw.githubusercontent.com", @"https://github.com")}{"/blob"}{sha1String}{relativePath}#L{lineNumber}"; + } + } + return result; + } + + /// + /// Returns location of PortablePDB file with the source information for a given callable. + /// Returns null if PortablePDB cannot be found. + /// + public static string GetPDBLocation(ICallable callable) + { + try + { + string filename = System.IO.Path.ChangeExtension(callable.UnwrapCallable().GetType().Assembly.Location, ".pdb"); + if (File.Exists(filename)) + { + return filename; + } + else + { + return null; + } + } + catch (NotSupportedException) + { + return null; + } + } + } + + /// + /// Caches path remapping from build machine to URL per location of PDB file. + /// + public static class PortablePDBSourceLinkInfoCache + { + /// + /// Key is the location of a PortablePDB file on a current machine + /// + // ThreadStaticAttribute makes sure that the cache is thread safe + [ThreadStatic] + private static Dictionary _cache = null; + + public static SourceLinkInfo GetSourceLinkInfo(string pdbLocation) + { + if (_cache == null) + { + _cache = new Dictionary(); + } + + SourceLinkInfo remappings; + if (_cache.TryGetValue(pdbLocation, out remappings)) + { + return remappings; + } + else + { + try + { + remappings = PortablePdbSymbolReader.GetSourceLinkInfo(pdbLocation); + } + finally + { + _cache.Add(pdbLocation, remappings); + } + return remappings; + } + } + + /// + /// Finds URL for given path on a build machine. + /// + public static string TryGetFileUrl(string pdbLocation, string fileName) + { + if (fileName == null) return null; + + SourceLinkInfo sourceLinks = GetSourceLinkInfo(pdbLocation); + if (sourceLinks != null) + { + if (sourceLinks.documents.ContainsKey(fileName)) + { + return sourceLinks.documents[fileName]; + } + + if (sourceLinks.Patterns != null) + { + foreach (ValueTuple replacement in sourceLinks.Patterns) + { + if (fileName.StartsWith(replacement.Item1)) + { + string rest = fileName.Replace(replacement.Item1, ""); + string prefix = replacement.Item2; + // Replace Windows-style separators by Web/Linux style + if (prefix.Contains(@"/") && rest.Contains(@"\")) + { + rest = rest.Replace(@"\", @"/"); + } + return prefix + rest; + } + } + } + } + return null; + } + } + + /// + /// Caches sources of source files per location of PDB file indexed by source file path + /// + public static class PortablePDBEmbeddedFilesCache + { + public const string lineMarkPrefix = ">>>"; + public const int lineNumberPaddingWidth = 6; + + /// + /// Key is the location of a PortablePDB file on a current machine. + /// Value is the dictionary returned by + /// called with a given Key. + /// + // ThreadStaticAttribute makes sure that the cache is thread safe + [ThreadStatic] + private static Dictionary> _cache = null; + + /// + /// Returns cached result of calling + /// + public static Dictionary GetEmbeddedFiles(string pdbLocation) + { + if (_cache == null) + { + _cache = new Dictionary>(); + } + + Dictionary embeddedFilesFromPath = null; + if (_cache.TryGetValue(pdbLocation, out embeddedFilesFromPath)) + { + return embeddedFilesFromPath; + } + else + { + try + { + embeddedFilesFromPath = PortablePdbSymbolReader.GetEmbeddedFiles(pdbLocation); + } + finally + { + _cache.Add(pdbLocation, embeddedFilesFromPath); + } + return embeddedFilesFromPath; + } + } + + public static string GetEmbeddedFileRange(string pdbLocation, string fullName, int lineStart, int lineEnd, bool showLineNumbers = false, int markedLine = -1, string markPrefix = lineMarkPrefix) + { + Dictionary fileNameToFileSourceText = GetEmbeddedFiles(pdbLocation); + if (fileNameToFileSourceText == null) return null; + CompressedSourceFile source = fileNameToFileSourceText.GetValueOrDefault(fullName); + if (source == null) return null; + + StringBuilder builder = new StringBuilder(); + using (StringReader reader = new StringReader(source.ToString())) + { + int lineNumber = 0; + + // first go through text source till we reach lineStart + while (reader.Peek() != -1) + { + lineNumber++; + if (lineNumber == lineStart) + break; + reader.ReadLine(); + } + + while (reader.Peek() != -1) + { + string currentLine = reader.ReadLine(); + if (showLineNumbers) + { + builder.Append($"{lineNumber} ".PadLeft(lineNumberPaddingWidth)); + } + if (lineNumber == markedLine) + { + builder.Append(markPrefix); + } + builder.AppendLine(currentLine); + + lineNumber++; + if (lineNumber == lineEnd) + break; + } + } + return builder.ToString(); + } + } +} diff --git a/src/Simulation/Common/SimulatorBase.cs b/src/Simulation/Common/SimulatorBase.cs index 3bc17a26e4e..66999f33bac 100644 --- a/src/Simulation/Common/SimulatorBase.cs +++ b/src/Simulation/Common/SimulatorBase.cs @@ -32,6 +32,18 @@ public abstract class SimulatorBase : AbstractFactory, IOperat public abstract string Name { get; } + + /// + /// If the execution finishes in failure, this method returns the call-stack of the Q# operations + /// executed up to the point when the failure happened. + /// If the execution was successful, this method returns null. + /// + /// + /// Only Q# operations are tracked and reported in the stack trace. Q# functions or calls from + /// classical hosts like C# or Python are not included. + /// + public StackFrame[] CallStack { get; private set; } + public SimulatorBase(IQubitManager qubitManager = null) { this.QubitManager = qubitManager; @@ -98,14 +110,41 @@ public override AbstractCallable CreateInstance(Type t) return result; } + public virtual O Execute(I args) where T : AbstractCallable, ICallable + { + StackTraceCollector stackTraceCollector = new StackTraceCollector(this); + var op = Get(); + + try + { + var result = op.Apply(args); + this.CallStack = null; + return result; + } + catch (Exception e) // Dumps q# call-stack in case of exception if CallStack tracking was enabled + { + this.CallStack = stackTraceCollector.CallStack; + OnLog?.Invoke($"Unhandled exception. {e.GetType().FullName}: {e.Message}"); + bool first = true; + foreach (StackFrame sf in this.CallStack) + { + var msg = (first ? " ---> " : " at ") + sf.ToStringWithBestSourceLocation(); + OnLog?.Invoke(msg); + first = false; + } + OnLog?.Invoke(""); + + throw; + } + finally + { + stackTraceCollector.Dispose(); + } + } public virtual Task Run(I args) where T : AbstractCallable, ICallable { - return Task.Run(() => - { - var op = Get(); - return op.Apply(args); - }); + return Task.Run(() => Execute(args)); } /// diff --git a/src/Simulation/Common/StackTrace.cs b/src/Simulation/Common/StackTrace.cs new file mode 100644 index 00000000000..2251de7d915 --- /dev/null +++ b/src/Simulation/Common/StackTrace.cs @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Quantum.Simulation.Core; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.Quantum.Simulation.Common +{ + /// + /// Stores information about Q# stack frames. During successful execution keeps track only of Callable and Argument. + /// When the exception happens, the rest of the information is populated by + /// method. + /// + [Serializable] + public class StackFrame + { + /// + /// Callable corresponding to the stack frame + /// + public ICallable Callable { get; private set; } + + /// + /// Arguments passed to the callable in the stack frame + /// + public IApplyData Argument { get; private set; } + + /// + /// The path to the source where operation is defined + /// + public string SourceFile { get; private set; } + + /// + /// One based line number in the operation that resulted in failure. Note that for automatically derived Adjoint and Controlled + /// variants of the operation, the line always points to the operation declaration + /// + public int FailedLineNumber { get; private set; } + + /// + /// One based line number where the declaration starts. + /// + public int DeclarationStartLineNumber { get; private set; } + + /// + /// One based line number of the first line after the declaration. + /// The value -1, if the declaration ends on the last line of the file. + /// + public int DeclarationEndLineNumber { get; private set; } + + public StackFrame(ICallable callable, IApplyData argument) + { + Callable = callable; + Argument = argument; + } + + /// + /// Uses PortablePDBs and SourceLink to get the source of failed operation. + /// + public string GetOperationSourceFromPDB() + { + string pdbFileLocation = PortablePdbSymbolReader.GetPDBLocation(Callable); + return PortablePDBEmbeddedFilesCache.GetEmbeddedFileRange( + pdbFileLocation, + SourceFile, + DeclarationStartLineNumber, + DeclarationEndLineNumber, + showLineNumbers: true, markedLine: FailedLineNumber); + } + + /// + /// Uses PortablePDBs and SourceLink to get URL for file and line number. + /// + /// + public string GetURLFromPDB() + { + string pdbFileLocation = PortablePdbSymbolReader.GetPDBLocation(Callable); + string result = PortablePDBSourceLinkInfoCache.TryGetFileUrl(pdbFileLocation, SourceFile); + return PortablePdbSymbolReader.TryFormatGitHubUrl(result, FailedLineNumber); + } + + private const string messageFormat = "{0} on {1}"; + + public override string ToString() + { + return string.Format(messageFormat, Callable.FullName, $"{SourceFile}:line {FailedLineNumber}"); + } + + /// + /// The same as , but tries to point to best source location. + /// If the source is not available on local machine, source location will be replaced + /// by URL pointing to GitHub repository. + /// This is more costly than because it checks if source file exists on disk. + /// If the file does not exist it calls to get the URL + /// which is also more costly than . + /// + public virtual string ToStringWithBestSourceLocation() + { + string message = ToString(); + if (System.IO.File.Exists(SourceFile)) + { + return message; + } + else + { + string url = GetURLFromPDB(); + if (url == null) + { + return message; + } + else + { + return string.Format(messageFormat, Callable.FullName, url); + } + } + } + + /// + /// Finds correspondence between Q# and C# stack frames and populates Q# stack frame information from C# stack frames + /// + public static StackFrame[] PopulateSourceLocations(Stack qsharpCallStack, System.Diagnostics.StackFrame[] csharpCallStack) + { + foreach (StackFrame currentFrame in qsharpCallStack) + { + ICallable op = currentFrame.Callable.UnwrapCallable(); + object[] locations = op.GetType().GetCustomAttributes(typeof(SourceLocationAttribute), true); + foreach (object location in locations) + { + SourceLocationAttribute sourceLocation = (location as SourceLocationAttribute); + if (sourceLocation != null && sourceLocation.SpecializationKind == op.Variant) + { + currentFrame.SourceFile = System.IO.Path.GetFullPath(sourceLocation.SourceFile); + currentFrame.DeclarationStartLineNumber = sourceLocation.StartLine; + currentFrame.DeclarationEndLineNumber = sourceLocation.EndLine; + } + } + } + + StackFrame[] qsharpStackFrames = qsharpCallStack.ToArray(); + int qsharpStackFrameId = 0; + for (int csharpStackFrameId = 0; csharpStackFrameId < csharpCallStack.Length; ++csharpStackFrameId) + { + string fileName = csharpCallStack[csharpStackFrameId].GetFileName(); + if (fileName != null) + { + fileName = System.IO.Path.GetFullPath(fileName); + int failedLineNumber = csharpCallStack[csharpStackFrameId].GetFileLineNumber(); + StackFrame currentQsharpStackFrame = qsharpStackFrames[qsharpStackFrameId]; + if (fileName == currentQsharpStackFrame.SourceFile && + currentQsharpStackFrame.DeclarationStartLineNumber <= failedLineNumber && + ( + (failedLineNumber < currentQsharpStackFrame.DeclarationEndLineNumber) || + (currentQsharpStackFrame.DeclarationEndLineNumber == -1) + ) + ) + { + currentQsharpStackFrame.FailedLineNumber = failedLineNumber; + qsharpStackFrameId++; + if (qsharpStackFrameId == qsharpStackFrames.Length) break; + } + } + } + return qsharpStackFrames; + } + } + + /// + /// Tracks Q# operations call-stack till the first failure resulting in + /// event invocation. + /// + /// + /// Only Q# operations are tracked and reported in the stack trace. Q# functions or calls from + /// classical hosts like C# or Python are not included. + /// + public class StackTraceCollector : IDisposable + { + private readonly Stack callStack; + private readonly SimulatorBase sim; + private System.Diagnostics.StackFrame[] frames = null; + StackFrame[] stackFramesWithLocations = null; + bool hasFailed = false; + + public StackTraceCollector(SimulatorBase sim) + { + callStack = new Stack(); + this.sim = sim; + + this.Start(); + } + + private void Start() + { + sim.OnOperationStart += this.OnOperationStart; + sim.OnOperationEnd += this.OnOperationEnd; + sim.OnFail += this.OnFail; + } + + private void Stop() + { + sim.OnOperationStart -= this.OnOperationStart; + sim.OnOperationEnd -= this.OnOperationEnd; + sim.OnFail -= this.OnFail; + } + + void OnOperationStart(ICallable callable, IApplyData arg) + { + if (!hasFailed) + { + callStack.Push(new StackFrame(callable, arg)); + } + } + + void OnOperationEnd(ICallable callable, IApplyData arg) + { + if (!hasFailed) + { + callStack.Pop(); + } + } + + void OnFail(System.Runtime.ExceptionServices.ExceptionDispatchInfo exceptionInfo) + { + if (!hasFailed) + { + hasFailed = true; + } + + System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace(exceptionInfo.SourceException, 0, true); + System.Diagnostics.StackFrame[] currentFrames = stackTrace.GetFrames(); + + if (frames == null) + { + frames = currentFrames; + } + else + { + // When the exception is thrown, OnFail can be called multiple times. + // With every next call we see bigger part of the call stack, so we save the biggest call stack + if (currentFrames.Length > frames.Length) + { + Debug.Assert((frames.Length == 0) || (frames[0].ToString() == currentFrames[0].ToString())); + frames = currentFrames; + } + } + } + + + + /// + /// If failure has happened returns the call-stack at time of failure. + /// Returns null if the failure has not happened. + /// + public StackFrame[] CallStack + { + get + { + if (hasFailed) + { + if( stackFramesWithLocations == null ) + { + stackFramesWithLocations = StackFrame.PopulateSourceLocations(callStack, frames); + } + return stackFramesWithLocations; + } + else + { + return null; + } + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.Stop(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/src/Simulation/Core/Generics/Adjoint.cs b/src/Simulation/Core/Generics/Adjoint.cs index b00116bb7c8..45577250ebd 100644 --- a/src/Simulation/Core/Generics/Adjoint.cs +++ b/src/Simulation/Core/Generics/Adjoint.cs @@ -23,7 +23,7 @@ public interface IAdjointable : ICallable /// input Type is not resolved until it gets Applied at runtime. /// [DebuggerTypeProxy(typeof(GenericAdjoint.DebuggerProxy))] - public class GenericAdjoint : GenericCallable, IApplyData + public class GenericAdjoint : GenericCallable, IApplyData, IOperationWrapper { public GenericAdjoint(GenericCallable baseOp) : base(baseOp.Factory, null) { @@ -31,6 +31,7 @@ public GenericAdjoint(GenericCallable baseOp) : base(baseOp.Factory, null) } public GenericCallable BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; IEnumerable IApplyData.Qubits => ((IApplyData)this.BaseOp)?.Qubits; diff --git a/src/Simulation/Core/Generics/Controlled.cs b/src/Simulation/Core/Generics/Controlled.cs index 65d5235c0bb..cc9b05fb994 100644 --- a/src/Simulation/Core/Generics/Controlled.cs +++ b/src/Simulation/Core/Generics/Controlled.cs @@ -25,7 +25,7 @@ public partial interface IControllable : ICallable /// input Type is not resolved until it gets Applied at runtime. /// [DebuggerTypeProxy(typeof(GenericControlled.DebuggerProxy))] - public class GenericControlled : GenericCallable, IApplyData + public class GenericControlled : GenericCallable, IApplyData, IOperationWrapper { public GenericControlled(GenericCallable baseOp) : base(baseOp.Factory, null) { @@ -33,6 +33,7 @@ public GenericControlled(GenericCallable baseOp) : base(baseOp.Factory, null) } public GenericCallable BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; IEnumerable IApplyData.Qubits => ((IApplyData)this.BaseOp)?.Qubits; diff --git a/src/Simulation/Core/Generics/GenericPartial.cs b/src/Simulation/Core/Generics/GenericPartial.cs index 295502d7c8f..8db1263639e 100644 --- a/src/Simulation/Core/Generics/GenericPartial.cs +++ b/src/Simulation/Core/Generics/GenericPartial.cs @@ -14,7 +14,7 @@ namespace Microsoft.Quantum.Simulation.Core /// input Type is not resolved until it gets Applied at runtime. /// [DebuggerTypeProxy(typeof(GenericPartial.DebuggerProxy))] - public class GenericPartial : GenericCallable, IApplyData + public class GenericPartial : GenericCallable, IApplyData, IOperationWrapper { private Lazy __qubits = null; @@ -29,6 +29,7 @@ public GenericPartial(GenericCallable baseOp, object partialValues) : base(baseO } public GenericCallable BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; public override string Name => this.BaseOp.Name; public override string FullName => this.BaseOp.FullName; diff --git a/src/Simulation/Core/Operations/Adjoint.cs b/src/Simulation/Core/Operations/Adjoint.cs index 428858f4c66..6db9f4a9f81 100644 --- a/src/Simulation/Core/Operations/Adjoint.cs +++ b/src/Simulation/Core/Operations/Adjoint.cs @@ -39,7 +39,7 @@ public Adjointable(IOperationFactory m) : base(m) /// Class used to represents an operation that has been adjointed. /// [DebuggerTypeProxy(typeof(AdjointedOperation<,>.DebuggerProxy))] - public class AdjointedOperation : Unitary, IApplyData, ICallable + public class AdjointedOperation : Unitary, IApplyData, ICallable, IOperationWrapper { public AdjointedOperation(Operation op) : base(op.Factory) { @@ -50,6 +50,7 @@ public AdjointedOperation(Operation op) : base(op.Factory) } public Operation BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; public override void Init() { } diff --git a/src/Simulation/Core/Operations/Controlled.cs b/src/Simulation/Core/Operations/Controlled.cs index 2c9f1e183cc..8df3a8eb579 100644 --- a/src/Simulation/Core/Operations/Controlled.cs +++ b/src/Simulation/Core/Operations/Controlled.cs @@ -38,7 +38,7 @@ public Controllable(IOperationFactory m) : base(m) { } /// This class is used to represents an operation that has been controlled. /// [DebuggerTypeProxy(typeof(ControlledOperation<,>.DebuggerProxy))] - public class ControlledOperation : Unitary<(IQArray, I)>, IApplyData, ICallable + public class ControlledOperation : Unitary<(IQArray, I)>, IApplyData, ICallable, IOperationWrapper { public class In : IApplyData { @@ -66,6 +66,7 @@ public ControlledOperation(Operation op) : base(op.Factory) } public Operation BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; public override void Init() { } diff --git a/src/Simulation/Core/Operations/Operation.cs b/src/Simulation/Core/Operations/Operation.cs index 7963654f389..e81248f2528 100644 --- a/src/Simulation/Core/Operations/Operation.cs +++ b/src/Simulation/Core/Operations/Operation.cs @@ -17,7 +17,15 @@ public partial interface ICallable : ICallable ICallable Partial

(Func mapper); } - + ///

+ /// An operation that wraps another operation, for example + /// , , + /// , + /// + public interface IOperationWrapper + { + ICallable BaseOperation { get; } + } /// /// The base class for all ClosedType quantum operations. diff --git a/src/Simulation/Core/Operations/OperationPartial.cs b/src/Simulation/Core/Operations/OperationPartial.cs index 4b059962132..12e03f01913 100644 --- a/src/Simulation/Core/Operations/OperationPartial.cs +++ b/src/Simulation/Core/Operations/OperationPartial.cs @@ -17,7 +17,7 @@ namespace Microsoft.Quantum.Simulation.Core /// Optionally it can receive a Mapper to do the same. /// [DebuggerTypeProxy(typeof(OperationPartial<,,>.DebuggerProxy))] - public class OperationPartial : Operation, IUnitary

+ public class OperationPartial : Operation, IUnitary

, IOperationWrapper { private Lazy __qubits = null; @@ -59,6 +59,7 @@ public OperationPartial(Operation op, object partialTuple) : base(op.Facto public override void Init() { } public Operation BaseOp { get; } + ICallable IOperationWrapper.BaseOperation => BaseOp; public Func Mapper { get; } @@ -137,6 +138,7 @@ ICallable ICallable.Partial(Func mapper) IUnitary

IUnitary

.Adjoint => base.Adjoint; IUnitary<(IQArray, P)> IUnitary

.Controlled => base.Controlled; + IUnitary IUnitary

.Partial(Func mapper) => new OperationPartial(this, mapper); public override string ToString() => $"{this.BaseOp}{{_}}"; diff --git a/src/Simulation/Core/TypeExtensions.cs b/src/Simulation/Core/TypeExtensions.cs index 9822b37f373..94bc5ebabf6 100644 --- a/src/Simulation/Core/TypeExtensions.cs +++ b/src/Simulation/Core/TypeExtensions.cs @@ -368,5 +368,15 @@ public static string QSharpType(this Type t) return t.Name; } + + public static ICallable UnwrapCallable(this ICallable op) + { + ICallable res = op; + while (res as IOperationWrapper != null) + { + res = (res as IOperationWrapper).BaseOperation; + } + return res; + } } } diff --git a/src/Simulation/Simulators.Tests/Circuits/Fail.qs b/src/Simulation/Simulators.Tests/Circuits/Fail.qs index 3fb9302255c..ce8e75ca30c 100644 --- a/src/Simulation/Simulators.Tests/Circuits/Fail.qs +++ b/src/Simulation/Simulators.Tests/Circuits/Fail.qs @@ -3,10 +3,88 @@ namespace Microsoft.Quantum.Simulation.Simulators.Tests.Circuits { - operation AlwaysFail() : Unit { - fail "Always fail"; + operation AlwaysFail() : Unit is Adj + Ctl{ + Fail(); } -} + operation AlwaysFail1() : Unit is Adj + Ctl{ + AlwaysFail(); + } + operation AlwaysFail2() : Unit is Adj + Ctl { + Controlled AlwaysFail1(new Qubit[0],()); + } + operation AlwaysFail3() : Unit is Adj + Ctl { + Adjoint AlwaysFail2(); + } + operation AlwaysFail4() : Unit is Adj + Ctl { + Adjoint AlwaysFail3(); + } + operation GenericFail<'T,'U>( a : 'T, b : 'U ) : Unit is Adj + Ctl { + AlwaysFail(); + } + + operation GenericFail1() : Unit is Adj + Ctl { + GenericFail(5,6); + } + + operation PartialFail( a : Int, b : Int ) : Unit is Adj + Ctl { + AlwaysFail(); + } + + operation PartialFail1() : Unit is Adj + Ctl { + let op = PartialFail(0,_); + op(2); + } + + operation PartialAdjFail1() : Unit is Adj + Ctl { + let op = PartialFail(0,_); + Adjoint op(2); + } + + operation PartialCtlFail1() : Unit is Adj + Ctl { + let op = PartialFail(0,_); + Controlled op(new Qubit[0], 2); + } + + operation GenericAdjFail1() : Unit is Adj + Ctl { + Adjoint GenericFail(5,6); + } + + operation GenericCtlFail1() : Unit is Adj + Ctl { + Controlled GenericFail( new Qubit[0], (5,6)); + } + + function Fail() : Unit { + fail "Always fail"; + } + + operation RecursionFail( a : Int) : Unit is Adj { + if ( a >= 1 ) + { + RecursionFail(a-1); + } + else + { + Fail(); + } + } + + operation RecursionFail1() : Unit { + RecursionFail(2); + } + + operation DivideBy0() : Int { + let z = 0; + return 3 / z; + } + + operation AllGood() : Unit { + Microsoft.Quantum.Intrinsic.Message("All good!"); + } + + operation AllGood1() : Unit { + AllGood(); + } +} \ No newline at end of file diff --git a/src/Simulation/Simulators.Tests/StackTraceTests.cs b/src/Simulation/Simulators.Tests/StackTraceTests.cs new file mode 100644 index 00000000000..e7d28b2d49b --- /dev/null +++ b/src/Simulation/Simulators.Tests/StackTraceTests.cs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; + +using System; +using System.Threading.Tasks; + +using Microsoft.Quantum.Simulation.Core; +using Microsoft.Quantum.Simulation.Common; +using Microsoft.Quantum.Simulation.Simulators.Tests.Circuits; +using Microsoft.Quantum.Simulation.Simulators.Exceptions; +using Xunit.Abstractions; +using System.Text; +using System.Collections.Generic; + +namespace Microsoft.Quantum.Simulation.Simulators.Tests +{ + public class StackTraceTests + { + const string namespacePrefix = "Microsoft.Quantum.Simulation.Simulators.Tests.Circuits."; + + private readonly ITestOutputHelper output; + public StackTraceTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void AlwaysFail4Test() + { + using (var sim = new QuantumSimulator()) + { + try + { + QVoid res = AlwaysFail4.Run(sim).Result; + } + catch (AggregateException ex) + { + Assert.True(ex.InnerException is ExecutionFailException); + + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(5, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "AlwaysFail1", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "AlwaysFail2", stackFrames[2].Callable.FullName); + Assert.Equal(namespacePrefix + "AlwaysFail3", stackFrames[3].Callable.FullName); + Assert.Equal(namespacePrefix + "AlwaysFail4", stackFrames[4].Callable.FullName); + + Assert.Equal(OperationFunctor.Controlled, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Controlled, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + Assert.Equal(OperationFunctor.Adjoint, stackFrames[3].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[4].Callable.Variant); + + Assert.Equal(14, stackFrames[2].FailedLineNumber); + Assert.Equal(21, stackFrames[4].FailedLineNumber); + + // For Adjoint and Controlled we expect failedLineNumber to be equal to declarationStartLineNumber + Assert.Equal(stackFrames[0].DeclarationStartLineNumber, stackFrames[0].FailedLineNumber); + Assert.Equal(stackFrames[1].DeclarationStartLineNumber, stackFrames[1].FailedLineNumber); + Assert.Equal(stackFrames[3].DeclarationStartLineNumber, stackFrames[3].FailedLineNumber); + + for (int i = 0; i < stackFrames.Length; ++i) + { + Assert.StartsWith(@"https://github.com/", stackFrames[i].GetURLFromPDB()); + Assert.EndsWith($"#L{stackFrames[i].FailedLineNumber}", stackFrames[i].GetURLFromPDB()); + } + + StringBuilder builder = new StringBuilder(); + builder.Append("13 ".PadLeft(PortablePDBEmbeddedFilesCache.lineNumberPaddingWidth)); + builder.AppendLine(" operation AlwaysFail2() : Unit is Adj + Ctl {"); + builder.Append("14 ".PadLeft(PortablePDBEmbeddedFilesCache.lineNumberPaddingWidth) + PortablePDBEmbeddedFilesCache.lineMarkPrefix); + builder.AppendLine(" Controlled AlwaysFail1(new Qubit[0],());"); + builder.Append("15 ".PadLeft(PortablePDBEmbeddedFilesCache.lineNumberPaddingWidth)); + builder.AppendLine(" }"); + Assert.Equal(builder.ToString(), stackFrames[2].GetOperationSourceFromPDB()); + + for (int i = 0; i < stackFrames.Length; ++i) + { + output.WriteLine($"operation:{stackFrames[i].Callable.FullName}"); + output.WriteLine(stackFrames[i].GetOperationSourceFromPDB()); + } + } + } + } + + [Fact] + public void GenericFail1Test() + { + ResourcesEstimator sim = new ResourcesEstimator(); + + { + try + { + QVoid res = sim.Execute(QVoid.Instance); + } + catch (ExecutionFailException) + { + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Body, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(7, stackFrames[0].FailedLineNumber); + Assert.Equal(25, stackFrames[1].FailedLineNumber); + Assert.Equal(29, stackFrames[2].FailedLineNumber); + } + } + + { + try + { + QVoid res = sim.Execute(QVoid.Instance); + } + catch (ExecutionFailException) + { + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericAdjFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Adjoint, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Adjoint, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(6, stackFrames[0].FailedLineNumber); + Assert.Equal(24, stackFrames[1].FailedLineNumber); + Assert.Equal(52, stackFrames[2].FailedLineNumber); + } + } + + { + try + { + QVoid res = sim.Execute(QVoid.Instance); + } + catch (ExecutionFailException) + { + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "GenericCtlFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Controlled, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Controlled, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(6, stackFrames[0].FailedLineNumber); + Assert.Equal(24, stackFrames[1].FailedLineNumber); + Assert.Equal(56, stackFrames[2].FailedLineNumber); + } + } + } + + [Fact] + public void PartialFail1Test() + { + ToffoliSimulator sim = new ToffoliSimulator(); + + { + try + { + QVoid res = PartialFail1.Run(sim).Result; + } + catch (AggregateException ex) + { + Assert.True(ex.InnerException is ExecutionFailException); + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Body, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(7, stackFrames[0].FailedLineNumber); + Assert.Equal(33, stackFrames[1].FailedLineNumber); + Assert.Equal(38, stackFrames[2].FailedLineNumber); + } + } + + { + try + { + QVoid res = PartialAdjFail1.Run(sim).Result; + } + catch (AggregateException ex) + { + Assert.True(ex.InnerException is ExecutionFailException); + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialAdjFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Adjoint, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Adjoint, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(6, stackFrames[0].FailedLineNumber); + Assert.Equal(32, stackFrames[1].FailedLineNumber); + Assert.Equal(43, stackFrames[2].FailedLineNumber); + } + } + + { + try + { + QVoid res = PartialCtlFail1.Run(sim).Result; + } + catch (AggregateException ex) + { + Assert.True(ex.InnerException is ExecutionFailException); + StackFrame[] stackFrames = sim.CallStack; + + Assert.Equal(3, stackFrames.Length); + + Assert.Equal(namespacePrefix + "AlwaysFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "PartialCtlFail1", stackFrames[2].Callable.FullName); + + Assert.Equal(OperationFunctor.Controlled, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Controlled, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + + Assert.Equal(6, stackFrames[0].FailedLineNumber); + Assert.Equal(32, stackFrames[1].FailedLineNumber); + Assert.Equal(48, stackFrames[2].FailedLineNumber); + } + } + } + + [Fact] + public void RecursionFail1Test() + { + ToffoliSimulator sim = new ToffoliSimulator(); + + { + StackTraceCollector sc = new StackTraceCollector(sim); + ICallable op = sim.Get(); + try + { + QVoid res = op.Apply(QVoid.Instance); + } + catch (ExecutionFailException) + { + StackFrame[] stackFrames = sc.CallStack; + + Assert.Equal(4, stackFrames.Length); + + Assert.Equal(namespacePrefix + "RecursionFail", stackFrames[0].Callable.FullName); + Assert.Equal(namespacePrefix + "RecursionFail", stackFrames[1].Callable.FullName); + Assert.Equal(namespacePrefix + "RecursionFail", stackFrames[2].Callable.FullName); + Assert.Equal(namespacePrefix + "RecursionFail1", stackFrames[3].Callable.FullName); + + Assert.Equal(OperationFunctor.Body, stackFrames[0].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[1].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[2].Callable.Variant); + Assert.Equal(OperationFunctor.Body, stackFrames[3].Callable.Variant); + + Assert.Equal(70, stackFrames[0].FailedLineNumber); + Assert.Equal(66, stackFrames[1].FailedLineNumber); + Assert.Equal(66, stackFrames[2].FailedLineNumber); + Assert.Equal(75, stackFrames[3].FailedLineNumber); + } + } + } + + [Fact] + public void DivideByZeroTest() + { + ToffoliSimulator sim = new ToffoliSimulator(); + + try + { + sim.Execute(QVoid.Instance); + } + catch (Exception) + { + StackFrame[] stackFrames = sim.CallStack; + + Assert.Single(stackFrames); + Assert.Equal(namespacePrefix + "DivideBy0", stackFrames[0].Callable.FullName); + } + } + + [Fact] + public void AllGoodTest() + { + ToffoliSimulator sim = new ToffoliSimulator(); + + QVoid res = sim.Execute(QVoid.Instance); + StackFrame[] stackFrames = sim.CallStack; + Assert.Null(stackFrames); + } + + [Fact] + public void UrlMappingTest() + { + const string rawUrl = @"https://raw.githubusercontent.com/microsoft/qsharp-runtime/af6262c05522d645d0a0952272443e84eeab677a/src/Xunit/TestCaseDiscoverer.cs"; + const string expectedURL = @"https://github.com/microsoft/qsharp-runtime/blob/af6262c05522d645d0a0952272443e84eeab677a/src/Xunit/TestCaseDiscoverer.cs#L13"; + Assert.Equal(expectedURL, PortablePdbSymbolReader.TryFormatGitHubUrl(rawUrl, 13)); + } + + [Fact] + public void ErrorLogTest() + { + ToffoliSimulator sim = new ToffoliSimulator(); + + var logs = new List(); + sim.OnLog += (msg) => logs.Add(msg); + try + { + QVoid res = sim.Execute(QVoid.Instance); + } + catch (ExecutionFailException) + { + Assert.Equal(7, logs.Count); + Assert.StartsWith("Unhandled exception. Microsoft.Quantum.Simulation.Core.ExecutionFailException: Always fail", logs[0]); + Assert.StartsWith(" ---> Microsoft.Quantum.Simulation.Simulators.Tests.Circuits.AlwaysFail", logs[1]); + Assert.StartsWith(" at Microsoft.Quantum.Simulation.Simulators.Tests.Circuits.AlwaysFail1 on", logs[2]); + Assert.StartsWith(" at Microsoft.Quantum.Simulation.Simulators.Tests.Circuits.AlwaysFail2 on", logs[3]); + Assert.StartsWith(" at Microsoft.Quantum.Simulation.Simulators.Tests.Circuits.AlwaysFail3 on", logs[4]); + Assert.StartsWith(" at Microsoft.Quantum.Simulation.Simulators.Tests.Circuits.AlwaysFail4 on", logs[5]); + Assert.Equal("", logs[6]); + } + } + } +} \ No newline at end of file diff --git a/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj b/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj index 64d3e4bb24e..565987988c9 100644 --- a/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj +++ b/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj @@ -1,6 +1,7 @@  + netcoreapp3.0