diff --git a/.gitignore b/.gitignore
index 5b33b48524e..7127ca52075 100644
--- a/.gitignore
+++ b/.gitignore
@@ -339,3 +339,6 @@ ASALocalRun/
/src/Simulation/Simulators.Tests/TestProjects/QSharpExe/built
/src/Simulation/Simulators.Tests/TestProjects/TargetedExe/built
dbw_test
+
+# Controller test artifacts
+/src/Qir/Controller/test-artifacts/
\ No newline at end of file
diff --git a/Simulation.sln b/Simulation.sln
index f888733494b..7f2fab57378 100644
--- a/Simulation.sln
+++ b/Simulation.sln
@@ -117,6 +117,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controller", "Controller",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QirController", "src\Qir\Controller\QirController.csproj", "{A77E6661-D143-4E3E-BCD1-8E321A966829}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.QirController", "src\Qir\Controller\Tests.QirController\Tests.QirController.csproj", "{2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -769,6 +771,22 @@ Global
{A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
{A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
{A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|x64.Build.0 = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|x64.Build.0 = Debug|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|x64.ActiveCfg = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|x64.Build.0 = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -824,6 +842,7 @@ Global
{D7D34736-A719-4B45-A33F-2723F59EC29D} = {A7DB7367-9FD6-4164-8263-A05077BE54AB}
{4E07F247-ED93-4497-8B58-022314308E67} = {F6C2D4C0-12DC-40E3-9C86-FA5308D9B567}
{A77E6661-D143-4E3E-BCD1-8E321A966829} = {4E07F247-ED93-4497-8B58-022314308E67}
+ {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68} = {4E07F247-ED93-4497-8B58-022314308E67}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {929C0464-86D8-4F70-8835-0A5EAF930821}
diff --git a/src/Qir/Controller/Constant.cs b/src/Qir/Controller/Constant.cs
new file mode 100644
index 00000000000..23d5a8ba351
--- /dev/null
+++ b/src/Qir/Controller/Constant.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.Quantum.Qir
+{
+ public static class Constant
+ {
+ // TODO: errors will be added as dependencies are implemented.
+ public static class ErrorCode
+ {
+ public const string InternalError = "InternalError";
+ }
+ }
+}
diff --git a/src/Qir/Controller/Controller.cs b/src/Qir/Controller/Controller.cs
index 0f613d5fc0c..5f9cc2b65cc 100644
--- a/src/Qir/Controller/Controller.cs
+++ b/src/Qir/Controller/Controller.cs
@@ -1,26 +1,97 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System;
using System.IO;
-using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir.Driver;
+using Microsoft.Quantum.Qir.Executable;
+using Microsoft.Quantum.Qir.Model;
+using Microsoft.Quantum.Qir.Utility;
+using QirExecutionWrapperSerialization = Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper.Protocols;
namespace Microsoft.Quantum.Qir
{
- internal static class Controller
+ public static class Controller
{
- internal static void Execute(
- FileInfo input,
- FileInfo output,
- FileInfo error)
+ private const string SourceDirectoryPath = "src";
+ private const string BinaryDirectoryPath = "bin";
+ private const string ExecutableName = "simulation.exe";
+
+ public static async Task ExecuteAsync(
+ FileInfo inputFile,
+ FileInfo outputFile,
+ DirectoryInfo libraryDirectory,
+ DirectoryInfo includeDirectory,
+ FileInfo errorFile,
+ IQirDriverGenerator driverGenerator,
+ IQirExecutableGenerator executableGenerator,
+ IQuantumExecutableRunner executableRunner,
+ ILogger logger)
{
- var outputFileStream = output.Exists ? output.OpenWrite() : output.Create();
- outputFileStream.Write(new UTF8Encoding().GetBytes("output"));
- outputFileStream.Flush();
- outputFileStream.Close();
- var errorFileStream = error.Exists ? error.OpenWrite() : error.Create();
- errorFileStream.Write(new UTF8Encoding().GetBytes("error"));
- errorFileStream.Flush();
- errorFileStream.Close();
+ try
+ {
+ // Step 1: Parse input.
+ logger.LogInfo("Parsing input.");
+ using var inputFileStream = inputFile.OpenRead();
+ var input = QirExecutionWrapperSerialization.DeserializeFromFastBinary(inputFileStream);
+
+ // Step 32: Create driver.
+ logger.LogInfo("Creating driver file.");
+ var sourceDirectory = new DirectoryInfo(SourceDirectoryPath);
+ await driverGenerator.GenerateQirDriverCppAsync(sourceDirectory, input.EntryPoint, input.QirBytecode);
+
+ // Step 3: Create executable.
+ logger.LogInfo("Compiling and linking executable.");
+ var binaryDirectory = new DirectoryInfo(BinaryDirectoryPath);
+ var executableFile = new FileInfo(Path.Combine(BinaryDirectoryPath, ExecutableName));
+ await executableGenerator.GenerateExecutableAsync(executableFile, sourceDirectory, libraryDirectory, includeDirectory);
+
+ // Step 4: Run executable.
+ logger.LogInfo("Running executable.");
+ using var outputFileStream = outputFile.OpenWrite();
+ await executableRunner.RunExecutableAsync(executableFile, input.EntryPoint, outputFile);
+ }
+ catch (Exception e)
+ {
+ logger.LogError("An error has been encountered. Will write an error to the error file and delete any output that has been generated.");
+ logger.LogException(e);
+ await WriteExceptionToFileAsync(e, errorFile);
+ }
+ }
+
+ private static async Task WriteExceptionToFileAsync(Exception e, FileInfo errorFile)
+ {
+ // Create the error object.
+ Error error;
+ if (e is ControllerException controllerException)
+ {
+ error = new Error
+ {
+ Code = controllerException.Code,
+ Message = controllerException.Message,
+ };
+ }
+ else
+ {
+ error = new Error
+ {
+ Code = Constant.ErrorCode.InternalError,
+ Message = ErrorMessages.InternalError,
+ };
+ }
+
+ // Serialize the error to JSON.
+ var errorJson = JsonSerializer.Serialize(error, new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ });
+
+ // Write the error to the error file.
+ using var errorFileStream = errorFile.OpenWrite();
+ using var streamWriter = new StreamWriter(errorFileStream);
+ await streamWriter.WriteAsync(errorJson);
}
}
}
diff --git a/src/Qir/Controller/ControllerException.cs b/src/Qir/Controller/ControllerException.cs
new file mode 100644
index 00000000000..05cbd872cf9
--- /dev/null
+++ b/src/Qir/Controller/ControllerException.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Quantum.Qir
+{
+ ///
+ /// Exception that represents an error that can be written to an error file.
+ ///
+ public class ControllerException : Exception
+ {
+ public ControllerException(string message, string code)
+ : base(message)
+ {
+ Code = code;
+ }
+
+ public string Code { get; }
+ }
+}
diff --git a/src/Qir/Controller/Driver/IQirDriverGenerator.cs b/src/Qir/Controller/Driver/IQirDriverGenerator.cs
new file mode 100644
index 00000000000..e4dc2a6172c
--- /dev/null
+++ b/src/Qir/Controller/Driver/IQirDriverGenerator.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint;
+
+namespace Microsoft.Quantum.Qir.Driver
+{
+ public interface IQirDriverGenerator
+ {
+ ///
+ /// Generates the C++ driver source file and writes the bytecode to a file.
+ ///
+ /// Directory to which driver and bytecode will be written.
+ /// Entry point information.
+ /// The QIR bytecode.
+ ///
+ Task GenerateQirDriverCppAsync(DirectoryInfo sourceDirectory, EntryPointOperation entryPointOperation, ArraySegment bytecode);
+ }
+}
diff --git a/src/Qir/Controller/Driver/QirDriverGenerator.cs b/src/Qir/Controller/Driver/QirDriverGenerator.cs
new file mode 100644
index 00000000000..e169ea12be4
--- /dev/null
+++ b/src/Qir/Controller/Driver/QirDriverGenerator.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir.Utility;
+using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint;
+
+namespace Microsoft.Quantum.Qir.Driver
+{
+ public class QirDriverGenerator : IQirDriverGenerator
+ {
+ private readonly ILogger logger;
+
+ public QirDriverGenerator(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ public Task GenerateQirDriverCppAsync(DirectoryInfo sourceDirectory, EntryPointOperation entryPointOperation, ArraySegment bytecode)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Qir/Controller/ErrorMessages.Designer.cs b/src/Qir/Controller/ErrorMessages.Designer.cs
new file mode 100644
index 00000000000..dbd8ce8ba5e
--- /dev/null
+++ b/src/Qir/Controller/ErrorMessages.Designer.cs
@@ -0,0 +1,72 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Quantum.Qir {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class ErrorMessages {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ErrorMessages() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Quantum.Qir.ErrorMessages", typeof(ErrorMessages).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to An internal error occurred..
+ ///
+ public static string InternalError {
+ get {
+ return ResourceManager.GetString("InternalError", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Qir/Controller/ErrorMessages.resx b/src/Qir/Controller/ErrorMessages.resx
new file mode 100644
index 00000000000..6cfb82bccc5
--- /dev/null
+++ b/src/Qir/Controller/ErrorMessages.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ An internal error occurred.
+
+
\ No newline at end of file
diff --git a/src/Qir/Controller/Executable/ClangClient.cs b/src/Qir/Controller/Executable/ClangClient.cs
new file mode 100644
index 00000000000..72fc903d4e8
--- /dev/null
+++ b/src/Qir/Controller/Executable/ClangClient.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine.Invocation;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir.Utility;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ public class ClangClient : IClangClient
+ {
+ private const string LinkFlag = " -l ";
+ private readonly ILogger logger;
+
+ public ClangClient(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ public async Task CreateExecutableAsync(string[] inputFiles, string[] libraries, string libraryPath, string includePath, string outputPath)
+ {
+ var inputsArg = string.Join(' ', inputFiles);
+
+ // string.Join does not automatically prepend the delimiter, so it is included again in the string here.
+ var librariesArg = $"{LinkFlag} {string.Join(LinkFlag, libraries)}";
+ var arguments = $"{inputsArg} -I {includePath} -L {libraryPath} {librariesArg} -o {outputPath}";
+ logger.LogInfo($"Invoking clang with the following arguments: {arguments}");
+ var result = await Process.ExecuteAsync(
+ "clang",
+ arguments,
+ stdOut: s => { logger.LogInfo("clang: " + s); },
+ stdErr: s => { logger.LogError("clang: " + s); });
+ }
+ }
+}
diff --git a/src/Qir/Controller/Executable/IClangClient.cs b/src/Qir/Controller/Executable/IClangClient.cs
new file mode 100644
index 00000000000..7e7a8e1c95d
--- /dev/null
+++ b/src/Qir/Controller/Executable/IClangClient.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ ///
+ /// Wraps the 'clang' tool used for compilation.
+ ///
+ public interface IClangClient
+ {
+ Task CreateExecutableAsync(string[] inputFiles, string[] libraries, string libraryPath, string includePath, string outputPath);
+ }
+}
diff --git a/src/Qir/Controller/Executable/IQirExecutableGenerator.cs b/src/Qir/Controller/Executable/IQirExecutableGenerator.cs
new file mode 100644
index 00000000000..e8ce44f7b7a
--- /dev/null
+++ b/src/Qir/Controller/Executable/IQirExecutableGenerator.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ public interface IQirExecutableGenerator
+ {
+ ///
+ /// Generates a quantum simulation program executable.
+ ///
+ /// File path to create the executable at. Dependencies will be copied to its directory.
+ /// Location of the source files.
+ /// Location of the libraries that must be linked.
+ /// Location of the headers that must be included.
+ ///
+ public Task GenerateExecutableAsync(FileInfo executableFile, DirectoryInfo sourceDirectory, DirectoryInfo libraryDirectory, DirectoryInfo includeDirectory);
+ }
+}
diff --git a/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs b/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs
new file mode 100644
index 00000000000..135fbbbfb4f
--- /dev/null
+++ b/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ public interface IQuantumExecutableRunner
+ {
+ ///
+ /// Runs a quantum program executable with the given arguments.
+ ///
+ /// Location of the executable to run.
+ /// Entry point and arguments to pass.
+ /// Location to write program output.
+ ///
+ Task RunExecutableAsync(FileInfo executableFile, EntryPointOperation entryPointOperation, FileInfo outputFile);
+ }
+}
diff --git a/src/Qir/Controller/Executable/QirExecutableGenerator.cs b/src/Qir/Controller/Executable/QirExecutableGenerator.cs
new file mode 100644
index 00000000000..c09a7a0fa67
--- /dev/null
+++ b/src/Qir/Controller/Executable/QirExecutableGenerator.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir.Utility;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ public class QirExecutableGenerator : IQirExecutableGenerator
+ {
+ private readonly IClangClient clangClient;
+ private readonly ILogger logger;
+
+ public QirExecutableGenerator(IClangClient clangClient, ILogger logger)
+ {
+ this.clangClient = clangClient;
+ this.logger = logger;
+ }
+
+ public Task GenerateExecutableAsync(FileInfo executableFile, DirectoryInfo sourceDirectory, DirectoryInfo libraryDirectory, DirectoryInfo includeDirectory)
+ {
+ // TODO: Compile and link libraries- "Microsoft.Quantum.Qir.Runtime", "Microsoft.Quantum.Qir.QSharp.Foundation", "Microsoft.Quantum.Qir.QSharp.Core"
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Qir/Controller/Executable/QuantumExecutableRunner.cs b/src/Qir/Controller/Executable/QuantumExecutableRunner.cs
new file mode 100644
index 00000000000..774aa0cf517
--- /dev/null
+++ b/src/Qir/Controller/Executable/QuantumExecutableRunner.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir.Utility;
+using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint;
+
+namespace Microsoft.Quantum.Qir.Executable
+{
+ public class QuantumExecutableRunner : IQuantumExecutableRunner
+ {
+ private readonly ILogger logger;
+
+ public QuantumExecutableRunner(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ public Task RunExecutableAsync(FileInfo executableFile, EntryPointOperation entryPointOperation, FileInfo outputFile)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/src/Qir/Controller/Model/Error.cs b/src/Qir/Controller/Model/Error.cs
new file mode 100644
index 00000000000..ee96bff5348
--- /dev/null
+++ b/src/Qir/Controller/Model/Error.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Quantum.Qir.Model
+{
+ public class Error
+ {
+ [JsonPropertyName("code")]
+ public string Code { get; set; }
+
+ [JsonPropertyName("message")]
+ public string Message { get; set; }
+ }
+}
diff --git a/src/Qir/Controller/Program.cs b/src/Qir/Controller/Program.cs
index e7fec1e6fd0..88677d0d1ce 100644
--- a/src/Qir/Controller/Program.cs
+++ b/src/Qir/Controller/Program.cs
@@ -4,6 +4,9 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
+using Microsoft.Quantum.Qir.Driver;
+using Microsoft.Quantum.Qir.Executable;
+using Microsoft.Quantum.Qir.Utility;
namespace Microsoft.Quantum.Qir
{
@@ -11,6 +14,12 @@ class Program
{
static void Main(string[] args)
{
+ var logger = new Logger(new Clock());
+ var execGenerator = new QirExecutableGenerator(new ClangClient(logger), logger);
+ var driverGenerator = new QirDriverGenerator(logger);
+ var execRunner = new QuantumExecutableRunner(logger);
+ logger.LogInfo("QIR controller beginning.");
+
var rootCommand = new RootCommand(
description: "Builds and runs QIR executable.");
@@ -31,6 +40,23 @@ static void Main(string[] args)
};
rootCommand.AddOption(outputOption);
+
+ var libraryDirectoryOption = new Option(
+ aliases: new string[] { "--libraryDirectory" })
+ {
+ Description = "Path to the directory containing the libraries that must be linked to the driver executable.",
+ IsRequired = true
+ };
+
+ rootCommand.AddOption(libraryDirectoryOption);
+ var includeDirectoryOption = new Option(
+ aliases: new string[] { "--includeDirectory" })
+ {
+ Description = "Path to the directory containing headers that must be included by the C++ driver.",
+ IsRequired = true
+ };
+
+ rootCommand.AddOption(includeDirectoryOption);
var errorOption = new Option(
aliases: new string[] { "--error",})
{
@@ -41,7 +67,9 @@ static void Main(string[] args)
rootCommand.AddOption(errorOption);
// Bind to a handler and invoke.
- rootCommand.Handler = CommandHandler.Create((input, output, error) => Controller.Execute(input, output, error));
+ rootCommand.Handler = CommandHandler.Create(
+ async (input, output, libraryDirectory, includeDirectory, error) =>
+ await Controller.ExecuteAsync(input, output, libraryDirectory, includeDirectory, error, driverGenerator, execGenerator, execRunner, logger));
rootCommand.Invoke(args);
}
}
diff --git a/src/Qir/Controller/QirController.csproj b/src/Qir/Controller/QirController.csproj
index 0c21b5914f9..4570cafa210 100644
--- a/src/Qir/Controller/QirController.csproj
+++ b/src/Qir/Controller/QirController.csproj
@@ -1,12 +1,40 @@
-
+
Exe
netcoreapp3.1
+ Microsoft.Quantum.Qir
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ ErrorMessages.resx
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ ErrorMessages.Designer.cs
+
+
+
diff --git a/src/Qir/Controller/Tests.QirController/ControllerTests.cs b/src/Qir/Controller/Tests.QirController/ControllerTests.cs
new file mode 100644
index 00000000000..5da9e861d37
--- /dev/null
+++ b/src/Qir/Controller/Tests.QirController/ControllerTests.cs
@@ -0,0 +1,213 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Qir;
+using Microsoft.Quantum.Qir.Driver;
+using Microsoft.Quantum.Qir.Executable;
+using Microsoft.Quantum.Qir.Model;
+using Microsoft.Quantum.Qir.Utility;
+using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint;
+using Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper;
+using Moq;
+using Xunit;
+using QirExecutionWrapperSerialization = Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper.Protocols;
+
+namespace Tests.QirController
+{
+ public class ControllerTests : IDisposable
+ {
+ private Mock driverGeneratorMock;
+ private Mock executableGeneratorMock;
+ private Mock executableRunnerMock;
+ private Mock loggerMock;
+ private FileInfo inputFile;
+ private FileInfo bytecodeFile;
+ private FileInfo errorFile;
+ private FileInfo outputFile;
+ private QirExecutionWrapper input;
+
+ public ControllerTests()
+ {
+ driverGeneratorMock = new Mock();
+ executableGeneratorMock = new Mock();
+ executableRunnerMock = new Mock();
+ inputFile = new FileInfo($"{Guid.NewGuid()}-input");
+ bytecodeFile = new FileInfo($"{Guid.NewGuid()}-bytecode");
+ errorFile = new FileInfo($"{Guid.NewGuid()}-error");
+ outputFile = new FileInfo($"{Guid.NewGuid()}-output");
+ loggerMock = new Mock();
+
+ // Create a QirExecutableWrapper to be used by the tests.
+ byte[] bytecode = { 1, 2, 3, 4, 5 };
+ input = new QirExecutionWrapper()
+ {
+ EntryPoint = new EntryPointOperation()
+ {
+ Arguments = new List
+ {
+ new Argument()
+ {
+ Position = 0,
+ Name = "argname",
+ Values = new List { new ArgumentValue { String = "argvalue" } },
+ }
+ }
+ },
+ QirBytecode = new ArraySegment(bytecode, 1, 3),
+ };
+ using var fileStream = inputFile.OpenWrite();
+ QirExecutionWrapperSerialization.SerializeToFastBinary(input, fileStream);
+ }
+
+ public void Dispose()
+ {
+ inputFile.Delete();
+ bytecodeFile.Delete();
+ errorFile.Delete();
+ outputFile.Delete();
+ }
+
+ [Fact]
+ public async Task TestExecute()
+ {
+ var libraryDirectory = new DirectoryInfo("libraries");
+ var includeDirectory = new DirectoryInfo("includes");
+ FileInfo actualExecutableFile = null;
+ Action generateExecutableCallback = async (executableFile, srcDir, libDir, inclDir) =>
+ {
+ actualExecutableFile = executableFile;
+ await Task.CompletedTask;
+ };
+ executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.Is(actualLibraryDirectory => actualLibraryDirectory.FullName == libraryDirectory.FullName),
+ It.Is(actualIncludeDirectory => actualIncludeDirectory.FullName == includeDirectory.FullName))).Callback(generateExecutableCallback);
+
+ await Controller.ExecuteAsync(
+ inputFile,
+ outputFile,
+ libraryDirectory,
+ includeDirectory,
+ errorFile,
+ driverGeneratorMock.Object,
+ executableGeneratorMock.Object,
+ executableRunnerMock.Object,
+ loggerMock.Object);
+
+ // Verify driver was created.
+ driverGeneratorMock.Verify(obj => obj.GenerateQirDriverCppAsync(
+ It.IsAny(),
+ It.Is(entryPoint => EntryPointsAreEqual(entryPoint, input.EntryPoint)),
+ It.Is>(bytecode => BytecodesAreEqual(bytecode, input.QirBytecode))));
+
+ // Verify executable was generated.
+ executableGeneratorMock.Verify(obj => obj.GenerateExecutableAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.Is(actualLibraryDirectory => actualLibraryDirectory.FullName == libraryDirectory.FullName),
+ It.Is(actualIncludeDirectory => actualIncludeDirectory.FullName == includeDirectory.FullName)));
+ Assert.NotNull(actualExecutableFile);
+
+ // Verify executable was run.
+ executableRunnerMock.Verify(obj => obj.RunExecutableAsync(
+ It.Is(executableFile => actualExecutableFile.FullName == executableFile.FullName),
+ It.Is(entryPoint => EntryPointsAreEqual(entryPoint, input.EntryPoint)),
+ It.Is(actualOutputFile => actualOutputFile.FullName == outputFile.FullName)));
+ }
+
+ [Fact]
+ public async Task TestExecuteEncountersGenericException()
+ {
+ var libraryDirectory = new DirectoryInfo("libraries");
+ var includeDirectory = new DirectoryInfo("includes");
+ executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new Exception("exception message"));
+
+ // Execute controller.
+ await Controller.ExecuteAsync(
+ inputFile,
+ outputFile,
+ libraryDirectory,
+ includeDirectory,
+ errorFile,
+ driverGeneratorMock.Object,
+ executableGeneratorMock.Object,
+ executableRunnerMock.Object,
+ loggerMock.Object);
+
+ // Verify error file was created and contains the error.
+ Assert.True(errorFile.Exists);
+ using var errorFileStream = errorFile.OpenRead();
+ using var streamReader = new StreamReader(errorFileStream);
+ var errorFileContents = await streamReader.ReadToEndAsync();
+ var error = JsonSerializer.Deserialize(errorFileContents);
+ Assert.Equal(ErrorMessages.InternalError, error.Message);
+ Assert.Equal(Constant.ErrorCode.InternalError, error.Code);
+ }
+
+ [Fact]
+ public async Task TestExecuteEncountersControllerException()
+ {
+ var exceptionMessage = "exception message";
+ var errorCode = "error code";
+ var libraryDirectory = new DirectoryInfo("libraries");
+ var includeDirectory = new DirectoryInfo("includes");
+ executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new ControllerException(exceptionMessage, errorCode));
+
+ // Execute controller.
+ await Controller.ExecuteAsync(
+ inputFile,
+ outputFile,
+ libraryDirectory,
+ includeDirectory,
+ errorFile,
+ driverGeneratorMock.Object,
+ executableGeneratorMock.Object,
+ executableRunnerMock.Object,
+ loggerMock.Object);
+
+ // Verify error file was created and contains the error.
+ Assert.True(errorFile.Exists);
+ using var errorFileStream = errorFile.OpenRead();
+ using var streamReader = new StreamReader(errorFileStream);
+ var errorFileContents = await streamReader.ReadToEndAsync();
+ var error = JsonSerializer.Deserialize(errorFileContents);
+ Assert.Equal(exceptionMessage, error.Message);
+ Assert.Equal(errorCode, error.Code);
+ }
+
+ private bool EntryPointsAreEqual(EntryPointOperation entryPointA, EntryPointOperation entryPointB)
+ {
+ var method = typeof(Extensions)
+ .GetMethod("ValueEquals", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(EntryPointOperation), typeof(EntryPointOperation) }, null);
+ object[] parameters = { entryPointA, entryPointB };
+ return (bool)method.Invoke(null, parameters);
+ }
+
+ private bool BytecodesAreEqual(ArraySegment bytecodeA, ArraySegment bytecodeB)
+ {
+ if (bytecodeA.Count != bytecodeB.Count)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < bytecodeA.Count; ++i)
+ {
+ if (bytecodeA[i] != bytecodeB[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Qir/Controller/Tests.QirController/LoggerTests.cs b/src/Qir/Controller/Tests.QirController/LoggerTests.cs
new file mode 100644
index 00000000000..ee7adfee70a
--- /dev/null
+++ b/src/Qir/Controller/Tests.QirController/LoggerTests.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using Microsoft.Quantum.Qir.Utility;
+using Moq;
+using Xunit;
+
+namespace Tests.QirController
+{
+ public class LoggerTests
+ {
+ private readonly Mock clockMock;
+ private readonly Logger logger;
+
+ public LoggerTests()
+ {
+ clockMock = new Mock();
+ logger = new Logger(clockMock.Object);
+ }
+
+ [Fact]
+ public void TestLogInfo()
+ {
+ using var consoleOutput = new StringWriter();
+ var message = "some message";
+ var time = DateTimeOffset.MinValue;
+ clockMock.SetupGet(obj => obj.Now).Returns(time);
+ var expectedLog = $"{time} [INFO]: some message" + Environment.NewLine;
+ Console.SetOut(consoleOutput);
+ logger.LogInfo(message);
+ var actualLog = consoleOutput.ToString();
+ Assert.Equal(expectedLog, actualLog);
+ }
+
+ [Fact]
+ public void TestLogError()
+ {
+ using var consoleOutput = new StringWriter();
+ var message = "some message";
+ var time = DateTimeOffset.MinValue;
+ clockMock.SetupGet(obj => obj.Now).Returns(time);
+ var expectedLog = $"{time} [ERROR]: some message" + Environment.NewLine;
+ Console.SetOut(consoleOutput);
+ logger.LogError(message);
+ var actualLog = consoleOutput.ToString();
+ Assert.Equal(expectedLog, actualLog);
+ }
+
+ [Fact]
+ public void TestLogExceptionWithoutStackTrace()
+ {
+ using var consoleOutput = new StringWriter();
+ var time = DateTimeOffset.MinValue;
+ clockMock.SetupGet(obj => obj.Now).Returns(time);
+ var exception = new InvalidOperationException();
+ var expectedLog = $"{time} [ERROR]: " +
+ "Exception encountered: System.InvalidOperationException: " +
+ exception.Message + Environment.NewLine + exception.StackTrace + Environment.NewLine;
+ Console.SetOut(consoleOutput);
+ logger.LogException(exception);
+ var actualLog = consoleOutput.ToString();
+ Assert.Equal(expectedLog, actualLog);
+ }
+
+ [Fact]
+ public void TestLogExceptionWithStackTrace()
+ {
+ using var consoleOutput = new StringWriter();
+ var time = DateTimeOffset.MinValue;
+ clockMock.SetupGet(obj => obj.Now).Returns(time);
+ Exception exception;
+ try
+ {
+ throw new InvalidOperationException();
+ }
+ // Throw exception to generate stack trace.
+ catch (Exception thrownException)
+ {
+ exception = thrownException;
+ }
+
+ var expectedLog = $"{time} [ERROR]: " +
+ "Exception encountered: System.InvalidOperationException: " +
+ exception.Message + Environment.NewLine + exception.StackTrace + Environment.NewLine;
+ Console.SetOut(consoleOutput);
+ logger.LogException(exception);
+ var actualLog = consoleOutput.ToString();
+ Assert.Equal(expectedLog, actualLog);
+ }
+ }
+}
diff --git a/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj b/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj
new file mode 100644
index 00000000000..ba5183ca742
--- /dev/null
+++ b/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp3.1
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Qir/Controller/Utility/Clock.cs b/src/Qir/Controller/Utility/Clock.cs
new file mode 100644
index 00000000000..7f1fdc8c78e
--- /dev/null
+++ b/src/Qir/Controller/Utility/Clock.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Quantum.Qir.Utility
+{
+ public class Clock : IClock
+ {
+ public DateTimeOffset Now => DateTimeOffset.Now;
+ }
+}
diff --git a/src/Qir/Controller/Utility/IClock.cs b/src/Qir/Controller/Utility/IClock.cs
new file mode 100644
index 00000000000..da69204b74a
--- /dev/null
+++ b/src/Qir/Controller/Utility/IClock.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Quantum.Qir.Utility
+{
+ ///
+ /// Mockable clock interface.
+ ///
+ public interface IClock
+ {
+ public DateTimeOffset Now { get; }
+ }
+}
diff --git a/src/Qir/Controller/Utility/ILogger.cs b/src/Qir/Controller/Utility/ILogger.cs
new file mode 100644
index 00000000000..7049bee9582
--- /dev/null
+++ b/src/Qir/Controller/Utility/ILogger.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Quantum.Qir.Utility
+{
+ ///
+ /// Logger for internal traces and errors.
+ ///
+ /// For now, this thinly wraps console logging. In the future, this might be extended to log to files or other systems.
+ public interface ILogger
+ {
+ ///
+ /// Logs a message at the "information" level.
+ ///
+ /// Message to log.
+ void LogInfo(string message);
+
+ ///
+ /// Logs a message at the "error" level.
+ ///
+ /// Message to log.
+ void LogError(string message);
+
+ ///
+ /// Formats an exception into an error log. Logs the exception type, message, and stack trace.
+ ///
+ /// Exception to log.
+ void LogException(Exception e);
+ }
+}
diff --git a/src/Qir/Controller/Utility/Logger.cs b/src/Qir/Controller/Utility/Logger.cs
new file mode 100644
index 00000000000..4aa2074a760
--- /dev/null
+++ b/src/Qir/Controller/Utility/Logger.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Quantum.Qir.Utility
+{
+ public class Logger : ILogger
+ {
+ private readonly IClock clock;
+
+ public Logger(IClock clock)
+ {
+ this.clock = clock;
+ }
+
+ // {timestamp} [{log level}]: {message}.
+ private const string LogFormat = "{0} [{1}]: {2}";
+
+ // ...{exception type}: {exception message}{Environment.NewLine}{stack trace}.
+ private const string ExceptionMessageFormat = "Exception encountered: {0}: {1}{2}{3}";
+ private const string InfoLevel = "INFO";
+ private const string ErrorLevel = "ERROR";
+
+ public void LogInfo(string message)
+ {
+ Console.WriteLine(LogFormat, clock.Now, InfoLevel, message);
+ }
+
+ public void LogError(string message)
+ {
+ Console.WriteLine(LogFormat, clock.Now, ErrorLevel, message);
+ }
+
+ public void LogException(Exception e)
+ {
+ var message = string.Format(ExceptionMessageFormat, e.GetType(), e.Message, Environment.NewLine, e.StackTrace);
+ LogError(message);
+ }
+ }
+}
diff --git a/src/Qir/Controller/test-cases/01.err b/src/Qir/Controller/test-cases/01.err
deleted file mode 100644
index 760589cb5d6..00000000000
--- a/src/Qir/Controller/test-cases/01.err
+++ /dev/null
@@ -1 +0,0 @@
-error
\ No newline at end of file
diff --git a/src/Qir/Controller/test-cases/01.in b/src/Qir/Controller/test-cases/01.in
deleted file mode 100644
index 770eab4480b..00000000000
--- a/src/Qir/Controller/test-cases/01.in
+++ /dev/null
@@ -1 +0,0 @@
-input
\ No newline at end of file
diff --git a/src/Qir/Controller/test-cases/01.out b/src/Qir/Controller/test-cases/01.out
deleted file mode 100644
index 6caf68aff42..00000000000
--- a/src/Qir/Controller/test-cases/01.out
+++ /dev/null
@@ -1 +0,0 @@
-output
\ No newline at end of file
diff --git a/src/Qir/Controller/test-cases/internal-error-test.err b/src/Qir/Controller/test-cases/internal-error-test.err
new file mode 100644
index 00000000000..cc5c5a256f3
--- /dev/null
+++ b/src/Qir/Controller/test-cases/internal-error-test.err
@@ -0,0 +1,4 @@
+{
+ "code": "InternalError",
+ "message": "An internal error occurred."
+}
\ No newline at end of file
diff --git a/src/Qir/Controller/test-cases/internal-error-test.in b/src/Qir/Controller/test-cases/internal-error-test.in
new file mode 100644
index 00000000000..37fc5162678
--- /dev/null
+++ b/src/Qir/Controller/test-cases/internal-error-test.in
@@ -0,0 +1 @@
+any input will do for now, but as errors become more specific, the corresponding test will need to change.
\ No newline at end of file
diff --git a/src/Qir/Controller/test-qir-controller.ps1 b/src/Qir/Controller/test-qir-controller.ps1
index 876626b703a..ad70d23d1b7 100644
--- a/src/Qir/Controller/test-qir-controller.ps1
+++ b/src/Qir/Controller/test-qir-controller.ps1
@@ -8,6 +8,7 @@ Write-Host "##[info]Test QIR Controller"
$controllerProject = (Join-Path $PSScriptRoot QirController.csproj)
$testCasesFolder = (Join-Path $PSScriptRoot "test-cases")
$testArtifactsFolder = (Join-Path $PSScriptRoot "test-artifacts")
+
if (!(Test-Path $testArtifactsFolder -PathType Container)) {
New-Item -ItemType Directory -Force -Path $testArtifactsFolder
}
@@ -19,23 +20,29 @@ Foreach-Object {
# Get the paths to the output and error files to pass to the QIR controller.
$outputFile = (Join-Path $testArtifactsFolder ($_.BaseName + ".out"))
$errorFile = (Join-Path $testArtifactsFolder ($_.BaseName + ".err"))
- dotnet run --project $controllerProject -- --input $_.FullName --output $outputFile --error $errorFile
+ dotnet run --project $controllerProject -- --input $_.FullName --output $outputFile --error $errorFile --includeDirectory "placeholder for now" --libraryDirectory "placeholder for now"
# Compare the expected content of the output and error files vs the actual content.
$expectedOutputFile = (Join-Path $testCasesFolder ($_.BaseName + ".out"))
- $expectedOutput = Get-Content -Path $expectedOutputFile -Raw
- $actualOutput = Get-Content -Path $outputFile -Raw
- if (-not ($expectedOutput -ceq $actualOutput)) {
- Write-Host "##vso[task.logissue type=error;]Failed QIR Controller test case: $($_.BaseName)"
- Write-Host "##[info]Expected output:"
- Write-Host $expectedOutput
- Write-Host "##[info]Actual output:"
- Write-Host $actualOutput
- $script:all_ok = $False
- break
+ $expectedErrorFile = (Join-Path $testCasesFolder ($_.BaseName + ".err"))
+
+ if ((Test-Path $expectedOutputFile)) {
+ $expectedOutput = Get-Content -Path $expectedOutputFile -Raw
+ $actualOutput = Get-Content -Path $outputFile -Raw
+ if (-not ($expectedOutput -ceq $actualOutput)) {
+ Write-Host "##vso[task.logissue type=error;]Failed QIR Controller test case: $($_.BaseName)"
+ Write-Host "##[info]Expected output:"
+ Write-Host $expectedOutput
+ Write-Host "##[info]Actual output:"
+ Write-Host $actualOutput
+ $script:all_ok = $False
+ }
+ else {
+ Write-Host "##[info]Test case '$($_.BaseName)' passed"
+ }
+ continue;
}
- $expectedErrorFile = (Join-Path $testCasesFolder ($_.BaseName + ".err"))
$expectedError = Get-Content -Path $expectedErrorFile -Raw
$actualError = Get-Content -Path $errorFile -Raw
if (-not ($expectedError -ceq $actualError)) {
@@ -45,10 +52,11 @@ Foreach-Object {
Write-Host "##[info]Actual error:"
Write-Host $actualError
$script:all_ok = $False
- break
+ continue
+ }
+ else {
+ Write-Host "##[info]Test case '$($_.BaseName)' passed"
}
-
- Write-Host "##[info]Test case '$($_.BaseName)' passed"
}
if (-not $all_ok) {