diff --git a/examples/QIR/Optimization/Hello/Hello.csproj b/examples/QIR/Optimization/Hello/Hello.csproj index 6e084c907e..b66f81f612 100644 --- a/examples/QIR/Optimization/Hello/Hello.csproj +++ b/examples/QIR/Optimization/Hello/Hello.csproj @@ -1,9 +1,34 @@ - + Exe netcoreapp3.1 true + $(MSBuildThisFileDirectory)build + + + + + + + + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/any/native/include + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/osx-x64/native + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/win-x64/native + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/linux-x64/native + $(PkgMicrosoft_Quantum_Simulators)/runtimes/osx-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + $(PkgMicrosoft_Quantum_Simulators)/runtimes/win-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + $(PkgMicrosoft_Quantum_Simulators)/runtimes/linux-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + + + <_QirRuntimeLibFiles Include="$(QirRuntimeLibs)/**/*.*" Exclude="$(QirRuntimeLibs)/**/*.exe" /> + <_QirRuntimeHeaderFiles Include="$(QirRuntimeHeaders)/**/*.hpp" /> + + + + + + diff --git a/examples/QIR/Optimization/Hello/Main.cpp b/examples/QIR/Optimization/Hello/Main.cpp new file mode 100644 index 0000000000..ddfe6b396f --- /dev/null +++ b/examples/QIR/Optimization/Hello/Main.cpp @@ -0,0 +1,15 @@ +#include "QirContext.hpp" +#include "QirRuntime.hpp" +#include "SimFactory.hpp" + +using namespace Microsoft::Quantum; +using namespace std; + +extern "C" void Hello__HelloQ(); + +int main(int argc, char* argv[]){ + unique_ptr sim = CreateFullstateSimulator(); + QirContextScope qirctx(sim.get(), true /*trackAllocatedObjects*/); + Hello__HelloQ(); + return 0; +} diff --git a/examples/QIR/Optimization/README.md b/examples/QIR/Optimization/README.md index 84c7cfcb83..4c193a02e4 100644 --- a/examples/QIR/Optimization/README.md +++ b/examples/QIR/Optimization/README.md @@ -1,7 +1,7 @@ -# QIR Generation and Optimization +# Optimizing QIR Since QIR is a specification of LLVM IR, the IR code can be manipulated via the usual tools provided by LLVM, such as [Clang](https://clang.llvm.org/), the [LLVM optimizer](https://llvm.org/docs/CommandGuide/opt.html), the [LLVM static compiler](https://llvm.org/docs/CommandGuide/llc.html), and the [just-in-time (JIT) compiler](https://llvm.org/docs/CommandGuide/lli.html). -This example is structured as a walk-through of the process covering installation, QIR generation, and running built-in and custom LLVM optimization pipelines. +This example is structured as a walk-through of the process covering installation, QIR generation, running built-in and custom LLVM optimization pipelines, and compiling to an executable. ## Prerequisites @@ -23,7 +23,7 @@ Steps for QDK v0.18.2106 (June 2021): * Install the [.NET Core SDK 3.1](https://dotnet.microsoft.com/download) (NOTE: use `dotnet-sdk-3.1` instead of `dotnet-sdk-5.0` in the Linux guides) * Install the QDK with `dotnet new -i Microsoft.Quantum.ProjectTemplates` -* (Linux only) Ensure the [GNU OpenMP support library](https://gcc.gnu.org/onlinedocs/libgomp/) is installed on your system, e.g. via `sudo apt install libgomp1` +* (**Linux**) Ensure the [GNU OpenMP support library](https://gcc.gnu.org/onlinedocs/libgomp/) is installed on your system, e.g. via `sudo apt install libgomp1` ### Installing Clang @@ -50,7 +50,7 @@ Pre-built binaries/installers: * **macOS** : get `clang+llvm-11.0.0-x86_64-apple-darwin.tar.xz` from the [11.0.0 release](https://github.com/llvm/llvm-project/releases/tag/llvmorg-11.0.0) (11.1.0 not released) -On Linux, if installing via `apt`, the clang/llvm commands will have `-11` attached to their name. +(**Linux**) If installing via `apt`, the clang/llvm commands will have `-11` attached to their name. It's convenient to define aliases for these commands so as not to have to type out the full name every time. If you want to skip this step, substitute `clang`/`clang++`/`opt` with `clang-11`/`clang++-11`/`opt-11` throughout the rest of this document. @@ -106,7 +106,7 @@ namespace Hello { open Microsoft.Quantum.Intrinsic; @EntryPoint() - operation SayHello() : Unit { + operation HelloQ() : Unit { Message("Hello quantum world!"); } } @@ -120,7 +120,7 @@ Q# uses .NET project files to control the compilation process, located in the pr The standard one provided for a standalone Q# application looks as follows: ```xml - + Exe @@ -133,7 +133,7 @@ The standard one provided for a standalone Q# application looks as follows: Enabling QIR generation is a simple matter of adding the `` property to the project file: ```xml - + Exe @@ -147,7 +147,7 @@ Enabling QIR generation is a simple matter of adding the `` prope ### Building the project Build the project by running `dotnet build` from the project root folder (`cd Hello`), or specify the path manually as `dotnet build path/to/Hello.csproj`. -In addition to building the (simulator) executable, the compiler will also create a `qir` folder with an LLVM representation of the program (`Hello.ll`). +Instead of building the (simulator) executable, the compiler will create a `qir` folder with an LLVM representation of the program (`Hello.ll`). For small projects, such as the default hello world program, a lot of the generated QIR code may not actually be required to run the program. The next section describes how to run optimization passes on QIR, which can strip away unnecessary code or perform various other transformations written for LLVM. @@ -155,7 +155,7 @@ The next section describes how to run optimization passes on QIR, which can stri ## Optimizing QIR While Clang is typically used to compile and optimize e.g. C code, it can also be used to manipulate LLVM IR directly. -The command below tells Clang to run a series of optimizations on the generated QIR code `Hello.ll` and output back LLVM IR: +The command below tells Clang to run a series of optimizations on the generated QIR code `Hello.ll` and output back LLVM IR (run from the qir folder): ```shell clang -S qir/Hello.ll -O3 -emit-llvm -o qir/Hello-o3.ll @@ -258,7 +258,7 @@ attributes #0 = { "InteropFriendly" } attributes #1 = { "EntryPoint" } ``` -Note that compared to the output from `-O3`, the function `Hello__SayHello__body` was not inlined, as well as some other small differences such as missing tail calls. +Note that compared to the output from `-O3`, the function `Hello__HelloQ__body` was not inlined, as well as some other small differences such as missing tail calls. Add *function inlining* with the following pass: ```shell @@ -299,3 +299,259 @@ attributes #1 = { "EntryPoint" } ``` Check out the full list of [LLVM passes](https://llvm.org/docs/Passes.html) for other optimizations. + +## Running QIR + +Since QIR code *is* LLVM IR, the usual code generation tools provided by LLVM can be used to produce an executable. +However, in order to handle QIR-specific types and functions, proper linkage of the QIR runtime and simulator libraries is required. + +### Obtaining the QIR runtime & simulator + +The [QIR runtime](https://github.com/microsoft/qsharp-runtime/tree/main/src/Qir/Runtime) is distributed in the form of a NuGet package, from which we will pull the necessary library files. +The same goes for the [full state quantum simulator](https://docs.microsoft.com/azure/quantum/user-guide/machines/full-state-simulator), which the QIR runtime can hook into to simulate the quantum program. +In this section, the project file `Hello.csproj` is modified to generate these library files automatically. + +For convenience, a variable `BuildOutputPath` is defined with the following line added to the top-level `PropertyGroup` section: + +```xml + + Exe + netcoreapp3.1 + true + $(MSBuildThisFileDirectory)build + +``` + +All QIR runtime and simulator dependencies will be copied there. + +Next, the aforementioned NuGet package dependencies must be declared. +One for the runtime and one for the simulator, using the `PackageReference` command: + +```xml + + + + +``` + +The package versions should match the version of the QDK specified at the top of the file, however, the runtime is only available as an alpha version at the moment. +The `GeneratePathProperty` will allow us to directly reference specific files in the packages later on. + +Lastly, a new build target is added called `GetDependencies`: + +```xml + +``` + +The property `AfterTargets` indicates the target is to be run after the regular build stage. + +Inside, we simply copy library and C++ header files from the packages into the build folder with the `Copy` command: + +```xml + + + +``` + +The variables used to specify source files must be defined appropriately for each operating system. +For example, only these definitions would be active on Windows: + +```xml + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/any/native/include + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/win-x64/native + $(PkgMicrosoft_Quantum_Simulators)/runtimes/win-x64/native/Microsoft.Quantum.Simulator.Runtime.dll +``` + +Note the variable `$(PkgMicrosoft_Quantum_Qir_Runtime)` for example is only available because of the `GeneratePathProperty` in the `Microsoft.Quantum.Qir.Runtime` package declaration. + +Since `QirRuntimeHeaders` and `QirRuntimeLibs` only specify directories (whereas `SimulatorRuntime` specifies a single file), we further filter the files to be copied: + +```xml + <_QirRuntimeLibFiles Include="$(QirRuntimeLibs)/**/*.*" Exclude="$(QirRuntimeLibs)/**/*.exe" /> + <_QirRuntimeHeaderFiles Include="$(QirRuntimeHeaders)/**/*.hpp" /> +``` + +Only `.hpp` files from the QIR header directory will be copied, and no `.exe` files from QIR library directory. + +Put together, the new `Hello.csproj` project file should look as follows: + +```xml + + + + Exe + netcoreapp3.1 + true + $(MSBuildThisFileDirectory)build + + + + + + + + + + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/any/native/include + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/osx-x64/native + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/win-x64/native + $(PkgMicrosoft_Quantum_Qir_Runtime)/runtimes/linux-x64/native + $(PkgMicrosoft_Quantum_Simulators)/runtimes/osx-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + $(PkgMicrosoft_Quantum_Simulators)/runtimes/win-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + $(PkgMicrosoft_Quantum_Simulators)/runtimes/linux-x64/native/Microsoft.Quantum.Simulator.Runtime.dll + + + <_QirRuntimeLibFiles Include="$(QirRuntimeLibs)/**/*.*" Exclude="$(QirRuntimeLibs)/**/*.exe" /> + <_QirRuntimeHeaderFiles Include="$(QirRuntimeHeaders)/**/*.hpp" /> + + + + + + + +``` + +Build the project again with `dotnet build` from the project root directory. +You should see the following important files appear in a folder named `build`, among others: + +``` +build +├── Microsoft.Quantum.Qir.QSharp.Core.dll +├── Microsoft.Quantum.Qir.QSharp.Foundation.dll +├── Microsoft.Quantum.Qir.Runtime.dll +├── Microsoft.Quantum.Simulator.Runtime.dll +├── QirContext.hpp +├── QirRuntime.hpp +└── SimFactory.hpp +``` + +### Adding a driver + +Trying to compile the QIR code in `Hello.ll` as is would present some problems, as it's missing a program entry point and the proper setup of the simulator. +A small C++ driver program (`Main.cpp`) will handle the setup and invoke Q# operations or functions directly from the QIR code. + +```cpp +#include "QirContext.hpp" +#include "QirRuntime.hpp" +#include "SimFactory.hpp" + +using namespace Microsoft::Quantum; +using namespace std; + +extern "C" void Hello__HelloQ(); + +int main(int argc, char* argv[]){ + unique_ptr sim = CreateFullstateSimulator(); + QirContextScope qirctx(sim.get(), true /*trackAllocatedObjects*/); + Hello__HelloQ(); + return 0; +} +``` + +The driver consists of the following elements: + +* header files (to interface with the libraries): + + - `QirContext` : used to register the simulator with the QIR runtime + - `QirRuntime` : implements the types and functions defined in the [QIR specification](https://github.com/microsoft/qsharp-language/tree/main/Specifications/QIR) + - `SimFactory` : provides the Q# simulator + +* namespaces : + + - `Microsoft::Quantum` : the QIR context and simulator live here + - `std` : needed for `unique_ptr` + +* external function declarations : + + This is were we declare functions from other compilation units we'd like to invoke. + In our case, that compilation unit is the generated/optimized QIR code. + `extern "C"` is strictly required here in order for the compiler to use the given function name exactly as is ('C' style linkage). + Normally, C++ function names would be transformed during compilation to include namespace and call argument information in the function name, known as [mangling](https://en.wikipedia.org/wiki/Name_mangling). + We can check that the QIR function `Hello_HelloQ` indeed appears in the `Hello.ll` file with that name. + +* simulator invocation: + + Here we create a Q# [full state simulator](https://docs.microsoft.com/azure/quantum/user-guide/machines/full-state-simulator) instance that will run our quantum program and register it with the current context. + Following this, everything is set up to call into Q# functions. + +### Compiling the program + +Multiple tools are available for this step, such as the LLVM static compiler + assembler + linker or the JIT compiler. +Here, Clang is used again, this time to compile and link the `Hello.ll` Q# program with the driver and QIR runtime libraries. + +Invoke the following command on Windows: + +```powershell +clang++ qir/Hello.ll Main.cpp -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -o build/Hello.exe +``` + +On Linux: + +```bash +clang++ qir/Hello.ll Main.cpp -Wl,-rpath=build -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -l'Microsoft.Quantum.Simulator.Runtime' -o build/Hello.exe +``` + +Parameters: + +* `qir/Hello.ll` : source file 1, the QIR execution unit containing the Q# code +* `Main.cpp` : source file 2, the driver containing the program entry point (main) +* `-Wl,-rpath=build` : add the `build` directory as a search path for dynamic libraries at runtime, `Wl,` is used to pass arguments to the linker (Linux only) +* `-Ibuild` : find header files in the `build` directory +* `-Lbuild` : find libraries in the `build` directory +* `-l''` : link dynamic libraries copied earlier +* `-o build/Hello.exe` : path of the generated executable, placed in the build directory so Windows can find the dynamic libraries at runtime + +Running the program should now print the output `Hello quantum world!` to the terminal: + +```shell +./build/Hello.exe +``` + +The same can be done with the optimized QIR code. + +On Windows: + +```powershell +clang++ qir/Hello-dce-inline.ll Main.cpp -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -o build/Hello.exe && ./build/Hello.exe +``` + +On Linux: + +```bash +clang++ qir/Hello-dce-inline.ll Main.cpp -Wl,-rpath=build -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -l'Microsoft.Quantum.Simulator.Runtime' -o build/Hello.exe && ./build/Hello.exe +``` + +As a last example, let's modify the Q# program `Program.qs` with a random bit generator and run through the whole process: + +```csharp +namespace Hello { + + open Microsoft.Quantum.Canon; + open Microsoft.Quantum.Intrinsic; + + @EntryPoint() + operation HelloQ() : Result { + Message("Hello quantum world!"); + + use qb = Qubit(); + H(qb); + Message("Random bit:"); + return M(qb); + } +} +``` + +Steps: + +* build the project `dotnet build` +* optimize the code `clang -S qir/Hello.ll -O3 -emit-llvm -o qir/Hello-o3.ll` +* compile the code on Windows + ```powershell + clang++ qir/Hello-o3.ll Main.cpp -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -o build/Hello.exe + ``` + or Linux + ```bash + clang++ qir/Hello.ll Main.cpp -Wl,-rpath=build -Ibuild -Lbuild -l'Microsoft.Quantum.Qir.Runtime' -l'Microsoft.Quantum.Qir.QSharp.Core' -l'Microsoft.Quantum.Qir.QSharp.Foundation' -l'Microsoft.Quantum.Simulator.Runtime' -o build/Hello.exe + ``` +* simulate the program `./build/Hello.exe` diff --git a/src/QsCompiler/CSharpGeneration/EntryPoint.fs b/src/QsCompiler/CSharpGeneration/EntryPoint.fs index e4b2f97a70..0d2e6c6ea5 100644 --- a/src/QsCompiler/CSharpGeneration/EntryPoint.fs +++ b/src/QsCompiler/CSharpGeneration/EntryPoint.fs @@ -200,7 +200,11 @@ let private qirArguments parameters parseResult = |> qirArgumentValue param.QSharpType |> Option.map (fun value -> ``new`` (ident argumentType) ``(`` [ literal param.Name; value ] ``)``) + // N.B. The parameters sequence is in the right order when it is used here. + // It has to be reversed here because the fold changes the order of the expression syntax. + // This is a problem because the API for QIR submission expects the list of arguments in order. parameters + |> Seq.rev |> Seq.fold (fun state param -> Option.map2 (fun xs x -> x :: xs) state (argument param)) (Some []) |> Option.map (fun args -> ident listType <.> (sprintf "Create<%s>" argumentType |> ident, args)) diff --git a/src/QsCompiler/QirGeneration/Context.cs b/src/QsCompiler/QirGeneration/Context.cs index dfd84b916d..109bf12288 100644 --- a/src/QsCompiler/QirGeneration/Context.cs +++ b/src/QsCompiler/QirGeneration/Context.cs @@ -36,7 +36,6 @@ public sealed class GenerationContext : IDisposable static GenerationContext() { LibContext = Library.InitializeLLVM(); - LibContext.RegisterTarget(CodeGenTarget.Native); } #region Member variables diff --git a/src/QsCompiler/SyntaxProcessor/TypeInference/InferenceContext.fs b/src/QsCompiler/SyntaxProcessor/TypeInference/InferenceContext.fs index 5f455eefa9..2dc06b171f 100644 --- a/src/QsCompiler/SyntaxProcessor/TypeInference/InferenceContext.fs +++ b/src/QsCompiler/SyntaxProcessor/TypeInference/InferenceContext.fs @@ -367,9 +367,12 @@ type InferenceContext(symbolTracker: SymbolTracker) = |> rememberErrors (resolvedType :: Constraint.types typeConstraint) member internal context.Resolve resolvedType = - let resolveWithRange type' = - let type' = context.Resolve type' - type' |> ResolvedType.withRange (type'.Range |> TypeRange.orElse resolvedType.Range) + let resolveWithRange type_ = + let resolvedType' = context.Resolve type_ + + // Prefer the original type range since it may be closer to the source of an error, but otherwise fall back + // to the newly resolved type range. + resolvedType' |> ResolvedType.withRange (resolvedType.Range |> TypeRange.orElse resolvedType'.Range) match resolvedType.Resolution with | TypeParameter param -> diff --git a/src/QsCompiler/Tests.Compiler/ClassicalControlTests.fs b/src/QsCompiler/Tests.Compiler/ClassicalControlTests.fs index 16c34d81ed..ae943dfac6 100644 --- a/src/QsCompiler/Tests.Compiler/ClassicalControlTests.fs +++ b/src/QsCompiler/Tests.Compiler/ClassicalControlTests.fs @@ -1421,3 +1421,325 @@ type ClassicalControlTests() = IsApplyIfElseArgsMatch args "r" SubOp1 NoOp |> (fun (x, _, _, _, _) -> Assert.True(x, "ApplyIfElse did not have the correct arguments")) + + [] + [] + member this.``Don't Lift Classical Conditions``() = + CompileClassicalControlTest 41 |> ignore + + [] + [] + member this.``Mutables with Nesting Lift Both``() = + let result = CompileClassicalControlTest 42 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + let generated = GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + + Assert.True(2 = Seq.length generated) // Should already be asserted by the signature check + + let innerContentCheck call = + let lines = call |> GetBodyFromCallable |> GetLinesFromSpecialization + (List.ofArray lines) = [ "mutable x = 0;"; "set x = 1;" ] + + let (inner, outer) = + match innerContentCheck (Seq.head generated) with + | true -> (Seq.head generated, Seq.item 1 generated) + | false -> (Seq.item 1 generated, Seq.head generated) + + Assert.True(innerContentCheck inner) + + // Make sure original calls outer generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[1] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "r" outer.FullName + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + // Make sure outer calls inner generated + let lines = outer |> GetBodyFromCallable |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfOne.FullName.Namespace BuiltIn.ApplyIfOne.FullName.Name lines.[0] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" outer.FullName QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "r" inner.FullName + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + [] + [] + member this.``Mutables with Nesting Lift Outer``() = + let result = CompileClassicalControlTest 43 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[1] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "r" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + [] + [] + member this.``Mutables with Nesting Lift Neither``() = + CompileClassicalControlTest 44 |> ignore + + [] + [] + member this.``Mutables with Classic Nesting Lift Inner``() = + let result = CompileClassicalControlTest 45 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[4] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + // Make sure the classical condition is present + Assert.True(lines.[3] = " if x < 1 {", "The classical condition is missing after transformation.") + + [] + [] + member this.``Mutables with Classic Nesting Lift Outer``() = + let result = CompileClassicalControlTest 46 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[3] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + // Make sure the classical condition is present + let lines = generated |> GetLinesFromSpecialization + Assert.True(lines.[1] = "if x < 1 {", "The classical condition is missing after transformation.") + + [] + [] + member this.``Mutables with Classic Nesting Lift Outer With More Classic``() = + let result = CompileClassicalControlTest 47 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[3] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + // Make sure the classical condition is present + let lines = generated |> GetLinesFromSpecialization + Assert.True(lines.[1] = "if x < 1 {", "The classical condition is missing after transformation.") + Assert.True(lines.[2] = " if x < 2 {", "The classical condition is missing after transformation.") + + [] + [] + member this.``Mutables with Classic Nesting Lift Middle``() = + let result = CompileClassicalControlTest 48 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[4] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + + // Make sure the classical condition is present + Assert.True(lines.[3] = " if x < 1 {", "The classical condition is missing after transformation.") + let lines = generated |> GetLinesFromSpecialization + Assert.True(lines.[1] = "if x < 2 {", "The classical condition is missing after transformation.") + Assert.True(lines.[2] = " if x < 3 {", "The classical condition is missing after transformation.") + + [] + [] + member this.``Nested Invalid Lifting``() = + CompileClassicalControlTest 49 |> ignore + + [] + [] + member this.``Mutables with Classic Nesting Elif``() = + let result = CompileClassicalControlTest 50 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + let generated = GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + + Assert.True(3 = Seq.length generated) // Should already be asserted by the signature check + + let ifBlockContentCheck call = + let lines = call |> GetBodyFromCallable |> GetLinesFromSpecialization + lines.[1] = "if x < 2 {" && lines.[2] = " if x < 3 {" + + let elifBlockContentCheck call = + let lines = call |> GetBodyFromCallable |> GetLinesFromSpecialization + lines.[1] = "if x < 4 {" && lines.[2] = " if x < 5 {" + + let elseBlockContentCheck call = + let lines = call |> GetBodyFromCallable |> GetLinesFromSpecialization + lines.[1] = "if Microsoft.Quantum.Testing.General.M(q) == Zero {" && lines.[2] = " if x < 6 {" + + let ifBlock = Seq.find ifBlockContentCheck generated + let elifBlock = Seq.find elifBlockContentCheck generated + let elseBlock = Seq.find elseBlockContentCheck generated + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization + + Assert.True(lines.[3] = " if x < 1 {", "The classical condition is missing after transformation.") + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[4] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" ifBlock.FullName + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + Assert.True(lines.[6] = " else {", "The else condition is missing after transformation.") + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfElseR.FullName.Namespace BuiltIn.ApplyIfElseR.FullName.Name lines.[7] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _, _, _) = + IsApplyIfElseArgsMatch args "Microsoft.Quantum.Testing.General.M(q)" elifBlock.FullName elseBlock.FullName + + Assert.True(success, sprintf "ApplyIfElseR did not have the correct arguments") + + [] + [] + member this.``Mutables with Classic Nesting Elif Lift First``() = + let result = CompileClassicalControlTest 51 + let original = GetCallableWithName result Signatures.ClassicalControlNS "Foo" |> GetBodyFromCallable + + let generated = + GetCallablesWithSuffix result Signatures.ClassicalControlNS "_Foo" + |> (fun x -> + Assert.True(1 = Seq.length x) + Seq.item 0 x |> GetBodyFromCallable) + + let TrimWhitespaceFromLines (lines: string []) = lines |> Array.map (fun s -> s.Trim()) + + // Make sure original calls generated + let lines = original |> GetLinesFromSpecialization |> TrimWhitespaceFromLines + + Assert.True(lines.[3] = "if x < 1 {", "The classical condition is missing after transformation.") + + let (success, _, args) = + CheckIfLineIsCall BuiltIn.ApplyIfZero.FullName.Namespace BuiltIn.ApplyIfZero.FullName.Name lines.[4] + + Assert.True( + success, + sprintf "Callable %O(%A) did not have expected content" original.Parent QsSpecializationKind.QsBody + ) + + let (success, _, _) = IsApplyIfArgMatch args "Microsoft.Quantum.Testing.General.M(q)" generated.Parent + Assert.True(success, sprintf "ApplyIfZero did not have the correct arguments") + Assert.True(lines.[6] = "else {", "The else condition is missing after transformation.") + + Assert.True( + lines.[7] = "if Microsoft.Quantum.Testing.General.M(q) == Zero {", + "The quantum condition is missing after transformation." + ) + + Assert.True(lines.[8] = "if x < 4 {", "The classical condition is missing after transformation.") + Assert.True(lines.[9] = "if x < 5 {", "The classical condition is missing after transformation.") + Assert.True(lines.[14] = "else {", "The else condition is missing after transformation.") + + Assert.True( + lines.[16] = "if Microsoft.Quantum.Testing.General.M(q) == Zero {", + "The quantum condition is missing after transformation." + ) + + Assert.True(lines.[17] = "if x < 6 {", "The classical condition is missing after transformation.") + + let lines = generated |> GetLinesFromSpecialization |> TrimWhitespaceFromLines + Assert.True(lines.[1] = "if x < 2 {", "The classical condition is missing after transformation.") + Assert.True(lines.[2] = "if x < 3 {", "The classical condition is missing after transformation.") + + [] + [] + member this.``NOT Condition Remembers Known Symbols``() = + CompileClassicalControlTest 52 |> ignore diff --git a/src/QsCompiler/Tests.Compiler/TestCases/ClassicalControl.qs b/src/QsCompiler/Tests.Compiler/TestCases/ClassicalControl.qs index f2991156d5..3ec633bf00 100644 --- a/src/QsCompiler/Tests.Compiler/TestCases/ClassicalControl.qs +++ b/src/QsCompiler/Tests.Compiler/TestCases/ClassicalControl.qs @@ -11,6 +11,19 @@ namespace SubOps { operation SubOpCA3() : Unit is Ctl + Adj { } } +namespace Microsoft.Quantum.Testing.General { + operation Unitary (q : Qubit) : Unit { + body intrinsic; + adjoint auto; + controlled auto; + controlled adjoint auto; + } + + operation M (q : Qubit) : Result { + body intrinsic; + } +} + // ================================= // Basic Lift @@ -1084,3 +1097,274 @@ namespace Microsoft.Quantum.Testing.ClassicalControl { } } } + +// ================================= + +// Don't Lift Classical Conditions +namespace Microsoft.Quantum.Testing.ClassicalControl { + open SubOps; + + operation Foo() : Unit { + let x = 0; + + if x == 1 { + let y = 0; + SubOp1(); + } + + if x == 2 { + if x == 3 { + let y = 0; + SubOp1(); + } + } + else { + let y = 0; + SubOp1(); + } + } +} + +// ================================= + +// Mutables with Nesting Lift Both +namespace Microsoft.Quantum.Testing.ClassicalControl { + operation Foo() : Unit { + let r = Zero; + + if r == Zero { + if r == One { + mutable x = 0; + set x = 1; + } + } + } +} + +// ================================= + +// Mutables with Nesting Lift Outer +namespace Microsoft.Quantum.Testing.ClassicalControl { + operation Foo() : Unit { + let r = Zero; + + if r == Zero { + mutable x = 0; + if r == One { + set x = 1; + } + } + } +} + +// ================================= + +// Mutables with Nesting Lift Neither +namespace Microsoft.Quantum.Testing.ClassicalControl { + operation Foo() : Unit { + let r = Zero; + + mutable x = 0; + if r == Zero { + if r == One { + set x = 1; + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Lift Inner +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + let x = 0; + + if x < 1 { + if M(q) == Zero { + mutable y = 0; + set y = 1; + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Lift Outer +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + let x = 0; + + if M(q) == Zero { + mutable y = 0; + if x < 1 { + set y = 1; + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Lift Outer With More Classic +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + let x = 0; + + if M(q) == Zero { + mutable y = 0; + if x < 1 { + if x < 2 { + set y = 1; + } + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Lift Middle +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + let x = 0; + + if x < 1 { + if M(q) == Zero { + mutable y = 0; + if x < 2 { + if x < 3 { + set y = 1; + } + } + } + } + } +} + +// ================================= + +// Nested Invalid Lifting +namespace Microsoft.Quantum.Testing.ClassicalControl { + operation Foo() : Unit { + let r = Zero; + + if r == Zero { + if r == One { + return (); + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Elif +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + let x = 0; + + if x < 1 { + if M(q) == Zero { + mutable y = 0; + if x < 2 { + if x < 3 { + set y = 1; + } + } + } + } + elif M(q) == Zero { + mutable y = 0; + if x < 4 { + if x < 5 { + set y = 2; + } + } + } + else { + mutable y = 0; + if M(q) == Zero { + if x < 6 { + set y = 3; + } + } + } + } +} + +// ================================= + +// Mutables with Classic Nesting Elif Lift First +namespace Microsoft.Quantum.Testing.ClassicalControl { + open Microsoft.Quantum.Testing.General; + + operation Foo() : Unit { + use q = Qubit(); + Unitary(q); + mutable x = 0; + + if x < 1 { + if M(q) == Zero { + mutable y = 0; + if x < 2 { + if x < 3 { + set y = 1; + } + } + } + } + elif M(q) == Zero { + if x < 4 { + if x < 5 { + set x = 2; + } + } + } + else { + mutable y = 0; + if M(q) == Zero { + if x < 6 { + set y = 3; + } + } + } + } +} + +// ================================= + +// NOT Condition Remembers Known Symbols +namespace Microsoft.Quantum.Testing.ClassicalControl { + open SubOps; + + operation Foo() : Unit { + let r1 = Zero; + let r2 = Zero; + if (not (r1 == Zero and r2 == Zero)) { + SubOp1(); + SubOp2(); + } + } +} diff --git a/src/QsCompiler/Tests.Compiler/TestUtils/Signatures.fs b/src/QsCompiler/Tests.Compiler/TestUtils/Signatures.fs index 4b1d747d9d..4e4fb51645 100644 --- a/src/QsCompiler/Tests.Compiler/TestUtils/Signatures.fs +++ b/src/QsCompiler/Tests.Compiler/TestUtils/Signatures.fs @@ -535,6 +535,71 @@ let public ClassicalControlSignatures = |]) // One-sided NOT condition (_DefaultTypes, [| ClassicalControlNS, "Foo", [||], "Unit" |]) + // Don't Lift Classical Conditions + (_DefaultTypes, [| ClassicalControlNS, "Foo", [||], "Unit" |]) + // Mutables with Nesting Lift Both + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Result" |], "Unit" + ClassicalControlNS, "_Foo", [| "Result" |], "Unit" + |]) + // Mutables with Nesting Lift Outer + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Result" |], "Unit" + |]) + // Mutables with Nesting Lift Neither + (_DefaultTypes, [| ClassicalControlNS, "Foo", [||], "Unit" |]) + // Mutables with Classic Nesting Lift Inner + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // Mutables with Classic Nesting Lift Outer + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // Mutables with Classic Nesting Lift Outer With More Classic + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // Mutables with Classic Nesting Lift Middle + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // Nested Invalid Lifting + (_DefaultTypes, [| ClassicalControlNS, "Foo", [||], "Unit" |]) + // Mutables with Classic Nesting Elif + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // Mutables with Classic Nesting Elif Lift First + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Int"; "Qubit" |], "Unit" + |]) + // NOT Condition Remembers Known Symbols + (_DefaultTypes, + [| + ClassicalControlNS, "Foo", [||], "Unit" + ClassicalControlNS, "_Foo", [| "Result"; "Result" |], "Unit" + ClassicalControlNS, "_Foo", [| "Result"; "Result" |], "Unit" + ClassicalControlNS, "_Foo", [| "Result"; "Result" |], "Unit" + |]) |] |> _MakeSignatures diff --git a/src/QsCompiler/Transformations/ClassicallyControlled.cs b/src/QsCompiler/Transformations/ClassicallyControlled.cs index e1831be55e..dc97e281d7 100644 --- a/src/QsCompiler/Transformations/ClassicallyControlled.cs +++ b/src/QsCompiler/Transformations/ClassicallyControlled.cs @@ -163,6 +163,11 @@ public StatementTransformation(SyntaxTreeTransformation parent) } } + /// + /// Converts conditional statements whose top-most condition is a NOT. + /// Creates a blank `else` clause if there is no `else` clause already, then + /// swaps the `else` and `if` clauses while removing the NOT from the condition. + /// private (bool, QsConditionalStatement) ProcessNOT(QsConditionalStatement conditionStatement) { // This method expects elif blocks to have been abstracted out @@ -185,10 +190,10 @@ public StatementTransformation(SyntaxTreeTransformation parent) { var emptyScope = new QsScope( ImmutableArray.Empty, - LocalDeclarations.Empty); + block.Body.KnownSymbols); var newConditionalBlock = new QsPositionedBlock( emptyScope, - QsNullable.Null, + block.Location, QsComments.Empty); return (true, new QsConditionalStatement( ImmutableArray.Create(Tuple.Create(notCondition.Item, newConditionalBlock)), @@ -620,19 +625,19 @@ private QsStatement CreateControlStatement(QsStatement statement, TypedExpressio /// private QsStatement ConvertConditionalToControlCall(QsStatement statement) { - var condition = this.IsConditionWithSingleBlock(statement); + var condition = IsConditionWithSingleBlock(statement); if (condition.HasValue) { - if (this.IsConditionedOnResultLiteralExpression(condition.Value.Condition, out var literal, out var conditionExpression)) + if (IsConditionedOnResultLiteralExpression(condition.Value.Condition, out var literal, out var conditionExpression)) { return this.CreateControlStatement(statement, this.CreateApplyIfExpression(literal, conditionExpression, condition.Value.Body, condition.Value.Default)); } - else if (this.IsConditionedOnResultEqualityExpression(condition.Value.Condition, out var lhsConditionExpression, out var rhsConditionExpression)) + else if (IsConditionedOnResultEqualityExpression(condition.Value.Condition, out var lhsConditionExpression, out var rhsConditionExpression)) { return this.CreateControlStatement(statement, this.CreateApplyConditionallyExpression(lhsConditionExpression, rhsConditionExpression, condition.Value.Body, condition.Value.Default)); } - else if (this.IsConditionedOnResultInequalityExpression(condition.Value.Condition, out lhsConditionExpression, out rhsConditionExpression)) + else if (IsConditionedOnResultInequalityExpression(condition.Value.Condition, out lhsConditionExpression, out rhsConditionExpression)) { // The scope arguments are reversed to account for the negation of the NEQ return this.CreateControlStatement(statement, this.CreateApplyConditionallyExpression(lhsConditionExpression, rhsConditionExpression, condition.Value.Default, condition.Value.Body)); @@ -655,7 +660,7 @@ private QsStatement ConvertConditionalToControlCall(QsStatement statement) /// If it is, returns the condition, the body of the conditional block, and, optionally, the body of the /// default block, otherwise returns null. If there is no default block, the last value of the return tuple will be null. /// - private (TypedExpression Condition, QsScope Body, QsScope? Default)? IsConditionWithSingleBlock(QsStatement statement) + private static (TypedExpression Condition, QsScope Body, QsScope? Default)? IsConditionWithSingleBlock(QsStatement statement) { if (statement.Statement is QsStatementKind.QsConditionalStatement condition && condition.Item.ConditionalBlocks.Length == 1) { @@ -671,7 +676,7 @@ private QsStatement ConvertConditionalToControlCall(QsStatement statement) /// expression in the (in)equality, otherwise returns false with nulls. If it is an /// inequality, the returned result value will be the opposite of the result literal found. /// - private bool IsConditionedOnResultLiteralExpression( + private static bool IsConditionedOnResultLiteralExpression( TypedExpression expression, [NotNullWhen(true)] out QsResult? literal, [NotNullWhen(true)] out TypedExpression? conditionExpression) @@ -719,7 +724,7 @@ private bool IsConditionedOnResultLiteralExpression( /// Checks if the expression is an equality comparison between two Result-typed expressions. /// If it is, returns true along with the two expressions, otherwise returns false with nulls. /// - private bool IsConditionedOnResultEqualityExpression( + private static bool IsConditionedOnResultEqualityExpression( TypedExpression expression, [NotNullWhen(true)] out TypedExpression? lhs, [NotNullWhen(true)] out TypedExpression? rhs) @@ -743,7 +748,7 @@ private bool IsConditionedOnResultEqualityExpression( /// Checks if the expression is an inequality comparison between two Result-typed expressions. /// If it is, returns true along with the two expressions, otherwise returns false with nulls. /// - private bool IsConditionedOnResultInequalityExpression( + private static bool IsConditionedOnResultInequalityExpression( TypedExpression expression, [NotNullWhen(true)] out TypedExpression? lhs, [NotNullWhen(true)] out TypedExpression? rhs) @@ -845,6 +850,18 @@ public LiftContent() this.StatementKinds = new StatementKindTransformation(this); } + private static bool IsConditionValidForLifting(QsConditionalStatement conditionStatement) + { + var condition = conditionStatement.ConditionalBlocks.Single().Item1; + return + (condition.Expression is ExpressionKind.EQ eq + && eq.Item1.ResolvedType.Resolution == ResolvedTypeKind.Result + && eq.Item2.ResolvedType.Resolution == ResolvedTypeKind.Result) + || (condition.Expression is ExpressionKind.NEQ neq + && neq.Item1.ResolvedType.Resolution == ResolvedTypeKind.Result + && neq.Item2.ResolvedType.Resolution == ResolvedTypeKind.Result); + } + private new class StatementKindTransformation : ContentLifting.LiftContent.StatementKindTransformation { public StatementKindTransformation(SyntaxTreeTransformation parent) @@ -868,6 +885,13 @@ private bool IsScopeSingleCall(QsScope contents) public override QsStatementKind OnConditionalStatement(QsConditionalStatement stm) { + // If the conditional is classical, don't lift it. + if (!IsConditionValidForLifting(stm)) + { + // But still check nested conditionals. + return base.OnConditionalStatement(stm); + } + var contextIsConditionLiftable = this.SharedState.IsConditionLiftable; this.SharedState.IsConditionLiftable = true; @@ -883,9 +907,6 @@ public override QsStatementKind OnConditionalStatement(QsConditionalStatement st var (expr, block) = this.OnPositionedBlock(QsNullable.NewValue(conditionBlock.Item1), conditionBlock.Item2); - // ToDo: Reduce the number of unnecessary generated operations by generalizing - // the condition logic for the conversion and using that condition here - // var (isExprCondition, _, _) = IsConditionedOnResultLiteralExpression(expr.Item); if (block.Body.Statements.Length == 0) { // This is an empty scope, so it can just be treated as a call to NoOp. diff --git a/src/QsFmt/App.Tests/Tests.fs b/src/QsFmt/App.Tests/Tests.fs index b34ed90c87..f216fdbd77 100644 --- a/src/QsFmt/App.Tests/Tests.fs +++ b/src/QsFmt/App.Tests/Tests.fs @@ -90,7 +90,8 @@ let ``Formats file`` path output = ) [] -[] +let ``Mixed newlines`` = + "namespace Foo {}\n +namespace Bar {}\r\n" + [] let ``Function with one parameter`` = """namespace Foo { diff --git a/src/QsFmt/Formatter/Formatter.fs b/src/QsFmt/Formatter/Formatter.fs index a85fe61220..694c1ef5cb 100644 --- a/src/QsFmt/Formatter/Formatter.fs +++ b/src/QsFmt/Formatter/Formatter.fs @@ -33,14 +33,28 @@ let parse (source: string) = [] let format source = - parse source - |> Result.map ( - curry collapsedSpaces.Document () - >> curry operatorSpacing.Document () - >> curry newLines.Document () - >> curry indentation.Document 0 - >> printer.Document - ) + let formatDocument document = + let unparsed = printer.Document document + + // Test whether there is data loss during parsing and unparsing + if unparsed = source then + // The actual format process + document + |> curry collapsedSpaces.Document () + |> curry operatorSpacing.Document () + |> curry newLines.Document () + |> curry indentation.Document 0 + |> printer.Document + // Report error if the unparsing result does not match the original source + else + failwith ( + "The formatter's syntax tree is inconsistent with the input source code or unparsed wrongly. " + + "Please let us know by filing a new issue in https://github.com/microsoft/qsharp-compiler/issues/new/choose." + + "The unparsed code is: \n" + + unparsed + ) + + parse source |> Result.map formatDocument [] let identity source = diff --git a/src/QsFmt/Formatter/Printer.fs b/src/QsFmt/Formatter/Printer.fs index f5bd3a2988..1a0345311f 100644 --- a/src/QsFmt/Formatter/Printer.fs +++ b/src/QsFmt/Formatter/Printer.fs @@ -5,7 +5,6 @@ module Microsoft.Quantum.QsFmt.Formatter.Printer open Microsoft.Quantum.QsFmt.Formatter.SyntaxTree open Microsoft.Quantum.QsFmt.Formatter.SyntaxTree.Trivia -open System /// /// Prints a node to a string. @@ -13,7 +12,7 @@ open System let printTrivia = function | Whitespace ws -> ws - | NewLine -> Environment.NewLine + | NewLine nl -> nl | Comment comment -> comment /// diff --git a/src/QsFmt/Formatter/Rules.fs b/src/QsFmt/Formatter/Rules.fs index 7b0e3a3702..0a28258da3 100644 --- a/src/QsFmt/Formatter/Rules.fs +++ b/src/QsFmt/Formatter/Rules.fs @@ -24,7 +24,7 @@ let collectWithAdjacent = /// let collapseTriviaSpaces previous trivia _ = match previous, trivia with - | Some NewLine, Whitespace _ -> [ trivia ] + | Some (NewLine _), Whitespace _ -> [ trivia ] | _, Whitespace _ -> [ collapseSpaces trivia ] | _ -> [ trivia ] @@ -47,9 +47,9 @@ let operatorSpacing = let indentPrefix level = let indentTrivia previous trivia after = match previous, trivia, after with - | Some NewLine, Whitespace _, _ -> [ spaces (4 * level) ] - | _, NewLine, Some (Comment _) - | _, NewLine, None -> [ newLine; spaces (4 * level) ] + | Some (NewLine _), Whitespace _, _ -> [ spaces (4 * level) ] + | _, NewLine _, Some (Comment _) + | _, NewLine _, None -> [ trivia; spaces (4 * level) ] | _ -> [ trivia ] collectWithAdjacent indentTrivia @@ -79,7 +79,7 @@ let indentation = /// Prepends the with a new line node if it does not already contain one. /// let ensureNewLine prefix = - if List.contains newLine prefix then prefix else newLine :: prefix + if List.exists isNewLine prefix then prefix else newLine :: prefix let newLines = { new Rewriter<_>() with diff --git a/src/QsFmt/Formatter/SyntaxTree/Node.fs b/src/QsFmt/Formatter/SyntaxTree/Node.fs index 7d6a13f28c..2257ff2db7 100644 --- a/src/QsFmt/Formatter/SyntaxTree/Node.fs +++ b/src/QsFmt/Formatter/SyntaxTree/Node.fs @@ -4,13 +4,14 @@ namespace Microsoft.Quantum.QsFmt.Formatter.SyntaxTree open System.Text.RegularExpressions +open System type Trivia = /// A contiguous region of whitespace. | Whitespace of string /// A new line character. - | NewLine + | NewLine of string /// A comment. | Comment of string @@ -19,20 +20,25 @@ module Trivia = let (|Whitespace|NewLine|Comment|) = function | Whitespace ws -> Whitespace ws - | NewLine -> NewLine + | NewLine nl -> NewLine nl | Comment comment -> Comment comment let spaces count = String.replicate count " " |> Whitespace - let newLine = NewLine + let newLine = NewLine Environment.NewLine + + let isNewLine trivia = + match trivia with + | NewLine _ -> true + | _ -> false let collapseSpaces = let replace str = Regex.Replace(str, "\s+", " ") function | Whitespace ws -> replace ws |> Whitespace - | NewLine -> NewLine + | NewLine nl -> NewLine nl | Comment comment -> Comment comment /// @@ -47,9 +53,9 @@ module Trivia = let rec ofString = function | "" -> [] - | Prefix "\r\n" (_, rest) - | Prefix "\r" (_, rest) - | Prefix "\n" (_, rest) -> NewLine :: ofString rest + | Prefix "\r\n" (_, rest) -> NewLine "\r\n" :: ofString rest + | Prefix "\r" (_, rest) -> NewLine "\r" :: ofString rest + | Prefix "\n" (_, rest) -> NewLine "\n" :: ofString rest | Prefix "\s+" (result, rest) -> Whitespace result :: ofString rest | Prefix "//[^\r\n]*" (result, rest) -> Comment result :: ofString rest | _ -> diff --git a/src/QsFmt/Formatter/SyntaxTree/Node.fsi b/src/QsFmt/Formatter/SyntaxTree/Node.fsi index 0c5cadb107..10e8e41724 100644 --- a/src/QsFmt/Formatter/SyntaxTree/Node.fsi +++ b/src/QsFmt/Formatter/SyntaxTree/Node.fsi @@ -25,7 +25,7 @@ module internal Trivia = /// /// /// - val (|Whitespace|NewLine|Comment|): Trivia -> Choice + val (|Whitespace|NewLine|Comment|): Trivia -> Choice /// /// A node containing number of space characters. @@ -33,10 +33,13 @@ module internal Trivia = val spaces: count:int -> Trivia /// - /// The new line node. + /// The new line node containing the default new line character for the current platform.. /// val newLine: Trivia + /// Determine whether a Trivia is a NewLine + val isNewLine: Trivia -> bool + /// Replaces each occurrence of more than one whitespace character in a row with a single space. val collapseSpaces: (Trivia -> Trivia) diff --git a/src/QsFmt/SPECIFICATION.md b/src/QsFmt/SPECIFICATION.md new file mode 100644 index 0000000000..6f0b342f73 --- /dev/null +++ b/src/QsFmt/SPECIFICATION.md @@ -0,0 +1,68 @@ +# Q# Formatter Design Specification Document + +QsFmt is a source code formatter for Q#. + +## Where to get the formatter + +The `Microsoft.Quantum.QSharp.Formatter` NuGet package will be distributed with the rest of Q# on NuGet.org. +As a dotnet tool, it is installed with the command `dotnet tool install Microsoft.Quantum.QSharp.Formatter`. + +## How to use the command line formatter + +After installing the tool the command `qsfmt` is used to run the formatter. +There are two commands supported for the tool: 'format' and 'update'. +The 'format' command runs the tool as a formatter for Q# code, running only those transformation rules that update whitespace and affect indentation. The underlying code will not be changed. +The 'update' command allows the tool to be used as a means of updating old Q# code, replacing deprecated syntax with newer supported syntax. These transformation rules change the actual code given to them, and may occasionally fail. +These two commands work separately and can't be run together. If the user wishes to both format and update their code, they would need to run the tool twice, first with 'update' and then with 'format'. +Both of these commands should be idempotent, meaning that iterated uses of the commands will not continue to have effects on the inputs. + +The tool will initially only support the 'update' command, as the formatting functionality is still being worked on. The 'format' command will be unavailable until it is supported. + +## Input and Output +Input files to the formatter will be the last argument(s) to the tool. +At least one input argument is expected. If no inputs are given, usage information is printed. +Inputs can be given as one to many paths to directories, files, or any combination. In directories specified, all files with the '.qs' extension found will be taken as input. +Basic wild cards may be used in the paths. +The `--recurse` option can be specified to process the input directories recursively for input files. You can specify this for all input directories, not per-directory. +The `--backup` option can be specified to create backup files for all input files with the original content in them. + +The output of the formatter is to overwrite the input files that it processes. + +## Rules that are run + +The tool will parse the input into a concrete syntax tree using ANTLR. This tree will then have rules applied to it in the form of transformations on the tree. +The transformations are separated into two groups: transformations run by the 'format' command, and transformations run by the 'update' command. +Listed here are some of the transformations we intend to use. + +### Formatting Transformations + + - Collapsed Spaces - removes duplicate spaces so that there is only a single space where spaces are used + - Operator Spacing - ensure that operators have the appropriate spaces in them + - New Lines - removes excessive new lines + - Indentation - ensures that there are appropriate indentation for the text + +### Updating Transformations + +Updating transformations remove outdated syntax that is deprecated and will be no longer supported in the future. + +Some syntaxes that can be updated are: + - Array Syntax - [proposal](https://github.com/microsoft/qsharp-language/blob/main/Approved/2-enhanced-array-literals.md) + - Using and Borrowing Syntax - [proposal](https://github.com/microsoft/qsharp-language/blob/main/Approved/1-implicitly-scoped-qubit-allocation.md) + - Parentheses in For Loop Syntax - Removes deprecated parentheses around for-loop range expressions. + - Boolean Operator Syntax - Replaces deprecated boolean operators (`&&`, `||`, `!`) for keywords (`and`, `or`, `not`). + - Unit Syntax - Replaces deprecated unit syntax `()` for `Unit`. + - Body and Adjoint Functor Argument Syntax - When specifying `body` and `adjoint` functors on an operation, replaces the deprecated empty argument `()` with the required `(...)`. + +## How errors are handled + +If an internal unhandled exception is thrown by the tool, that will be surfaced to the user and is a bug with the tool that should be filed and addressed. +Exceptions like these will prevent any output from being written on the file that caused the exception. The tool will try to recover from the exception to process other input files. + +Parsing errors may occur if the formatter is given text that is not proper Q# code. In this case ANTLR gives the appropriate error message. +Parsing errors prevent the given input file from being processed. Other input files may still be processed. +Errors should not occur during a formatting transformation. All formatting transformations should never encounter the syntax that they are expected to change but are unable to change. +Warnings may occur during an updating transformation of the concrete syntax tree if an updating transformation encounters an issue, such as syntax that it would be expected to update, but can't for some reason. +Warnings like this will prevent the referenced code piece from being updated, but the rest of the file will still be processed. + +Errors and warnings of all kinds should be collected and reported to the user through stderr. +Errors and warnings should be handled in a way so as not to affect the tool's idempotency. diff --git a/src/VSCodeExtension/package-lock.json.v.template b/src/VSCodeExtension/package-lock.json.v.template index ed4ccf5a12..12071c94b1 100644 --- a/src/VSCodeExtension/package-lock.json.v.template +++ b/src/VSCodeExtension/package-lock.json.v.template @@ -4376,9 +4376,9 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { "version": "1.8.0", @@ -5342,9 +5342,9 @@ } }, "node_modules/tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", + "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -5801,6 +5801,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "bin": { "uuid": "bin/uuid" } @@ -10061,9 +10062,9 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { "version": "1.8.0", @@ -10770,9 +10771,9 @@ } }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", + "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0",