diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index ee639ec3e8..a282d18788 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -5,18 +5,20 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Azure.Quantum.Storage; +using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.Simulation.Core; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; using Microsoft.Rest.Azure; -using Microsoft.Azure.Quantum.Client.Models; -using Microsoft.Azure.Quantum.Storage; -using Microsoft.Azure.Quantum; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -25,6 +27,9 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + private ILogger Logger { get; } + private IReferences References { get; } + private IEntryPointGenerator EntryPointGenerator { get; } private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } private AuthenticationResult? AuthenticationResult { get; set; } @@ -41,10 +46,20 @@ private string ValidExecutionTargetsDisplayText : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); } + public AzureClient( + IReferences references, + IEntryPointGenerator entryPointGenerator, + ILogger logger, + IEventService eventService) + { + References = references; + EntryPointGenerator = entryPointGenerator; + Logger = logger; + eventService?.TriggerServiceInitialized(this); + } /// - public async Task ConnectAsync( - IChannel channel, + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, @@ -127,7 +142,7 @@ public async Task ConnectAsync( } catch (Exception e) { - channel.Stderr(e.ToString()); + Logger?.LogError(e, $"Failed to download providers list from Azure Quantum workspace: {e.Message}"); return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } @@ -151,11 +166,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); } - private async Task SubmitOrExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName, - bool execute) + private async Task SubmitOrExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters, bool execute) { if (ActiveWorkspace == null) { @@ -176,7 +187,7 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); + var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); if (machine == null) { // We should never get here, since ActiveTarget should have already been validated at the time it was set. @@ -184,49 +195,81 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.InvalidTarget.ToExecutionResult(); } - var operationInfo = operationResolver.Resolve(operationName); - var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); - var entryPointInput = QVoid.Instance; + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - if (execute) + IEntryPoint? entryPoint = null; + try { - channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}..."); - var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput); - MostRecentJobId = output.Job.Id; - - // TODO: Add encoder to visualize IEnumerable> - return output.Histogram.ToExecutionResult(); + entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); } - else + catch (UnsupportedOperationException e) { - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); - channel.Stdout($"Job {job.Id} submitted successfully."); + channel.Stderr($"{operationName} is not a recognized Q# operation name."); + return AzureClientError.UnrecognizedOperationName.ToExecutionResult(); + } + catch (CompilationErrorsException e) + { + channel.Stderr($"The Q# operation {operationName} could not be compiled as an entry point for job execution."); + foreach (var message in e.Errors) channel.Stderr(message); + return AzureClientError.InvalidEntryPoint.ToExecutionResult(); + } + try + { + var job = await entryPoint.SubmitAsync(machine, inputParameters); + channel.Stdout($"Job {job.Id} submitted successfully."); MostRecentJobId = job.Id; + } + catch (ArgumentException e) + { + channel.Stderr($"Failed to parse all expected parameters for Q# operation {operationName}."); + channel.Stderr(e.Message); + return AzureClientError.JobSubmissionFailed.ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr($"Failed to submit Q# operation {operationName} for execution."); + channel.Stderr(e.InnerException?.Message ?? e.Message); + return AzureClientError.JobSubmissionFailed.ToExecutionResult(); + } - // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. - return job.ToJupyterTable().ToExecutionResult(); + if (!execute) + { + return await GetJobStatusAsync(channel, MostRecentJobId); } + + var timeoutInSeconds = 30; + channel.Stdout($"Waiting up to {timeoutInSeconds} seconds for Azure Quantum job to complete..."); + + using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + CloudJob? cloudJob = null; + do + { + // TODO: Once jupyter-core supports interrupt requests (https://github.com/microsoft/jupyter-core/issues/55), + // handle Jupyter kernel interrupt here and break out of this loop + var pollingIntervalInSeconds = 5; + await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds)); + if (cts.IsCancellationRequested) break; + cloudJob = await GetCloudJob(MostRecentJobId); + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); + } + while (cloudJob == null || cloudJob.InProgress); + } + + return await GetJobResultAsync(channel, MostRecentJobId); } /// - public async Task SubmitJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName) => - await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: false); + public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => + await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: false); /// - public async Task ExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName) => - await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: true); + public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => + await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: true); /// - public async Task GetActiveTargetAsync( - IChannel channel) + public async Task GetActiveTargetAsync(IChannel channel) { if (AvailableProviders == null) { @@ -247,10 +290,7 @@ public async Task GetActiveTargetAsync( } /// - public async Task SetActiveTargetAsync( - IChannel channel, - IReferences references, - string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetName) { if (AvailableProviders == null) { @@ -279,15 +319,13 @@ public async Task SetActiveTargetAsync( ActiveTarget = executionTarget; channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); - await references.AddPackage(ActiveTarget.PackageName); + await References.AddPackage(ActiveTarget.PackageName); return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); } /// - public async Task GetJobResultAsync( - IChannel channel, - string jobId) + public async Task GetJobResultAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -306,7 +344,7 @@ public async Task GetJobResultAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -335,9 +373,7 @@ public async Task GetJobResultAsync( } /// - public async Task GetJobStatusAsync( - IChannel channel, - string jobId) + public async Task GetJobStatusAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -356,20 +392,19 @@ public async Task GetJobStatusAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. - return job.Details.ToExecutionResult(); + // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. + return job.ToJupyterTable().ToExecutionResult(); } /// - public async Task GetJobListAsync( - IChannel channel) + public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { @@ -377,7 +412,7 @@ public async Task GetJobListAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = ActiveWorkspace.ListJobs(); + var jobs = await GetCloudJobs(); if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); @@ -385,7 +420,35 @@ public async Task GetJobListAsync( } // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult(); + return jobs.ToJupyterTable().ToExecutionResult(); + } + + private async Task GetCloudJob(string jobId) + { + try + { + return await ActiveWorkspace.GetJobAsync(jobId); + } + catch (Exception e) + { + Logger?.LogError(e, $"Failed to retrieve the specified Azure Quantum job: {e.Message}"); + } + + return null; + } + + private async Task?> GetCloudJobs() + { + try + { + return await ActiveWorkspace.ListJobsAsync(); + } + catch (Exception e) + { + Logger?.LogError(e, $"Failed to retrieve the list of jobs from the Azure Quantum workspace: {e.Message}"); + } + + return null; } } } diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index b2fc4b4554..995ae372bb 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs new file mode 100644 index 0000000000..407f29d0d5 --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + internal class EntryPoint : IEntryPoint + { + private object EntryPointInfo { get; } + private Type InputType { get; } + private Type OutputType { get; } + private OperationInfo OperationInfo { get; } + + /// + /// Creates an object used to submit jobs to Azure Quantum. + /// + /// Must be an object with type + /// parameters specified by the types in the entryPointInputbeginWords argument. + /// Specifies the input parameter type for the + /// object provided as the entryPointInfo argument. + /// Specifies the output parameter type for the + /// object provided as the entryPointInfo argument. + /// Information about the Q# operation to be used as the entry point. + public EntryPoint(object entryPointInfo, Type inputType, Type outputType, OperationInfo operationInfo) + { + EntryPointInfo = entryPointInfo; + InputType = inputType; + OutputType = outputType; + OperationInfo = operationInfo; + } + + /// + public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters) + { + var parameterTypes = new List(); + var parameterValues = new List(); + foreach (var parameter in OperationInfo.RoslynParameters) + { + if (!inputParameters.ContainsKey(parameter.Name)) + { + throw new ArgumentException($"Required parameter {parameter.Name} was not specified."); + } + + string rawParameterValue = inputParameters[parameter.Name]; + object? parameterValue = null; + try + { + parameterValue = System.Convert.ChangeType(rawParameterValue, parameter.ParameterType); + } + catch (Exception e) + { + throw new ArgumentException($"The value {rawParameterValue} provided for parameter {parameter.Name} could not be converted to the expected type: {e.Message}"); + } + + parameterTypes.Add(parameter.ParameterType); + parameterValues.Add(parameterValue); + } + + var entryPointInput = parameterValues.Count switch + { + 0 => QVoid.Instance, + 1 => parameterValues.Single(), + _ => InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()) + }; + + // Find and invoke the method on IQuantumMachine that is declared as: + // Task SubmitAsync(EntryPointInfo info, TInput input) + var submitMethod = typeof(IQuantumMachine) + .GetMethods() + .Single(method => + method.Name == "SubmitAsync" + && method.IsGenericMethodDefinition + && method.GetParameters().Length == 2 + && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == EntryPointInfo.GetType().GetGenericTypeDefinition() + && method.GetParameters()[1].ParameterType.IsGenericMethodParameter) + .MakeGenericMethod(new Type[] { InputType, OutputType }); + var submitParameters = new object[] { EntryPointInfo, entryPointInput }; + return (Task)submitMethod.Invoke(machine, submitParameters); + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs new file mode 100644 index 0000000000..31f997f40f --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.Extensions.Logging; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + internal class EntryPointGenerator : IEntryPointGenerator + { + private ICompilerService Compiler { get; } + private ILogger Logger { get; } + private IWorkspace Workspace { get; } + private ISnippets Snippets { get; } + public IReferences References { get; } + public AssemblyInfo? WorkspaceAssemblyInfo { get; set; } + public AssemblyInfo? SnippetsAssemblyInfo { get; set; } + public AssemblyInfo? EntryPointAssemblyInfo { get; set; } + + public EntryPointGenerator( + ICompilerService compiler, + IWorkspace workspace, + ISnippets snippets, + IReferences references, + ILogger logger, + IEventService eventService) + { + Compiler = compiler; + Workspace = workspace; + Snippets = snippets; + References = references; + Logger = logger; + + AssemblyLoadContext.Default.Resolving += Resolve; + + eventService?.TriggerServiceInitialized(this); + } + + /// + /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext + /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. + /// + public Assembly? Resolve(AssemblyLoadContext context, AssemblyName name) => name.Name switch + { + var s when s == Path.GetFileNameWithoutExtension(EntryPointAssemblyInfo?.Location) => EntryPointAssemblyInfo?.Assembly, + var s when s == Path.GetFileNameWithoutExtension(SnippetsAssemblyInfo?.Location) => SnippetsAssemblyInfo?.Assembly, + var s when s == Path.GetFileNameWithoutExtension(WorkspaceAssemblyInfo?.Location) => WorkspaceAssemblyInfo?.Assembly, + _ => null + }; + + public IEntryPoint Generate(string operationName, string? executionTarget) + { + Logger?.LogDebug($"Generating entry point: operationName={operationName}, executionTarget={executionTarget}"); + + var logger = new QSharpLogger(Logger); + var compilerMetadata = References.CompilerMetadata; + + // Clear references to previously-built assemblies + WorkspaceAssemblyInfo = null; + SnippetsAssemblyInfo = null; + EntryPointAssemblyInfo = null; + + // Compile the workspace against the provided execution target + var workspaceFiles = Workspace.SourceFiles.ToArray(); + if (workspaceFiles.Any()) + { + Logger?.LogDebug($"{workspaceFiles.Length} files found in workspace. Compiling."); + WorkspaceAssemblyInfo = Compiler.BuildFiles( + workspaceFiles, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__workspace__.dll"), executionTarget); + if (WorkspaceAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling workspace."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + + compilerMetadata = compilerMetadata.WithAssemblies(WorkspaceAssemblyInfo); + } + + // Compile the snippets against the provided execution target + var snippets = Snippets.Items.ToArray(); + if (snippets.Any()) + { + Logger?.LogDebug($"{snippets.Length} items found in snippets. Compiling."); + SnippetsAssemblyInfo = Compiler.BuildSnippets( + snippets, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__snippets__.dll"), executionTarget); + if (SnippetsAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling snippets."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + + compilerMetadata = compilerMetadata.WithAssemblies(SnippetsAssemblyInfo); + } + + // Build the entry point assembly + var operationInfo = new EntryPointOperationResolver(this).Resolve(operationName); + if (operationInfo == null) + { + Logger?.LogError($"{operationName} is not a recognized Q# operation name."); + throw new UnsupportedOperationException(operationName); + } + + EntryPointAssemblyInfo = Compiler.BuildEntryPoint( + operationInfo, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll"), executionTarget); + if (EntryPointAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling entry point for operation {operationName}."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + + var entryPointOperationInfo = EntryPointAssemblyInfo.Operations.Single(); + + // Construct the EntryPointInfo<,> object + var parameterTypes = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).ToArray(); + var typeCount = parameterTypes.Length; + Type entryPointInputType = typeCount switch + { + 0 => typeof(QVoid), + 1 => parameterTypes.Single(), + _ => PartialMapper.TupleTypes[typeCount].MakeGenericType(parameterTypes) + }; + Type entryPointOutputType = entryPointOperationInfo.ReturnType; + + Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(new Type[] { entryPointInputType, entryPointOutputType }); + var entryPointInfo = entryPointInfoType.GetConstructor(new Type[] { typeof(Type) }) + .Invoke(new object[] { entryPointOperationInfo.RoslynType }); + + return new EntryPoint(entryPointInfo, entryPointInputType, entryPointOutputType, entryPointOperationInfo); + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs new file mode 100644 index 0000000000..28dcda3f4c --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class EntryPointOperationResolver : IOperationResolver + { + private IEntryPointGenerator EntryPointGenerator { get; } + + public EntryPointOperationResolver(IEntryPointGenerator entryPointGenerator) => + EntryPointGenerator = entryPointGenerator; + + public OperationInfo Resolve(string name) => OperationResolver.ResolveFromAssemblies(name, RelevantAssemblies()); + + private IEnumerable RelevantAssemblies() + { + if (EntryPointGenerator.SnippetsAssemblyInfo != null) yield return EntryPointGenerator.SnippetsAssemblyInfo; + if (EntryPointGenerator.WorkspaceAssemblyInfo != null) yield return EntryPointGenerator.WorkspaceAssemblyInfo; + + foreach (var asm in EntryPointGenerator.References.Assemblies) + { + yield return asm; + } + } + } +} diff --git a/src/AzureClient/EntryPoint/IEntryPoint.cs b/src/AzureClient/EntryPoint/IEntryPoint.cs new file mode 100644 index 0000000000..4cc6e063d7 --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPoint.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Represents a Q# entry point that can be submitted + /// for execution to Azure Quantum. + /// + public interface IEntryPoint + { + /// + /// Submits the entry point for execution to Azure Quantum. + /// + /// The object representing the job submission target. + /// The provided input parameters to the entry point operation. + /// The details of the submitted job. + public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters); + } +} diff --git a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs new file mode 100644 index 0000000000..1ed8b2a87a --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// This service is capable of generating entry points for + /// job submission to Azure Quantum. + /// + public interface IEntryPointGenerator + { + /// + /// Gets the compiled workspace assembly for the most recently-generated entry point. + /// + public AssemblyInfo? WorkspaceAssemblyInfo { get; } + + /// + /// Gets the compiled snippets assembly for the most recently-generated entry point. + /// + public AssemblyInfo? SnippetsAssemblyInfo { get; } + + /// + /// Gets the compiled entry point assembly for the most recently-generated entry point. + /// + public AssemblyInfo? EntryPointAssemblyInfo { get; } + + /// + /// Gets the references used for compilation of the entry point assembly. + /// + public IReferences References { get; } + + /// + /// Compiles an assembly and returns the object + /// representing an entry point that wraps the specified operation. + /// + /// The name of the operation to wrap in an entry point. + /// The intended execution target for the compiled entry point. + /// The generated entry point. + public IEntryPoint Generate(string operationName, string? executionTarget); + } +} diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 116643480f..0727e8aaca 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; @@ -27,6 +28,7 @@ public static class Extensions public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); } /// @@ -66,19 +68,19 @@ public static string ToDescription(this AzureClientError azureClientError) public static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); - internal static Table ToJupyterTable(this JobDetails jobDetails) => - new List { jobDetails }.ToJupyterTable(); + internal static Table ToJupyterTable(this CloudJob cloudJob) => + new List { cloudJob }.ToJupyterTable(); - internal static Table ToJupyterTable(this IEnumerable jobsList) => - new Table + internal static Table ToJupyterTable(this IEnumerable jobsList) => + new Table { - Columns = new List<(string, Func)> + Columns = new List<(string, Func)> { - ("JobId", jobDetails => jobDetails.Id), - ("JobName", jobDetails => jobDetails.Name), - ("JobStatus", jobDetails => jobDetails.Status), - ("Provider", jobDetails => jobDetails.ProviderId), - ("Target", jobDetails => jobDetails.Target), + ("JobId", cloudJob => cloudJob.Id), + ("JobName", cloudJob => cloudJob.Details.Name), + ("JobStatus", cloudJob => cloudJob.Status), + ("Provider", cloudJob => cloudJob.Details.ProviderId), + ("Target", cloudJob => cloudJob.Details.Target), }, Rows = jobsList.ToList() }; diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index f7df675652..952c28d796 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -56,6 +57,24 @@ public enum AzureClientError [Description(Resources.AzureClientErrorNoOperationName)] NoOperationName, + /// + /// The specified Q# operation name is not recognized. + /// + [Description(Resources.AzureClientErrorUnrecognizedOperationName)] + UnrecognizedOperationName, + + /// + /// The specified Q# operation cannot be used as an entry point. + /// + [Description(Resources.AzureClientErrorInvalidEntryPoint)] + InvalidEntryPoint, + + /// + /// The Azure Quantum job submission failed. + /// + [Description(Resources.AzureClientErrorJobSubmissionFailed)] + JobSubmissionFailed, + /// /// Authentication with the Azure service failed. /// @@ -81,8 +100,7 @@ public interface IAzureClient /// /// The list of execution targets available in the Azure Quantum workspace. /// - public Task ConnectAsync( - IChannel channel, + public Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, @@ -96,8 +114,7 @@ public Task ConnectAsync( /// The list of execution targets available in the Azure Quantum workspace, /// or an error if the Azure Quantum workspace connection has not yet been created. /// - public Task GetConnectionStatusAsync( - IChannel channel); + public Task GetConnectionStatusAsync(IChannel channel); /// /// Submits the specified Q# operation as a job to the currently active target. @@ -105,10 +122,7 @@ public Task GetConnectionStatusAsync( /// /// Details of the submitted job, or an error if submission failed. /// - public Task SubmitJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName); + public Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters); /// /// Executes the specified Q# operation as a job to the currently active target @@ -117,10 +131,7 @@ public Task SubmitJobAsync( /// /// The result of the executed job, or an error if execution failed. /// - public Task ExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName); + public Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters); /// /// Sets the specified target for job submission. @@ -128,10 +139,7 @@ public Task ExecuteJobAsync( /// /// Success if the target is valid, or an error if the target cannot be set. /// - public Task SetActiveTargetAsync( - IChannel channel, - IReferences references, - string targetName); + public Task SetActiveTargetAsync(IChannel channel, string targetName); /// /// Gets the currently specified target for job submission. @@ -139,8 +147,7 @@ public Task SetActiveTargetAsync( /// /// The target name. /// - public Task GetActiveTargetAsync( - IChannel channel); + public Task GetActiveTargetAsync(IChannel channel); /// /// Gets the result of a specified job. @@ -149,9 +156,7 @@ public Task GetActiveTargetAsync( /// The job result corresponding to the given job ID, /// or for the most recently-submitted job if no job ID is provided. /// - public Task GetJobResultAsync( - IChannel channel, - string jobId); + public Task GetJobResultAsync(IChannel channel, string jobId); /// /// Gets the status of a specified job. @@ -160,9 +165,7 @@ public Task GetJobResultAsync( /// The job status corresponding to the given job ID, /// or for the most recently-submitted job if no job ID is provided. /// - public Task GetJobStatusAsync( - IChannel channel, - string jobId); + public Task GetJobStatusAsync(IChannel channel, string jobId); /// /// Gets a list of all jobs in the current Azure Quantum workspace. @@ -170,7 +173,6 @@ public Task GetJobStatusAsync( /// /// A list of all jobs in the current workspace. /// - public Task GetJobListAsync( - IChannel channel); + public Task GetJobListAsync(IChannel channel); } } diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 8f69532952..a5f317688d 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -3,9 +3,7 @@ #nullable enable -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -19,22 +17,13 @@ public class ExecuteMagic : AzureClientMagicBase { private const string ParameterNameOperationName = "operationName"; - /// - /// Gets the symbol resolver used by this magic command to find - /// operations or functions to be simulated. - /// - public IOperationResolver OperationResolver { get; } - /// /// Initializes a new instance of the class. /// - /// - /// The object used to find and resolve operations. - /// /// /// The object to use for Azure functionality. /// - public ExecuteMagic(IOperationResolver operationResolver, IAzureClient azureClient) + public ExecuteMagic(IAzureClient azureClient) : base( azureClient, "azure.execute", @@ -60,8 +49,8 @@ The Azure Quantum workspace must previously have been initialized ``` ".Dedent(), }, - }) => - this.OperationResolver = operationResolver; + }) + { } /// /// Executes a new job in an Azure Quantum workspace given a Q# operation @@ -72,7 +61,14 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.ExecuteJobAsync(channel, OperationResolver, operationName); + + var decodedParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + decodedParameters[key] = inputParameters.DecodeParameter(key); + } + + return await AzureClient.ExecuteJobAsync(channel, operationName, decodedParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 8d779646d5..cb646a9903 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -3,9 +3,7 @@ #nullable enable -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -19,22 +17,13 @@ public class SubmitMagic : AzureClientMagicBase { private const string ParameterNameOperationName = "operationName"; - /// - /// Gets the symbol resolver used by this magic command to find - /// operations or functions to be simulated. - /// - public IOperationResolver OperationResolver { get; } - /// /// Initializes a new instance of the class. /// - /// - /// The object used to find and resolve operations. - /// /// /// The object to use for Azure functionality. /// - public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) + public SubmitMagic(IAzureClient azureClient) : base( azureClient, "azure.submit", @@ -58,8 +47,8 @@ The Azure Quantum workspace must previously have been initialized ``` ".Dedent(), }, - }) => - this.OperationResolver = operationResolver; + }) + { } /// /// Submits a new job to an Azure Quantum workspace given a Q# operation @@ -69,7 +58,14 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); + + var decodedParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + decodedParameters[key] = inputParameters.DecodeParameter(key); + } + + return await AzureClient.SubmitJobAsync(channel, operationName, decodedParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 7e5e2a4151..2a5cf36c93 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -19,18 +19,13 @@ public class TargetMagic : AzureClientMagicBase { private const string ParameterNameTargetName = "name"; - private IReferences? References { get; set; } - /// /// Initializes a new instance of the class. /// /// /// The object to use for Azure functionality. /// - /// - /// The object to use for loading target-specific packages. - /// - public TargetMagic(IAzureClient azureClient, IReferences references) + public TargetMagic(IAzureClient azureClient) : base( azureClient, "azure.target", @@ -62,8 +57,8 @@ available in the workspace. ``` ".Dedent(), }, - }) => - References = references; + }) + { } /// /// Sets or views the target for job submission to the current Azure Quantum workspace. @@ -74,7 +69,7 @@ public override async Task RunAsync(string input, IChannel chan if (inputParameters.ContainsKey(ParameterNameTargetName)) { string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, References, targetName); + return await AzureClient.SetActiveTargetAsync(channel, targetName); } return await AzureClient.GetActiveTargetAsync(channel); diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 1fedce95f2..a85a3cf463 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -28,6 +28,15 @@ internal static class Resources public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; + public const string AzureClientErrorUnrecognizedOperationName = + "The specified Q# operation name was not recognized."; + + public const string AzureClientErrorInvalidEntryPoint = + "The specified Q# operation cannot be used as an entry point for Azure Quantum job submission."; + + public const string AzureClientErrorJobSubmissionFailed = + "Failed to submit the job to the Azure Quantum workspace."; + public const string AzureClientErrorAuthenticationFailed = "Failed to authenticate to the specified Azure Quantum workspace."; diff --git a/src/Core/Compiler/CompilerService.cs b/src/Core/Compiler/CompilerService.cs index 2869592270..6ac40cdd4b 100644 --- a/src/Core/Compiler/CompilerService.cs +++ b/src/Core/Compiler/CompilerService.cs @@ -16,9 +16,12 @@ using Microsoft.Quantum.QsCompiler.CompilationBuilder; using Microsoft.Quantum.QsCompiler.CsharpGeneration; using Microsoft.Quantum.QsCompiler.DataTypes; +using Microsoft.Quantum.QsCompiler.ReservedKeywords; using Microsoft.Quantum.QsCompiler.Serialization; +using Microsoft.Quantum.QsCompiler.SyntaxProcessing; using Microsoft.Quantum.QsCompiler.SyntaxTree; using Microsoft.Quantum.QsCompiler.Transformations.BasicTransformations; +using Microsoft.Quantum.QsCompiler.Transformations.QsCodeOutput; using Newtonsoft.Json.Bson; using QsReferences = Microsoft.Quantum.QsCompiler.CompilationBuilder.References; @@ -67,33 +70,61 @@ private QsCompilation UpdateCompilation(ImmutableDictionary sources return loaded.CompilationOutput; } + /// + public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) + { + var signature = operation.Header.PrintSignature(); + var argumentTuple = SyntaxTreeToQsharp.ArgumentTuple(operation.Header.ArgumentTuple, type => type.ToString(), symbolsOnly: true); + + var entryPointUri = new Uri(Path.GetFullPath(Path.Combine("/", $"entrypoint.qs"))); + var entryPointSnippet = @$"namespace ENTRYPOINT + {{ + open {operation.Header.QualifiedName.Namespace.Value}; + @{BuiltIn.EntryPoint.FullName}() + operation {signature} + {{ + return {operation.Header.QualifiedName}{argumentTuple}; + }} + }}"; + + var sources = new Dictionary() {{ entryPointUri, entryPointSnippet }}.ToImmutableDictionary(); + return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true, executionTarget: executionTarget); + } + /// /// Builds the corresponding .net core assembly from the code in the given Q# Snippets. /// Each snippet code is wrapped inside the 'SNIPPETS_NAMESPACE' namespace and processed as a file /// with the same name as the snippet id. /// - public AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + public AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) { string WrapInNamespace(Snippet s) => $"namespace {Snippets.SNIPPETS_NAMESPACE} {{ open Microsoft.Quantum.Intrinsic; open Microsoft.Quantum.Canon; {s.code} }}"; + // Ignore any @EntryPoint() attributes found in snippets. + logger.ErrorCodesToIgnore.Add(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); + var sources = snippets.ToImmutableDictionary(s => s.Uri, WrapInNamespace); - return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: false); + var assembly = BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: false, executionTarget: executionTarget); + + logger.ErrorCodesToIgnore.Remove(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); + + return assembly; } /// /// Builds the corresponding .net core assembly from the code in the given files. /// - public AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + public AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) { var sources = ProjectManager.LoadSourceFiles(files, d => logger?.Log(d), ex => logger?.Log(ex)); - return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true); + return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true, executionTarget: executionTarget); } /// /// Builds the corresponding .net core assembly from the Q# syntax tree. /// - private AssemblyInfo BuildAssembly(ImmutableDictionary sources, CompilerMetadata metadata, QSharpLogger logger, string dllName, bool compileAsExecutable) + private AssemblyInfo BuildAssembly(ImmutableDictionary sources, CompilerMetadata metadata, QSharpLogger logger, string dllName, bool compileAsExecutable, string executionTarget) { logger.LogDebug($"Compiling the following Q# files: {string.Join(",", sources.Keys.Select(f => f.LocalPath))}"); @@ -103,12 +134,15 @@ private AssemblyInfo BuildAssembly(ImmutableDictionary sources, Com try { // Generate C# simulation code from Q# syntax tree and convert it into C# syntax trees: - var trees = new List(); + var trees = new List(); NonNullable GetFileId(Uri uri) => CompilationUnitManager.TryGetFileId(uri, out var id) ? id : NonNullable.New(uri.AbsolutePath); foreach (var file in sources.Keys) { var sourceFile = GetFileId(file); - var code = SimulationCode.generate(sourceFile, CodegenContext.Create(qsCompilation.Namespaces)); + var codegenContext = string.IsNullOrEmpty(executionTarget) + ? CodegenContext.Create(qsCompilation.Namespaces) + : CodegenContext.Create(qsCompilation.Namespaces, new Dictionary() { { AssemblyConstants.ExecutionTarget, executionTarget } }); + var code = SimulationCode.generate(sourceFile, codegenContext); var tree = CSharpSyntaxTree.ParseText(code, encoding: UTF8Encoding.UTF8); trees.Add(tree); logger.LogDebug($"Generated the following C# code for {sourceFile.Value}:\n=============\n{code}\n=============\n"); diff --git a/src/Core/Compiler/ICompilerService.cs b/src/Core/Compiler/ICompilerService.cs index 7a9548a61f..76f95f6d0c 100644 --- a/src/Core/Compiler/ICompilerService.cs +++ b/src/Core/Compiler/ICompilerService.cs @@ -12,15 +12,21 @@ namespace Microsoft.Quantum.IQSharp /// public interface ICompilerService { + /// + /// Builds an executable assembly with an entry point that invokes the Q# operation specified + /// by the provided object. + /// + AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); + /// /// Builds the corresponding .net core assembly from the code in the given Q# Snippets. /// - AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); /// /// Builds the corresponding .net core assembly from the code in the given files. /// - AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); /// /// Returns the names of all declared callables and types. diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a0b687d1ba..11460fc08e 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Core/Loggers/QsharpLogger.cs b/src/Core/Loggers/QsharpLogger.cs index 247227fc72..6afbb7ebb9 100644 --- a/src/Core/Loggers/QsharpLogger.cs +++ b/src/Core/Loggers/QsharpLogger.cs @@ -22,13 +22,12 @@ public class QSharpLogger : QsCompiler.Diagnostics.LogTracker public List Logs { get; } - public List ErrorCodesToIgnore { get; } + public List ErrorCodesToIgnore { get; } = new List(); - public QSharpLogger(ILogger logger, List errorCodesToIgnore = null) + public QSharpLogger(ILogger logger) { this.Logger = logger; this.Logs = new List(); - this.ErrorCodesToIgnore = errorCodesToIgnore ?? new List(); } public static LogLevel MapLevel(LSP.DiagnosticSeverity original) diff --git a/src/Core/OperationInfo.cs b/src/Core/OperationInfo.cs index caaeddbd5b..ebb9141f04 100644 --- a/src/Core/OperationInfo.cs +++ b/src/Core/OperationInfo.cs @@ -26,12 +26,14 @@ public class OperationInfo { private Lazy> _params; private Lazy _roslynParams; + private Lazy _returnType; internal OperationInfo(Type roslynType, CallableDeclarationHeader header) { this.Header = header ?? throw new ArgumentNullException(nameof(header)); RoslynType = roslynType; _roslynParams = new Lazy(() => RoslynType?.GetMethod("Run").GetParameters().Skip(1).ToArray()); + _returnType = new Lazy(() => RoslynType?.GetMethod("Run").ReturnType.GenericTypeArguments.Single()); _params = new Lazy>(() => RoslynParameters?.ToDictionary(p => p.Name, p => p.ParameterType.Name)); } @@ -60,6 +62,12 @@ internal OperationInfo(Type roslynType, CallableDeclarationHeader header) [JsonIgnore] public ParameterInfo[] RoslynParameters => _roslynParams.Value; + /// + /// The return type for the underlying compiled .NET Type for this Q# operation + /// + [JsonIgnore] + public Type ReturnType => _returnType.Value; + public override string ToString() => FullName; } diff --git a/src/Core/Resolver/OperationResolver.cs b/src/Core/Resolver/OperationResolver.cs index 8be04f2933..2bf52f5b16 100644 --- a/src/Core/Resolver/OperationResolver.cs +++ b/src/Core/Resolver/OperationResolver.cs @@ -71,11 +71,12 @@ private IEnumerable RelevantAssemblies() /// Symbol names without a dot are resolved to the first symbol /// whose base name matches the given name. /// - public OperationInfo Resolve(string name) + public OperationInfo Resolve(string name) => ResolveFromAssemblies(name, RelevantAssemblies()); + + public static OperationInfo ResolveFromAssemblies(string name, IEnumerable assemblies) { var isQualified = name.Contains('.'); - var relevant = RelevantAssemblies(); - foreach (var operation in relevant.SelectMany(asm => asm.Operations)) + foreach (var operation in assemblies.SelectMany(asm => asm.Operations)) { if (name == (isQualified ? operation.FullName : operation.Header.QualifiedName.Name.Value)) { diff --git a/src/Core/Snippets/ISnippets.cs b/src/Core/Snippets/ISnippets.cs index f8393cb6b3..60e52fb2ea 100644 --- a/src/Core/Snippets/ISnippets.cs +++ b/src/Core/Snippets/ISnippets.cs @@ -55,6 +55,11 @@ public interface ISnippets /// AssemblyInfo AssemblyInfo { get; } + /// + /// The list of currently available snippets. + /// + IEnumerable Items { get; set; } + /// /// Adds or updates a snippet of code. If successful, this updates the AssemblyInfo /// with the new operations found in the Snippet and returns a new Snippet diff --git a/src/Core/Snippets/Snippets.cs b/src/Core/Snippets/Snippets.cs index bf3f0cf189..f6afa05204 100644 --- a/src/Core/Snippets/Snippets.cs +++ b/src/Core/Snippets/Snippets.cs @@ -104,7 +104,7 @@ private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) /// /// The list of currently available snippets. /// - internal IEnumerable Items { get; set; } + public IEnumerable Items { get; set; } /// /// The list of Q# operations available across all snippets. @@ -144,11 +144,7 @@ public Snippet Compile(string code) if (string.IsNullOrWhiteSpace(code)) throw new ArgumentNullException(nameof(code)); var duration = Stopwatch.StartNew(); - var errorCodesToIgnore = new List() - { - QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary, // Ignore any @EntryPoint() attributes found in snippets. - }; - var logger = new QSharpLogger(Logger, errorCodesToIgnore); + var logger = new QSharpLogger(Logger); try { diff --git a/src/Core/Workspace/IWorkspace.cs b/src/Core/Workspace/IWorkspace.cs index 4bc0e806ca..f950f1ca79 100644 --- a/src/Core/Workspace/IWorkspace.cs +++ b/src/Core/Workspace/IWorkspace.cs @@ -64,6 +64,11 @@ public interface IWorkspace /// string Root { get; } + /// + /// Gets the source files to be built for this Workspace. + /// + public IEnumerable SourceFiles { get; } + /// /// The folder where the assembly is permanently saved for cache. /// diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs new file mode 100644 index 0000000000..3aba6eb27a --- /dev/null +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Common; +using Microsoft.Quantum.Simulation.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tests.IQSharp +{ + [TestClass] + public class AzureClientEntryPointTests + { + private IEntryPointGenerator Init(string workspace, IEnumerable? codeSnippets = null) + { + var services = Startup.CreateServiceProvider(workspace); + + if (codeSnippets != null) + { + var snippets = services.GetService(); + snippets.Items = codeSnippets.Select(codeSnippet => new Snippet() { code = codeSnippet }); + } + + return services.GetService(); + } + + [TestMethod] + public async Task FromSnippet() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.HelloQ }); + var entryPoint = entryPointGenerator.Generate("HelloQ", null); + Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + Assert.IsNotNull(job); + } + + [TestMethod] + public async Task FromBrokenSnippet() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.TwoErrors }); + Assert.ThrowsException(() => + entryPointGenerator.Generate("TwoErrors", null)); + } + + [TestMethod] + public async Task FromWorkspace() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" }, { "name", "test" } } ); + Assert.IsNotNull(job); + } + + [TestMethod] + public async Task FromWorkspaceMissingArgument() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + + Assert.ThrowsException(() => + entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" } })); + } + + [TestMethod] + public async Task FromWorkspaceIncorrectArgumentType() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + + Assert.ThrowsException(() => + entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "NaN" }, { "name", "test" } })); + } + + [TestMethod] + public async Task FromBrokenWorkspace() + { + var entryPointGenerator = Init("Workspace.Broken"); + Assert.ThrowsException(() => + entryPointGenerator.Generate("Tests.qss.HelloAgain", null)); + } + + [TestMethod] + public async Task FromSnippetDependsOnWorkspace() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.DependsOnWorkspace }); + var entryPoint = entryPointGenerator.Generate("DependsOnWorkspace", null); + Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + Assert.IsNotNull(job); + } + + [TestMethod] + public async Task InvalidOperationName() + { + var entryPointGenerator = Init("Workspace"); + Assert.ThrowsException(() => + entryPointGenerator.Generate("InvalidOperationName", null)); + } + + [TestMethod] + public async Task InvalidEntryPointOperation() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.InvalidEntryPoint }); + Assert.ThrowsException(() => + entryPointGenerator.Generate("InvalidEntryPoint", null)); + } + } + + public class MockQuantumMachine : IQuantumMachine + { + public string ProviderId => throw new NotImplementedException(); + + public string Target => throw new NotImplementedException(); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input) + => ExecuteAsync(info, input, null as IQuantumMachineSubmissionContext); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext) + => ExecuteAsync(info, input, submissionContext, null as IQuantumMachine.ConfigureJob); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => ExecuteAsync(info, input, submissionContext, null, configureJobCallback); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext) + => ExecuteAsync(info, input, null as IQuantumMachineExecutionContext); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => ExecuteAsync(info, input, executionContext, null as IQuantumMachine.ConfigureJob); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext) + => ExecuteAsync(info, input, submissionContext, executionContext, null); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => throw new NotImplementedException(); + + public Task SubmitAsync(EntryPointInfo info, TInput input) + => SubmitAsync(info, input, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext) + => SubmitAsync(info, input, null, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob); + + public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input) + => throw new NotImplementedException(); + } + + public class MockQuantumMachineJob : IQuantumMachineJob + { + public bool Failed => throw new NotImplementedException(); + + public string Id => throw new NotImplementedException(); + + public bool InProgress => throw new NotImplementedException(); + + public string Status => throw new NotImplementedException(); + + public bool Succeeded => throw new NotImplementedException(); + + public Uri Uri => throw new NotImplementedException(); + + public Task CancelAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Task RefreshAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } +} diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index fbbc880253..a45f1eb6c0 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -85,8 +85,7 @@ public void TestSubmitMagic() { // no arguments var azureClient = new MockAzureClient(); - var operationResolver = new MockOperationResolver(); - var submitMagic = new SubmitMagic(operationResolver, azureClient); + var submitMagic = new SubmitMagic(azureClient); submitMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); @@ -101,8 +100,7 @@ public void TestExecuteMagic() { // no arguments var azureClient = new MockAzureClient(); - var operationResolver = new MockOperationResolver(); - var executeMagic = new ExecuteMagic(operationResolver, azureClient); + var executeMagic = new ExecuteMagic(azureClient); executeMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); @@ -141,19 +139,15 @@ public void TestJobsMagic() [TestMethod] public void TestTargetMagic() { - var workspace = "Workspace"; - var services = Startup.CreateServiceProvider(workspace); - var references = services.GetService(); - // single argument - should set active target var azureClient = new MockAzureClient(); - var targetMagic = new TargetMagic(azureClient, references); + var targetMagic = new TargetMagic(azureClient); targetMagic.Test(targetName); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); // no arguments - should print active target azureClient = new MockAzureClient(); - targetMagic = new TargetMagic(azureClient, references); + targetMagic = new TargetMagic(azureClient); targetMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget); } @@ -182,7 +176,7 @@ public class MockAzureClient : IAzureClient internal List SubmittedJobs = new List(); internal List ExecutedJobs = new List(); - public async Task SetActiveTargetAsync(IChannel channel, IReferences references, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetName) { LastAction = AzureClientAction.SetActiveTarget; ActiveTargetName = targetName; @@ -194,14 +188,14 @@ public async Task GetActiveTargetAsync(IChannel channel) return ActiveTargetName.ToExecutionResult(); } - public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) { LastAction = AzureClientAction.SubmitJob; SubmittedJobs.Add(operationName); return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task ExecuteJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) { LastAction = AzureClientAction.ExecuteJob; ExecutedJobs.Add(operationName); diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index af75ade1b8..2044b2116a 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -31,15 +31,14 @@ public void TestTargets() { var workspace = "Workspace"; var services = Startup.CreateServiceProvider(workspace); - var references = services.GetService(); var azureClient = services.GetService(); // SetActiveTargetAsync with recognized target name, but not yet connected - var result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "ionq.simulator").GetAwaiter().GetResult(); + var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); // SetActiveTargetAsync with unrecognized target name - result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "contoso.qpu").GetAwaiter().GetResult(); + result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); // GetActiveTargetAsync, but not yet connected diff --git a/src/Tests/SNIPPETS.cs b/src/Tests/SNIPPETS.cs index 8aff3f3214..ef29cf6628 100644 --- a/src/Tests/SNIPPETS.cs +++ b/src/Tests/SNIPPETS.cs @@ -207,6 +207,15 @@ operation InvalidFunctor(q: Qubit) : Unit { } "; + public static string InvalidEntryPoint = +@" + /// # Summary + /// This script has an operation that is not valid to be marked as an entry point. + operation InvalidEntryPoint(q : Qubit) : Unit { + H(q); + } +"; + public static string Reverse = @" /// # Summary diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 3edf302854..f6975e9c6c 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,24 +6,24 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2006.207", + "Microsoft.Quantum.Compiler::0.11.2006.403", - "Microsoft.Quantum.CsharpGeneration::0.11.2006.207", - "Microsoft.Quantum.Development.Kit::0.11.2006.207", - "Microsoft.Quantum.Simulators::0.11.2006.207", - "Microsoft.Quantum.Xunit::0.11.2006.207", + "Microsoft.Quantum.CsharpGeneration::0.11.2006.403", + "Microsoft.Quantum.Development.Kit::0.11.2006.403", + "Microsoft.Quantum.Simulators::0.11.2006.403", + "Microsoft.Quantum.Xunit::0.11.2006.403", - "Microsoft.Quantum.Standard::0.11.2006.207", - "Microsoft.Quantum.Chemistry::0.11.2006.207", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.207", - "Microsoft.Quantum.Numerics::0.11.2006.207", + "Microsoft.Quantum.Standard::0.11.2006.403", + "Microsoft.Quantum.Chemistry::0.11.2006.403", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.403", + "Microsoft.Quantum.Numerics::0.11.2006.403", - "Microsoft.Quantum.Katas::0.11.2006.207", + "Microsoft.Quantum.Katas::0.11.2006.403", - "Microsoft.Quantum.Research::0.11.2006.207", + "Microsoft.Quantum.Research::0.11.2006.403", - "Microsoft.Quantum.Providers.IonQ::0.11.2006.207", - "Microsoft.Quantum.Providers.Honeywell::0.11.2006.207", - "Microsoft.Quantum.Providers.QCI::0.11.2006.207", + "Microsoft.Quantum.Providers.IonQ::0.11.2006.403", + "Microsoft.Quantum.Providers.Honeywell::0.11.2006.403", + "Microsoft.Quantum.Providers.QCI::0.11.2006.403", ] }