This repository has been archived by the owner on Aug 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
KataMagic.cs
236 lines (206 loc) · 9.15 KB
/
KataMagic.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Jupyter.Core;
using Microsoft.Quantum.IQSharp;
using Microsoft.Quantum.IQSharp.Jupyter;
using Microsoft.Quantum.IQSharp.Common;
using Microsoft.Quantum.Simulation.Common;
using Microsoft.Quantum.Simulation.Core;
namespace Microsoft.Quantum.Katas
{
public class KataMagic : MagicSymbol
{
/// <summary>
/// IQ# Magic that enables executing the Katas on Jupyter.
/// </summary>
public KataMagic(IOperationResolver resolver, ISnippets snippets, ILogger<KataMagic> logger, IConfigurationSource configurationSource)
{
this.Name = $"%kata";
this.Documentation = new Microsoft.Jupyter.Core.Documentation
{
Summary = "Executes a single test.",
Description = "Executes a single test, and reports whether the test passed successfully.",
Examples = new []
{
"To run a test called `Test`:\n" +
"```\n" +
"In []: %kata T101_StateFlip \n" +
" operation StateFlip (q : Qubit) : Unit is Adj + Ctl {\n" +
" // The Pauli X gate will change the |0⟩ state to the |1⟩ state and vice versa.\n" +
" // Type X(q);\n" +
" // Then run the cell using Ctrl/⌘+Enter.\n" +
"\n" +
" // ...\n" +
" }\n" +
"Out[]: Qubit in invalid state. Expecting: Zero\n" +
" \tExpected:\t0\n"+
" \tActual:\t0.5000000000000002\n" +
" Try again!\n" +
"```\n"
}
};
this.Kind = SymbolKind.Magic;
this.Execute = this.Run;
this.ConfigurationSource = configurationSource;
this.Resolver = resolver;
this.Snippets = snippets;
this.Logger = logger;
this.AllAnswers = new Dictionary<OperationInfo, OperationInfo>();
}
/// <summary>
/// The Resolver lets us find compiled Q# operations from the workspace
/// </summary>
protected IOperationResolver Resolver { get; }
/// <summary>
/// The configuration source used by this magic command to control
/// simulation options (e.g.: dump formatting options).
/// </summary>
public IConfigurationSource ConfigurationSource { get; }
/// <summary>
/// The list of user-defined Q# code snippets from the notebook.
/// </summary>
protected ISnippets Snippets { get; }
protected ILogger<KataMagic> Logger { get; }
protected Dictionary<OperationInfo, OperationInfo> AllAnswers { get; }
/// <summary>
/// What this Magic does when triggered. It will:
/// - find the Test to execute based on the given name,
/// - compile the code after found after the name as the user's answer.
/// - run (simulate) the test and report its result.
/// </summary>
public virtual async Task<ExecutionResult> Run(string input, IChannel channel)
{
channel = channel.WithNewLines();
// Expect exactly two arguments, the name of the Kata and the user's answer (code).
var args = input?.Split(new char[] { ' ', '\n', '\t' }, 2);
if (args == null || args.Length != 2)
{
channel.Stderr("Invalid parameters. Usage: `%kata Test \"Q# operation\"`");
return ExecuteStatus.Error.ToExecutionResult();
}
var name = args[0];
var code = args[1];
var test = FindTest(name);
if (test == null)
{
channel.Stderr($"Invalid test name: {name}");
return ExecuteStatus.Error.ToExecutionResult();
}
var userAnswer = await Compile(code, channel);
if (userAnswer == null) { return ExecuteStatus.Error.ToExecutionResult(); }
return Simulate(test, userAnswer, channel)
? "Success!".ToExecutionResult()
: ExecuteStatus.Error.ToExecutionResult();
}
/// <summary>
/// Compiles the given code. Checks there is only one operation defined in the code,
/// and returns its corresponding OperationInfo
/// </summary>
public virtual async Task<OperationInfo> Compile(string code, IChannel channel)
{
try
{
var result = await Snippets.Compile(code);
foreach (var m in result.warnings) { channel.Stdout(m); }
// Gets the names of all the operations found for this snippet
var opsNames =
result.Elements?
.Where(e => e.IsQsCallable)
.Select(e => e.ToFullName().WithoutNamespace(Microsoft.Quantum.IQSharp.Snippets.SNIPPETS_NAMESPACE))
.OrderBy(o => o)
.ToArray();
if (opsNames.Length > 1)
{
channel.Stdout("Expecting only one Q# operation in code. Using the first one");
}
return Resolver.Resolve(opsNames.First());
}
catch (CompilationErrorsException c)
{
foreach (var m in c.Errors) channel.Stderr(m);
return null;
}
catch (Exception e)
{
Logger?.LogWarning(e, "Unexpected error.");
channel.Stderr(e.Message);
return null;
}
}
/// <summary>
/// Executes the given kata using the provided <c>userAnswer</c> as the actual answer.
/// To do this, it finds another operation with the same name but in the Kata's namespace
/// (by calling `FindSkeltonAnswer`) and replace its implementation with the userAnswer
/// in the simulator.
/// </summary>
public virtual bool Simulate(OperationInfo test, OperationInfo userAnswer, IChannel channel)
{
var skeletonAnswer = FindSkeletonAnswer(test, userAnswer);
if (skeletonAnswer == null)
{
channel.Stderr($"Invalid task: {userAnswer.FullName}");
return false;
}
try
{
var qsim = CreateSimulator();
qsim.DisableExceptionPrinting();
qsim.DisableLogToConsole();
qsim.OnLog += channel.Stdout;
qsim.OnDisplayableDiagnostic += channel.Display;
// Register all solutions to previously executed tasks (including the current one)
foreach (KeyValuePair<OperationInfo, OperationInfo> answer in AllAnswers) {
Logger.LogDebug($"Registering {answer.Key.FullName}");
qsim.Register(answer.Key.RoslynType, answer.Value.RoslynType, typeof(ICallable));
}
var value = test.RunAsync(qsim, null).Result;
if (qsim is IDisposable dis) { dis.Dispose(); }
return true;
}
catch (AggregateException agg)
{
foreach (var e in agg.InnerExceptions) { channel.Stderr(e.Message); }
channel.Stderr($"Try again!");
return false;
}
catch (Exception e)
{
channel.Stderr(e.Message);
channel.Stderr($"Try again!");
return false;
}
}
/// <summary>
/// Creates the instance of the simulator to use to run the Test
/// (for now always CounterSimulator from the same package).
/// </summary>
public virtual SimulatorBase CreateSimulator() =>
new CounterSimulator();
/// <summary>
/// Returns the OperationInfo with the Test to run based on the given name.
/// </summary>
public virtual OperationInfo FindTest(string testName) =>
Resolver.Resolve(testName);
/// <summary>
/// Returns the original shell for the test's answer in the workspace for the given userAnswer.
/// It does this by finding another operation with the same name as the `userAnswer` but in the
/// test's namespace
/// </summary>
public virtual OperationInfo FindSkeletonAnswer(OperationInfo test, OperationInfo userAnswer)
{
var skeletonAnswer = Resolver.Resolve($"{test.Header.QualifiedName.Namespace}.{userAnswer.FullName}");
Logger.LogDebug($"Resolved {userAnswer.FullName} to {skeletonAnswer}");
if (skeletonAnswer != null)
{
// Remember the last user answer for this task
AllAnswers[skeletonAnswer] = userAnswer;
}
return skeletonAnswer;
}
}
}