diff --git a/.gitignore b/.gitignore index 9f77ea53d20..7cffa506f2b 100644 --- a/.gitignore +++ b/.gitignore @@ -329,3 +329,4 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/built diff --git a/Simulation.sln b/Simulation.sln index ce58ef86a2b..c5a58260186 100644 --- a/Simulation.sln +++ b/Simulation.sln @@ -35,6 +35,10 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Tests.RoslynWrapper", "src\ EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Microsoft.Quantum.CsharpGeneration", "src\Simulation\CsharpGeneration\Microsoft.Quantum.CsharpGeneration.fsproj", "{B96E97F4-2DC8-45AC-ADF5-861D0D3073FC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestProjects", "TestProjects", "{09C842CB-930C-4C7D-AD5F-E30DE4A55820}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QsharpExe", "src\Simulation\Simulators.Tests\TestProjects\QsharpExe\QsharpExe.csproj", "{2F5796A7-4AF8-4B78-928A-0A3A80752F9D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -239,6 +243,22 @@ Global {B96E97F4-2DC8-45AC-ADF5-861D0D3073FC}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU {B96E97F4-2DC8-45AC-ADF5-861D0D3073FC}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU {B96E97F4-2DC8-45AC-ADF5-861D0D3073FC}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Debug|x64.Build.0 = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Release|Any CPU.Build.0 = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Release|x64.ActiveCfg = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.Release|x64.Build.0 = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D}.RelWithDebInfo|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -256,6 +276,8 @@ Global {618FBF9D-4EF3-435D-9728-81C726236668} = {A567C185-A429-418B-AFDE-6F1785BA4A77} {48206BD6-48DD-4442-A395-3A6594E4C9C6} = {A567C185-A429-418B-AFDE-6F1785BA4A77} {B96E97F4-2DC8-45AC-ADF5-861D0D3073FC} = {A567C185-A429-418B-AFDE-6F1785BA4A77} + {09C842CB-930C-4C7D-AD5F-E30DE4A55820} = {34D419E9-CCF1-4E48-9FA4-3AD4B86BEEB4} + {2F5796A7-4AF8-4B78-928A-0A3A80752F9D} = {09C842CB-930C-4C7D-AD5F-E30DE4A55820} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {929C0464-86D8-4F70-8835-0A5EAF930821} diff --git a/src/Simulation/CsharpGeneration.Tests/Circuits/Core.qs b/src/Simulation/CsharpGeneration.Tests/Circuits/Core.qs new file mode 100644 index 00000000000..8dbddd57b9e --- /dev/null +++ b/src/Simulation/CsharpGeneration.Tests/Circuits/Core.qs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Quantum.Core { + @Attribute() + newtype Attribute = Unit; + + @Attribute() + newtype EntryPoint = Unit; +} diff --git a/src/Simulation/CsharpGeneration.Tests/Circuits/EntryPointTests.qs b/src/Simulation/CsharpGeneration.Tests/Circuits/EntryPointTests.qs new file mode 100644 index 00000000000..54efe03736d --- /dev/null +++ b/src/Simulation/CsharpGeneration.Tests/Circuits/EntryPointTests.qs @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// +// No Options +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ReturnUnit() : Unit { } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ReturnInt() : Int { + return 42; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ReturnString() : String { + return "Hello, World!"; + } +} + +// --- + +// +// Single Option +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptInt(n : Int) : Int { + return n; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptBigInt(n : BigInt) : BigInt { + return n; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptDouble(n : Double) : Double { + return n; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptBool(b : Bool) : Bool { + return b; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptPauli(p : Pauli) : Pauli { + return p; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptResult(r : Result) : Result { + return r; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptRange(r : Range) : Range { + return r; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptString(s : String) : String { + return s; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptStringArray(xs : String[]) : String[] { + return xs; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation AcceptUnit(u : Unit) : Unit { + return u; + } +} + +// --- + +// +// Multiple Options +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation TwoOptions(n : Int, b : Bool) : String { + return $"{n} {b}"; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ThreeOptions(n : Int, b : Bool, xs : String[]) : String { + return $"{n} {b} {xs}"; + } +} + +// --- + +// +// Name Conversion +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation CamelCase(camelCaseName : String) : String { + return camelCaseName; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation SingleLetter(x : String) : String { + return x; + } +} + +// --- + +// +// Shadowing +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ShadowSimulator(simulator : String) : String { + return simulator; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ShadowS(s : String) : String { + return s; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + @EntryPoint() + operation ShadowVersion(version : String) : String { + return version; + } +} + +// --- + +// +// Simulators +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + open Microsoft.Quantum.Intrinsic; + + @EntryPoint() + operation XOrH(useH : Bool) : String { + using (q = Qubit()) { + if (useH) { + H(q); + } else { + X(q); + } + + if (M(q) == One) { + X(q); + } + } + return "Hello, World!"; + } +} + +// --- + +// +// Help +// + +namespace Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint { + /// # Summary + /// This test checks that the entry point documentation appears correctly in the command line help message. + /// + /// # Input + /// ## n + /// A number. + /// + /// ## pauli + /// The name of a Pauli matrix. + /// + /// ## myCoolBool + /// A neat bit. + @EntryPoint() + operation Help(n : Int, pauli : Pauli, myCoolBool : Bool) : Unit { } +} diff --git a/src/Simulation/CsharpGeneration.Tests/EntryPointTests.fs b/src/Simulation/CsharpGeneration.Tests/EntryPointTests.fs new file mode 100644 index 00000000000..038acd5d4f0 --- /dev/null +++ b/src/Simulation/CsharpGeneration.Tests/EntryPointTests.fs @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +module Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint + +open System +open System.Collections.Immutable +open System.Globalization +open System.IO +open System.Reflection +open System.Text.RegularExpressions +open System.Threading.Tasks +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp +open Microsoft.VisualStudio.LanguageServer.Protocol +open Xunit + +open Microsoft.Quantum.QsCompiler.CompilationBuilder +open Microsoft.Quantum.QsCompiler.CsharpGeneration +open Microsoft.Quantum.QsCompiler.DataTypes +open Microsoft.Quantum.QsCompiler.ReservedKeywords +open Microsoft.Quantum.QsCompiler.SyntaxTree +open Microsoft.Quantum.Simulation.Simulators + + +/// The path to the Q# file that provides the Microsoft.Quantum.Core namespace. +let private coreFile = Path.Combine ("Circuits", "Core.qs") |> Path.GetFullPath + +/// The path to the Q# file that provides the Microsoft.Quantum.Intrinsic namespace. +let private intrinsicFile = Path.Combine ("Circuits", "Intrinsic.qs") |> Path.GetFullPath + +/// The path to the Q# file that contains the test cases. +let private testFile = Path.Combine ("Circuits", "EntryPointTests.qs") |> Path.GetFullPath + +/// The namespace used for the test cases. +let private testNamespace = "Microsoft.Quantum.CsharpGeneration.Testing.EntryPoint" + +/// The test case for the given test number. +let private testCase = + File.ReadAllText testFile + |> fun text -> text.Split "// ---" + |> fun cases num -> cases.[num - 1] + +/// Compiles Q# source code into a syntax tree with the list of entry points names. +let private compileQsharp source = + let uri name = Uri ("file://" + name) + let fileManager name content = + CompilationUnitManager.InitializeFileManager (uri name, content) + + use compilationManager = new CompilationUnitManager (isExecutable = true) + let fileManagers = ImmutableHashSet.Create (fileManager coreFile (File.ReadAllText coreFile), + fileManager intrinsicFile (File.ReadAllText intrinsicFile), + fileManager testFile source) + compilationManager.AddOrUpdateSourceFilesAsync fileManagers |> ignore + let compilation = compilationManager.Build () + let errors = + compilation.Diagnostics () + |> Seq.filter (fun diagnostic -> diagnostic.Severity = DiagnosticSeverity.Error) + Assert.Empty errors + compilation.BuiltCompilation.Namespaces, compilation.BuiltCompilation.EntryPoints + +/// Generates C# source code for the given test case number and default simulator. +let private generateCsharp defaultSimulator (syntaxTree : QsNamespace seq, entryPoints) = + let assemblyConstants = + match defaultSimulator with + | Some simulator -> ImmutableDictionary.Empty.Add (AssemblyConstants.DefaultSimulator, simulator) + | None -> ImmutableDictionary.Empty + let context = CodegenContext.Create (syntaxTree, assemblyConstants) + let entryPoint = context.allCallables.[Seq.exactlyOne entryPoints] + [ + SimulationCode.generate (NonNullable<_>.New testFile) context + EntryPoint.generate context entryPoint + ] + +/// The full path to a referenced assembly given its short name. +let private referencedAssembly name = + let delimiter = if Environment.OSVersion.Platform = PlatformID.Win32NT then ';' else ':' + let path = + (AppContext.GetData "TRUSTED_PLATFORM_ASSEMBLIES" :?> string).Split delimiter + |> Seq.tryFind (fun path -> String.Equals (Path.GetFileNameWithoutExtension path, name, + StringComparison.InvariantCultureIgnoreCase)) + path |> Option.defaultWith (fun () -> failwith (sprintf "Missing reference to assembly '%s'." name)) + +/// Compiles the C# sources into an assembly. +let private compileCsharp (sources : string seq) = + let references : MetadataReference list = + [ + "netstandard" + "System.Collections.Immutable" + "System.CommandLine" + "System.Console" + "System.Linq" + "System.Private.CoreLib" + "System.Runtime" + "System.Runtime.Extensions" + "System.Runtime.Numerics" + "Microsoft.Quantum.QSharp.Core" + "Microsoft.Quantum.Runtime.Core" + "Microsoft.Quantum.Simulation.Common" + "Microsoft.Quantum.Simulation.Simulators" + ] + |> List.map (fun name -> upcast MetadataReference.CreateFromFile (referencedAssembly name)) + + let syntaxTrees = sources |> Seq.map CSharpSyntaxTree.ParseText + let compilation = CSharpCompilation.Create ("GeneratedEntryPoint", syntaxTrees, references) + use stream = new MemoryStream () + let result = compilation.Emit stream + Assert.True (result.Success, String.Join ("\n", result.Diagnostics)) + Assert.Equal (0L, stream.Seek (0L, SeekOrigin.Begin)) + Assembly.Load (stream.ToArray ()) + +/// The assembly for the given test case and default simulator. +let private testAssembly testNum defaultSimulator = + testNum + |> testCase + |> compileQsharp + |> generateCsharp defaultSimulator + |> compileCsharp + +/// Runs the entry point driver in the assembly with the given command-line arguments, and returns the output, errors, +/// and exit code. +let private run (assembly : Assembly) (args : string[]) = + let driver = assembly.GetType (EntryPoint.generatedNamespace testNamespace + ".Driver") + let main = driver.GetMethod("Main", BindingFlags.NonPublic ||| BindingFlags.Static) + let previousCulture = CultureInfo.DefaultThreadCurrentCulture + let previousOut = Console.Out + let previousError = Console.Error + + CultureInfo.DefaultThreadCurrentCulture <- CultureInfo ("en-US", false) + use outStream = new StringWriter () + use errorStream = new StringWriter () + try + Console.SetOut outStream + Console.SetError errorStream + let exitCode = main.Invoke (null, [| args |]) :?> Task |> Async.AwaitTask |> Async.RunSynchronously + outStream.ToString (), errorStream.ToString (), exitCode + finally + Console.SetError previousError + Console.SetOut previousOut + CultureInfo.DefaultThreadCurrentCulture <- previousCulture + +/// Asserts that running the entry point in the assembly with the given arguments succeeds and yields the expected +/// output. +let private yields expected (assembly, args) = + let normalize text = Regex.Replace(text, @"\s+", " ").Trim() + let out, error, exitCode = run assembly args + Assert.True (0 = exitCode, sprintf "Expected exit code 0, but got %d with:\n\n%s\n\n%s" exitCode out error) + Assert.Equal (normalize expected, normalize out) + +/// Asserts that running the entry point in the assembly with the given arguments fails. +let private fails (assembly, args) = + let out, error, exitCode = run assembly args + Assert.True (0 <> exitCode, sprintf "Expected non-zero exit code, but got 0 with:\n\n%s\n\n%s" out error) + +/// A tuple of the test assembly and arguments using the standard default simulator. The tuple can be passed to yields +/// or fails. +let private test testNum = + let assembly = testAssembly testNum None + fun args -> assembly, Array.ofList args + +/// A tuple of the test assembly and arguments using the given default simulator. The tuple can be passed to yields or +/// fails. +let private testWith testNum defaultSimulator = + let assembly = testAssembly testNum (Some defaultSimulator) + fun args -> assembly, Array.ofList args + + +// No Option + +[] +let ``Returns Unit`` () = + let given = test 1 + given [] |> yields "" + +[] +let ``Returns Int`` () = + let given = test 2 + given [] |> yields "42" + +[] +let ``Returns String`` () = + let given = test 3 + given [] |> yields "Hello, World!" + + +// Single Option + +[] +let ``Accepts Int`` () = + let given = test 4 + given ["-n"; "42"] |> yields "42" + given ["-n"; "4.2"] |> fails + given ["-n"; "9223372036854775807"] |> yields "9223372036854775807" + given ["-n"; "9223372036854775808"] |> fails + given ["-n"; "foo"] |> fails + +[] +let ``Accepts BigInt`` () = + let given = test 5 + given ["-n"; "42"] |> yields "42" + given ["-n"; "4.2"] |> fails + given ["-n"; "9223372036854775807"] |> yields "9223372036854775807" + given ["-n"; "9223372036854775808"] |> yields "9223372036854775808" + given ["-n"; "foo"] |> fails + +[] +let ``Accepts Double`` () = + let given = test 6 + given ["-n"; "4.2"] |> yields "4.2" + given ["-n"; "foo"] |> fails + +[] +let ``Accepts Bool`` () = + let given = test 7 + given ["-b"] |> yields "True" + given ["-b"; "false"] |> yields "False" + given ["-b"; "true"] |> yields "True" + given ["-b"; "one"] |> fails + +[] +let ``Accepts Pauli`` () = + let given = test 8 + given ["-p"; "PauliI"] |> yields "PauliI" + given ["-p"; "PauliX"] |> yields "PauliX" + given ["-p"; "PauliY"] |> yields "PauliY" + given ["-p"; "PauliZ"] |> yields "PauliZ" + given ["-p"; "PauliW"] |> fails + +[] +let ``Accepts Result`` () = + let given = test 9 + given ["-r"; "Zero"] |> yields "Zero" + given ["-r"; "zero"] |> yields "Zero" + given ["-r"; "One"] |> yields "One" + given ["-r"; "one"] |> yields "One" + given ["-r"; "0"] |> yields "Zero" + given ["-r"; "1"] |> yields "One" + given ["-r"; "Two"] |> fails + +[] +let ``Accepts Range`` () = + let given = test 10 + given ["-r"; "0..0"] |> yields "0..1..0" + given ["-r"; "0..1"] |> yields "0..1..1" + given ["-r"; "0..2..10"] |> yields "0..2..10" + given ["-r"; "0 ..1"] |> yields "0..1..1" + given ["-r"; "0.. 1"] |> yields "0..1..1" + given ["-r"; "0 .. 1"] |> yields "0..1..1" + given ["-r"; "0 ..2 ..10"] |> yields "0..2..10" + given ["-r"; "0.. 2 ..10"] |> yields "0..2..10" + given ["-r"; "0 .. 2 .. 10"] |> yields "0..2..10" + given ["-r"; "0 1"] |> fails + given ["-r"; "0 2 10"] |> fails + given ["-r"; "0"; "1"] |> fails + given ["-r"; "0"] |> fails + given ["-r"; "0.."] |> fails + given ["-r"; "0..2.."] |> fails + given ["-r"; "0..2..3.."] |> fails + given ["-r"; "0..2..3..4"] |> fails + given ["-r"; "0"; ".."; "1"] |> fails + given ["-r"; "0..1"; "..2"] |> fails + +[] +let ``Accepts String`` () = + let given = test 11 + given ["-s"; "Hello, World!"] |> yields "Hello, World!" + +[] +let ``Accepts String array`` () = + let given = test 12 + given ["--xs"; "foo"] |> yields "[foo]" + given ["--xs"; "foo"; "bar"] |> yields "[foo,bar]" + given ["--xs"; "foo bar"; "baz"] |> yields "[foo bar,baz]" + given ["--xs"; "foo"; "bar"; "baz"] |> yields "[foo,bar,baz]" + +[] +let ``Accepts Unit`` () = + let given = test 13 + given ["-u"; "()"] |> yields "" + given ["-u"; "42"] |> fails + + +// Multiple Options + +[] +let ``Accepts two options`` () = + let given = test 14 + given ["-n"; "7"; "-b"; "true"] |> yields "7 True" + given ["-b"; "true"; "-n"; "7"] |> yields "7 True" + +[] +let ``Accepts three options`` () = + let given = test 15 + given ["-n"; "7"; "-b"; "true"; "--xs"; "foo"] |> yields "7 True [foo]" + given ["--xs"; "foo"; "-n"; "7"; "-b"; "true"] |> yields "7 True [foo]" + given ["-n"; "7"; "--xs"; "foo"; "-b"; "true"] |> yields "7 True [foo]" + given ["-b"; "true"; "-n"; "7"; "--xs"; "foo"] |> yields "7 True [foo]" + +[] +let ``Requires all options`` () = + let given = test 15 + given ["-b"; "true"; "--xs"; "foo"] |> fails + given ["-n"; "7"; "--xs"; "foo"] |> fails + given ["-n"; "7"; "-b"; "true"] |> fails + given ["-n"; "7"] |> fails + given ["-b"; "true"] |> fails + given ["--xs"; "foo"] |> fails + given [] |> fails + + +// Name Conversion + +[] +let ``Uses kebab-case`` () = + let given = test 16 + given ["--camel-case-name"; "foo"] |> yields "foo" + given ["--camelCaseName"; "foo"] |> fails + +[] +let ``Uses single-dash short names`` () = + let given = test 17 + given ["-x"; "foo"] |> yields "foo" + given ["--x"; "foo"] |> fails + + +// Shadowing + +[] +let ``Shadows --simulator`` () = + let given = test 18 + given ["--simulator"; "foo"] |> yields "foo" + given ["--simulator"; AssemblyConstants.ResourcesEstimator] |> yields AssemblyConstants.ResourcesEstimator + given ["-s"; AssemblyConstants.ResourcesEstimator; "--simulator"; "foo"] |> fails + given ["-s"; "foo"] |> fails + +[] +let ``Shadows -s`` () = + let given = test 19 + given ["-s"; "foo"] |> yields "foo" + given ["--simulator"; AssemblyConstants.ToffoliSimulator; "-s"; "foo"] |> yields "foo" + given ["--simulator"; "bar"; "-s"; "foo"] |> fails + +[] +let ``Shadows --version`` () = + let given = test 20 + given ["--version"; "foo"] |> yields "foo" + + +// Simulators + +// The expected output from the resources estimator. +let private resourceSummary = "Metric Sum +CNOT 0 +QubitClifford 1 +R 0 +Measure 1 +T 0 +Depth 0 +Width 1 +BorrowedWidth 0" + +[] +let ``Supports QuantumSimulator`` () = + let given = test 21 + given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "false"] |> yields "Hello, World!" + given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "true"] |> yields "Hello, World!" + +[] +let ``Supports ToffoliSimulator`` () = + let given = test 21 + given ["--simulator"; AssemblyConstants.ToffoliSimulator; "--use-h"; "false"] |> yields "Hello, World!" + given ["--simulator"; AssemblyConstants.ToffoliSimulator; "--use-h"; "true"] |> fails + +[] +let ``Supports ResourcesEstimator`` () = + let given = test 21 + given ["--simulator"; AssemblyConstants.ResourcesEstimator; "--use-h"; "false"] |> yields resourceSummary + given ["--simulator"; AssemblyConstants.ResourcesEstimator; "--use-h"; "true"] |> yields resourceSummary + +[] +let ``Rejects unknown simulator`` () = + let given = test 21 + given ["--simulator"; "FooSimulator"; "--use-h"; "false"] |> fails + +[] +let ``Supports default standard simulator`` () = + let given = testWith 21 AssemblyConstants.ResourcesEstimator + given ["--use-h"; "false"] |> yields resourceSummary + given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "false"] |> yields "Hello, World!" + +[] +let ``Supports default custom simulator`` () = + // This is not really a "custom" simulator, but the driver does not recognize the fully-qualified name of the + // standard simulators, so it is treated as one. + let given = testWith 21 typeof.FullName + given ["--use-h"; "false"] |> yields "Hello, World!" + given ["--use-h"; "true"] |> fails + given ["--simulator"; typeof.FullName; "--use-h"; "false"] |> yields "Hello, World!" + given ["--simulator"; typeof.FullName; "--use-h"; "true"] |> fails + given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "false"] |> yields "Hello, World!" + given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "true"] |> yields "Hello, World!" + given ["--simulator"; AssemblyConstants.ResourcesEstimator; "--use-h"; "false"] |> yields resourceSummary + given ["--simulator"; typeof.FullName; "--use-h"; "false"] |> fails + + +// Help + +[] +let ``Uses documentation`` () = + let name = Path.GetFileNameWithoutExtension (Assembly.GetEntryAssembly().Location) + let message = (name, name) ||> sprintf "%s: + This test checks that the entry point documentation appears correctly in the command line help message. + +Usage: + %s [options] [command] + +Options: + -n (REQUIRED) A number. + --pauli (REQUIRED) The name of a Pauli matrix. + --my-cool-bool (REQUIRED) A neat bit. + -s, --simulator The name of the simulator to use. + --version Show version information + -?, -h, --help Show help and usage information + +Commands: + simulate (default) Run the program using a local simulator." + + let given = test 22 + given ["--help"] |> yields message + given ["-h"] |> yields message + given ["-?"] |> yields message diff --git a/src/Simulation/CsharpGeneration.Tests/Tests.CsharpGeneration.fsproj b/src/Simulation/CsharpGeneration.Tests/Tests.CsharpGeneration.fsproj index ec9defecf97..617f1637dbb 100644 --- a/src/Simulation/CsharpGeneration.Tests/Tests.CsharpGeneration.fsproj +++ b/src/Simulation/CsharpGeneration.Tests/Tests.CsharpGeneration.fsproj @@ -28,8 +28,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + @@ -39,6 +46,7 @@ + all @@ -48,7 +56,10 @@ + + + diff --git a/src/Simulation/CsharpGeneration/EntryPoint.fs b/src/Simulation/CsharpGeneration/EntryPoint.fs new file mode 100644 index 00000000000..0b1969b169e --- /dev/null +++ b/src/Simulation/CsharpGeneration/EntryPoint.fs @@ -0,0 +1,206 @@ +module internal Microsoft.Quantum.QsCompiler.CsharpGeneration.EntryPoint + +open Microsoft.CodeAnalysis.CSharp +open Microsoft.CodeAnalysis.CSharp.Syntax +open Microsoft.Quantum.QsCompiler.ReservedKeywords +open Microsoft.Quantum.QsCompiler.SyntaxProcessing.SyntaxExtensions +open Microsoft.Quantum.QsCompiler.SyntaxTokens +open Microsoft.Quantum.QsCompiler.SyntaxTree +open Microsoft.Quantum.RoslynWrapper +open System +open System.IO +open System.Reflection + + +/// An entry point parameter. +type private Parameter = + { Name : string + QsharpType : ResolvedType + CsharpTypeName : string + Description : string } + +/// The namespace in which to put generated code for the entry point. +let internal generatedNamespace entryPointNamespace = entryPointNamespace + ".__QsEntryPoint__" + +/// A public constant field. +let private constant name typeName value = + ``field`` typeName name [``public``; ``const``] (``:=`` value |> Some) + +/// A public static property with a getter. +let private readonlyProperty name typeName value = + ``property-arrow_get`` typeName name [``public``; ``static``] + ``get`` (``=>`` value) + +/// A static class containing constants used by the entry point driver. +let private constantsClass = + ``class`` "Constants" ``<<`` [] ``>>`` + ``:`` None ``,`` [] + [``internal``; ``static``] + ``{`` + [readonlyProperty "SimulatorOptions" "System.Collections.Generic.IEnumerable" + (``new array`` (Some "") [``literal`` ("--" + fst CommandLineArguments.SimulatorOption) + ``literal`` ("-" + snd CommandLineArguments.SimulatorOption)]) + constant "QuantumSimulator" "string" (``literal`` AssemblyConstants.QuantumSimulator) + constant "ToffoliSimulator" "string" (``literal`` AssemblyConstants.ToffoliSimulator) + constant "ResourcesEstimator" "string" (``literal`` AssemblyConstants.ResourcesEstimator)] + ``}`` + +/// A sequence of all of the named parameters in the argument tuple and their respective C# and Q# types. +let rec private parameters context doc = function + | QsTupleItem variable -> + match variable.VariableName, variable.Type.Resolution with + | ValidName name, ArrayType itemType -> + // Command-line parsing libraries can't convert to IQArray. Use IEnumerable instead. + let typeName = sprintf "System.Collections.Generic.IEnumerable<%s>" + (SimulationCode.roslynTypeName context itemType) + Seq.singleton { Name = name.Value + QsharpType = variable.Type + CsharpTypeName = typeName + Description = ParameterDescription doc name.Value } + | ValidName name, _ -> + Seq.singleton { Name = name.Value + QsharpType = variable.Type + CsharpTypeName = SimulationCode.roslynTypeName context variable.Type + Description = ParameterDescription doc name.Value } + | InvalidName, _ -> Seq.empty + | QsTuple items -> items |> Seq.map (parameters context doc) |> Seq.concat + +/// The custom argument handler for the given Q# type. +let private argumentHandler = + // TODO: Support array types derived from types that need a custom argument handler. + function + | UnitType -> Some "UnitArgumentHandler" + | Result -> Some "ResultArgumentHandler" + | BigInt -> Some "BigIntArgumentHandler" + | Range -> Some "RangeArgumentHandler" + | _ -> None + >> Option.map (fun handler -> ``ident`` "Driver" <|.|> ``ident`` handler) + +/// A property containing a sequence of command-line options corresponding to each parameter given. +let private parameterOptionsProperty parameters = + let optionTypeName = "System.CommandLine.Option" + let optionsEnumerableTypeName = sprintf "System.Collections.Generic.IEnumerable<%s>" optionTypeName + let toKebabCaseIdent = ``ident`` "System.CommandLine.Parsing.StringExtensions.ToKebabCase" + let getOption { Name = name; QsharpType = qsType; CsharpTypeName = typeName; Description = desc } = + let nameExpr = + if name.Length = 1 + then ``literal`` ("-" + name) + else ``literal`` "--" <+> ``invoke`` toKebabCaseIdent ``(`` [``literal`` name] ``)`` + let members = + argumentHandler qsType.Resolution + |> Option.map (fun handler -> ``ident`` "Argument" <-- handler :> ExpressionSyntax) + |> Option.toList + |> List.append [``ident`` "Required" <-- ``true``] + + ``new init`` (``type`` [sprintf "%s<%s>" optionTypeName typeName]) ``(`` [nameExpr; ``literal`` desc] ``)`` + ``{`` + members + ``}`` + + let options = parameters |> Seq.map getOption |> Seq.toList + ``property-arrow_get`` optionsEnumerableTypeName "Options" [``public``; ``static``] + ``get`` (``=>`` (``new array`` (Some optionTypeName) options)) + +/// The name of the parameter property for the given parameter name. +let private parameterPropertyName (s : string) = s.Substring(0, 1).ToUpper() + s.Substring 1 + +/// A sequence of properties corresponding to each parameter given. +let private parameterProperties = + Seq.map (fun { Name = name; CsharpTypeName = typeName } -> + ``prop`` typeName (parameterPropertyName name) [``public``]) + +/// The method for running the entry point using the parameter properties declared in the adapter. +let private runMethod context (entryPoint : QsCallable) = + let entryPointName = sprintf "%s.%s" entryPoint.FullName.Namespace.Value entryPoint.FullName.Name.Value + let returnTypeName = SimulationCode.roslynTypeName context entryPoint.Signature.ReturnType + let taskTypeName = sprintf "System.Threading.Tasks.Task<%s>" returnTypeName + let factoryParamName = "__factory__" + + let getArgExpr { Name = name; QsharpType = qsType } = + let property = ``ident`` "this" <|.|> ``ident`` (parameterPropertyName name) + match qsType.Resolution with + | ArrayType itemType -> + // Convert the IEnumerable property into a QArray. + let arrayTypeName = sprintf "QArray<%s>" (SimulationCode.roslynTypeName context itemType) + ``new`` (``type`` arrayTypeName) ``(`` [property] ``)`` + | _ -> property + + let callArgs : seq = + Seq.concat [ + Seq.singleton (upcast ``ident`` factoryParamName) + Seq.map getArgExpr (parameters context entryPoint.Documentation entryPoint.ArgumentTuple) + ] + + ``arrow_method`` taskTypeName "Run" ``<<`` [] ``>>`` + ``(`` [``param`` factoryParamName ``of`` (``type`` "IOperationFactory")] ``)`` + [``public``; ``async``] + (Some (``=>`` (``await`` (``ident`` entryPointName <.> (``ident`` "Run", callArgs))))) + +/// A method that creates an instance of the default simulator if it is a custom simulator. +let private customSimulatorFactory name = + let expr : ExpressionSyntax = + match name with + | "QuantumSimulator" | "ToffoliSimulator" | "ResourcesEstimator" -> + upcast SyntaxFactory.ThrowExpression (``new`` (``type`` "InvalidOperationException") ``(`` [] ``)``) + | _ -> ``new`` (``type`` name) ``(`` [] ``)`` + ``arrow_method`` "IOperationFactory" "CreateDefaultCustomSimulator" ``<<`` [] ``>>`` + ``(`` [] ``)`` + [``public``; ``static``] + (Some (``=>`` expr)) + +/// The class that adapts the entry point for use with the command-line parsing library and the driver. +let private adapterClass context (entryPoint : QsCallable) = + let summaryProperty = + readonlyProperty "Summary" "string" (``literal`` ((PrintSummary entryPoint.Documentation false).Trim ())) + let defaultSimulator = + context.assemblyConstants.TryGetValue "DefaultSimulator" + |> snd + |> (fun value -> if String.IsNullOrWhiteSpace value then "QuantumSimulator" else value) + let defaultSimulatorProperty = readonlyProperty "DefaultSimulator" "string" (``literal`` defaultSimulator) + let parameters = parameters context entryPoint.Documentation entryPoint.ArgumentTuple + + let members : seq = + Seq.concat [ + Seq.ofList [ + summaryProperty + defaultSimulatorProperty + parameterOptionsProperty parameters + customSimulatorFactory defaultSimulator + runMethod context entryPoint + ] + parameterProperties parameters |> Seq.map (fun property -> upcast property) + ] + + ``class`` "EntryPoint" ``<<`` [] ``>>`` + ``:`` None ``,`` [] + [``internal``] + ``{`` + members + ``}`` + +/// The source code for the entry point constants and adapter classes. +let private generatedClasses context (entryPoint : QsCallable) = + let ns = + ``namespace`` (generatedNamespace entryPoint.FullName.Namespace.Value) + ``{`` + (Seq.map ``using`` SimulationCode.autoNamespaces) + [ + constantsClass + adapterClass context entryPoint + ] + ``}`` + ``compilation unit`` [] [] [ns] + |> ``with leading comments`` SimulationCode.autogenComment + |> SimulationCode.formatSyntaxTree + |> fun code -> code + Environment.NewLine + +/// The source code for the entry point driver. +let private driver (entryPoint : QsCallable) = + let name = "Microsoft.Quantum.CsharpGeneration.Resources.EntryPointDriver.cs" + use stream = Assembly.GetExecutingAssembly().GetManifestResourceStream name + use reader = new StreamReader(stream) + reader.ReadToEnd().Replace("@Namespace", generatedNamespace entryPoint.FullName.Namespace.Value) + +/// Generates C# source code for a standalone executable that runs the Q# entry point. +let internal generate context entryPoint = + generatedClasses context entryPoint + driver entryPoint diff --git a/src/Simulation/CsharpGeneration/Microsoft.Quantum.CsharpGeneration.fsproj b/src/Simulation/CsharpGeneration/Microsoft.Quantum.CsharpGeneration.fsproj index 429f6cce38d..31751fbe1fb 100644 --- a/src/Simulation/CsharpGeneration/Microsoft.Quantum.CsharpGeneration.fsproj +++ b/src/Simulation/CsharpGeneration/Microsoft.Quantum.CsharpGeneration.fsproj @@ -8,10 +8,12 @@ + + @@ -21,7 +23,7 @@ - + diff --git a/src/Simulation/CsharpGeneration/Resources/EntryPointDriver.cs b/src/Simulation/CsharpGeneration/Resources/EntryPointDriver.cs new file mode 100644 index 00000000000..672ee3b049f --- /dev/null +++ b/src/Simulation/CsharpGeneration/Resources/EntryPointDriver.cs @@ -0,0 +1,394 @@ +namespace @Namespace +{ + using Microsoft.Quantum.Simulation.Core; + using Microsoft.Quantum.Simulation.Simulators; + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.CommandLine; + using System.CommandLine.Builder; + using System.CommandLine.Help; + using System.CommandLine.Invocation; + using System.CommandLine.Parsing; + using System.Linq; + using System.Numerics; + using System.Threading.Tasks; + + /// + /// The entry point driver is the entry point for the C# application that executes the Q# entry point. + /// + internal static class Driver + { + /// + /// The argument handler for the Q# Unit type. + /// + internal static Argument UnitArgumentHandler + { + get + { + var arg = new Argument(CreateArgumentParser(value => + value.Trim() == QVoid.Instance.ToString() ? (true, QVoid.Instance) : (false, default))); + arg.AddSuggestions(new[] { QVoid.Instance.ToString() }); + return arg; + } + } + + /// + /// The argument handler for the Q# Result type. + /// + internal static Argument ResultArgumentHandler + { + get + { + var arg = new Argument(CreateArgumentParser(value => + Enum.TryParse(value, ignoreCase: true, out ResultValue result) ? result switch + { + ResultValue.Zero => (true, Result.Zero), + ResultValue.One => (true, Result.One), + _ => (false, default) + } : (false, default))); + arg.AddSuggestions(new[] { ResultValue.Zero.ToString(), ResultValue.One.ToString() }); + return arg; + } + } + + /// + /// The argument handler for the Q# BigInt type. + /// + internal static Argument BigIntArgumentHandler => new Argument( + CreateArgumentParser(value => + BigInteger.TryParse(value, out var result) ? (true, result) : (false, default))); + + /// + /// The argument handler for the Q# Range type. + /// + internal static Argument RangeArgumentHandler => new Argument(result => + { + var option = ((OptionResult)result.Parent).Token.Value; + var value = result.Tokens.Single().Value; + var range = ParseRangeFromEnumerable(option, value, value.Split("..")); + if (range.IsFailure) + { + result.ErrorMessage = range.ErrorMessage; + } + return range.ValueOrDefault; + }) + { + Arity = ArgumentArity.ExactlyOne + }; + + /// + /// Runs the entry point. + /// + /// The command-line arguments. + /// The exit code. + private static async Task Main(string[] args) + { + var simulate = new Command("simulate", "(default) Run the program using a local simulator."); + TryCreateOption( + Constants.SimulatorOptions, + () => EntryPoint.DefaultSimulator, + "The name of the simulator to use.").Then(option => + { + option.Argument.AddSuggestions(ImmutableHashSet.Empty + .Add(Constants.QuantumSimulator) + .Add(Constants.ToffoliSimulator) + .Add(Constants.ResourcesEstimator) + .Add(EntryPoint.DefaultSimulator)); + simulate.AddOption(option); + }); + simulate.Handler = CommandHandler.Create(Simulate); + + var root = new RootCommand(EntryPoint.Summary) { simulate }; + foreach (var option in EntryPoint.Options) { root.AddGlobalOption(option); } + + // Set the simulate command as the default. + foreach (var option in simulate.Options) { root.AddOption(option); } + root.Handler = simulate.Handler; + + return await new CommandLineBuilder(root) + .UseDefaults() + .UseHelpBuilder(context => new QsHelpBuilder(context.Console)) + .Build() + .InvokeAsync(args); + } + + /// + /// Simulates the entry point. + /// + /// The entry point. + /// The simulator to use. + /// The exit code. + private static async Task Simulate(EntryPoint entryPoint, string simulator) + { + simulator = DefaultIfShadowed(Constants.SimulatorOptions.First(), simulator, EntryPoint.DefaultSimulator); + switch (simulator) + { + case Constants.ResourcesEstimator: + var resourcesEstimator = new ResourcesEstimator(); + await entryPoint.Run(resourcesEstimator); + Console.WriteLine(resourcesEstimator.ToTSV()); + break; + default: + var (isCustom, createSimulator) = simulator switch + { + Constants.QuantumSimulator => + (false, new Func(() => new QuantumSimulator())), + Constants.ToffoliSimulator => + (false, new Func(() => new ToffoliSimulator())), + _ => (true, EntryPoint.CreateDefaultCustomSimulator) + }; + if (isCustom && simulator != EntryPoint.DefaultSimulator) + { + DisplayCustomSimulatorError(simulator); + return 1; + } + await DisplayEntryPointResult(entryPoint, createSimulator); + break; + } + return 0; + } + + /// + /// Runs the entry point on a simulator and displays its return value. + /// + /// The entry point. + /// A function that creates an instance of the simulator to use. + private static async Task DisplayEntryPointResult( + EntryPoint entryPoint, Func createSimulator) + { + var simulator = createSimulator(); + try + { + var value = await entryPoint.Run(simulator); +#pragma warning disable CS0184 + if (!(value is QVoid)) +#pragma warning restore CS0184 + { + Console.WriteLine(value); + } + } + finally + { + if (simulator is IDisposable disposable) + { + disposable.Dispose(); + } + } + } + + /// + /// Returns true if the alias is available for use by the driver (that is, the alias is not already used by an + /// entry point option). + /// + /// The alias to check. + /// True if the alias is available for use by the driver. + private static bool IsAliasAvailable(string alias) => + !EntryPoint.Options.SelectMany(option => option.RawAliases).Contains(alias); + + /// + /// Returns the default value and displays a warning if the alias is shadowed by an entry point option, and + /// returns the original value otherwise. + /// + /// The type of the values. + /// The primary option alias corresponding to the value. + /// The value of the option given on the command line. + /// The default value for the option. + /// + private static T DefaultIfShadowed(string alias, T value, T defaultValue) + { + if (IsAliasAvailable(alias)) + { + return value; + } + else + { + var originalForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Error.WriteLine($"Warning: Option {alias} is overriden by an entry point parameter name."); + Console.Error.WriteLine($" Using default value {defaultValue}."); + Console.ForegroundColor = originalForeground; + return defaultValue; + } + } + + /// + /// Tries to create an option by removing aliases that are already in use by the entry point. If the first + /// alias, which is considered the primary alias, is in use, then the option is not created. + /// + /// The type of the option's argument. + /// The option's aliases. + /// A function that returns the option's default value. + /// The option's description. + /// The result of trying to create the option. + private static Result> TryCreateOption( + IEnumerable aliases, Func getDefaultValue, string description = null) => + IsAliasAvailable(aliases.First()) + ? Result>.Success( + new Option(aliases.Where(IsAliasAvailable).ToArray(), getDefaultValue, description)) + : Result>.Failure(); + + /// + /// Creates an argument parser that will use a default error message if parsing fails. + /// + /// The type of the argument. + /// + /// A function that takes the argument as a string and returns the parsed value and a boolean to indicate + /// whether parsing succeeded. + /// + /// An argument parser. + private static ParseArgument CreateArgumentParser(Func parse) => result => + { + var (success, value) = parse(result.Tokens.Single().Value); + if (!success) + { + result.ErrorMessage = GetArgumentErrorMessage( + ((OptionResult)result.Parent).Token.Value, result.Tokens.Single().Value, typeof(T)); + } + return value; + }; + + /// + /// Parses a Q# range from an enumerable of strings, where the items are start and end or start, step, and end. + /// + /// The name of the option being parsed. + /// The argument string for the option. + /// The items in the argument. + /// The result of parsing the strings. + private static Result ParseRangeFromEnumerable(string option, string arg, IEnumerable items) => + items.Select(item => TryParseLong(option, item)).Sequence().Bind(values => + values.Count() == 2 + ? Result.Success(new QRange(values.ElementAt(0), values.ElementAt(1))) + : values.Count() == 3 + ? Result.Success(new QRange(values.ElementAt(0), values.ElementAt(1), values.ElementAt(2))) + : Result.Failure(GetArgumentErrorMessage(option, arg, typeof(QRange)))); + + /// + /// Parses a long from a string. + /// + /// The name of the option being parsed. + /// The string to parse. + /// The result of parsing the string. + private static Result TryParseLong(string option, string str) => + long.TryParse(str, out var result) + ? Result.Success(result) + : Result.Failure(GetArgumentErrorMessage(option, str, typeof(long))); + + /// + /// Returns an error message string for an argument parser. + /// + /// The name of the option. + /// The value of the argument being parsed. + /// The expected type of the argument. + /// An error message string for an argument parser. + private static string GetArgumentErrorMessage(string option, string arg, Type type) => + $"Cannot parse argument '{arg}' for option '{option}' as expected type {type}."; + + /// + /// Displays an error message for using a non-default custom simulator. + /// + /// The name of the custom simulator. + private static void DisplayCustomSimulatorError(string name) + { + var originalForeground = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine($"The simulator '{name}' could not be found."); + Console.ForegroundColor = originalForeground; + Console.Error.WriteLine(); + Console.Error.WriteLine( + $"If '{name}' is a custom simulator, it must be set in the DefaultSimulator project property:"); + Console.Error.WriteLine(); + Console.Error.WriteLine(""); + Console.Error.WriteLine($" {name}"); + Console.Error.WriteLine(""); + } + } + + /// + /// A modification of the command line class. + /// + internal class QsHelpBuilder : HelpBuilder + { + public QsHelpBuilder(IConsole console) : base(console) { } + + protected override string ArgumentDescriptor(IArgument argument) + { + // Hide long argument descriptors. + var descriptor = base.ArgumentDescriptor(argument); + return descriptor.Length > 30 ? argument.Name : descriptor; + } + } + + /// + /// The result of a process that can either succeed or fail. + /// + /// The type of the result value. + internal struct Result + { + public bool IsSuccess { get; } + public bool IsFailure { get => !IsSuccess; } + public T Value { get => IsSuccess ? ValueOrDefault : throw new InvalidOperationException(); } + public T ValueOrDefault { get; } + public string ErrorMessage { get; } + + private Result(bool isSuccess, T value, string errorMessage) + { + IsSuccess = isSuccess; + ValueOrDefault = value; + ErrorMessage = errorMessage; + } + + public static Result Success(T value) => new Result(true, value, default); + + public static Result Failure(string errorMessage = null) => new Result(false, default, errorMessage); + } + + /// + /// Extension methods for . + /// + internal static class ResultExtensions + { + /// + /// Sequentially composes two results, passing the value of the first result to another result-producing + /// function if the first result is a success. + /// + /// The type of the first result value. + /// The type of the second result value. + /// The first result. + /// A function that takes the value of the first result and returns a second result. + /// + /// The first result if the first result is a failure; otherwise, the result of calling the bind function on the + /// first result's value. + /// + internal static Result Bind(this Result result, Func> bind) => + result.IsFailure ? Result.Failure(result.ErrorMessage) : bind(result.Value); + + /// + /// Converts an enumerable of results into a result of an enumerable. + /// + /// The type of the result values. + /// The results to sequence. + /// + /// A result that contains an enumerable of the result values if all of the results are a success, or the first + /// error message if one of the results is a failure. + /// + internal static Result> Sequence(this IEnumerable> results) => + results.All(result => result.IsSuccess) + ? Result>.Success(results.Select(results => results.Value)) + : Result>.Failure(results.First(results => results.IsFailure).ErrorMessage); + + /// + /// Calls the action on the result value if the result is a success. + /// + /// The type of the result value. + /// The result. + /// The action to call if the result is a success. + internal static void Then(this Result result, Action onSuccess) + { + if (result.IsSuccess) + { + onSuccess(result.Value); + } + } + } +} diff --git a/src/Simulation/CsharpGeneration/RewriteStep.fs b/src/Simulation/CsharpGeneration/RewriteStep.fs index fe5f9ce3183..7edfbc913cc 100644 --- a/src/Simulation/CsharpGeneration/RewriteStep.fs +++ b/src/Simulation/CsharpGeneration/RewriteStep.fs @@ -6,6 +6,7 @@ namespace Microsoft.Quantum.QsCompiler.CsharpGeneration open System open System.Collections.Generic open System.IO +open Microsoft.CodeAnalysis open Microsoft.Quantum.QsCompiler open Microsoft.Quantum.QsCompiler.CsharpGeneration open Microsoft.Quantum.QsCompiler.DataTypes @@ -16,20 +17,30 @@ open Microsoft.Quantum.QsCompiler.Transformations.BasicTransformations type Emitter() = - let _AssemblyConstants = new Dictionary() + let _AssemblyConstants = new Dictionary<_, _>() + let mutable _Diagnostics = [] interface IRewriteStep with member this.Name = "CsharpGeneration" member this.Priority = -1 // doesn't matter because this rewrite step is the only one in the dll - member this.AssemblyConstants = _AssemblyConstants :> IDictionary - member this.GeneratedDiagnostics = null + member this.AssemblyConstants = upcast _AssemblyConstants + member this.GeneratedDiagnostics = upcast _Diagnostics - member this.ImplementsPreconditionVerification = false + member this.ImplementsPreconditionVerification = true member this.ImplementsPostconditionVerification = false member this.ImplementsTransformation = true - member this.PreconditionVerification _ = NotImplementedException() |> raise + member this.PreconditionVerification compilation = + if compilation.EntryPoints.Length > 1 then + _Diagnostics <- IRewriteStep.Diagnostic + (Message = "C# generation is not possible because there is more than one entry point.", + Severity = DiagnosticSeverity.Error, + Stage = IRewriteStep.Stage.PreconditionVerification) :: _Diagnostics + false + else + true + member this.PostconditionVerification _ = NotImplementedException() |> raise member this.Transformation (compilation, transformed) = @@ -54,7 +65,10 @@ type Emitter() = let content = SimulationCode.loadedViaTestNames source context if content <> null then CompilationLoader.GeneratedFile(source, dir, ".dll.g.cs", content) |> ignore + if not compilation.EntryPoints.IsEmpty then + let callable = context.allCallables.[Seq.exactlyOne compilation.EntryPoints] + let content = EntryPoint.generate context callable + CompilationLoader.GeneratedFile(callable.SourceFile, dir, ".EntryPoint.g.cs", content) |> ignore + transformed <- compilation true - - diff --git a/src/Simulation/CsharpGeneration/SimulationCode.fs b/src/Simulation/CsharpGeneration/SimulationCode.fs index e4ceabdc4c4..8c45eedf7c0 100644 --- a/src/Simulation/CsharpGeneration/SimulationCode.fs +++ b/src/Simulation/CsharpGeneration/SimulationCode.fs @@ -1576,22 +1576,23 @@ module SimulationCode = |> Seq.filter (fun (_,elements) -> not elements.IsEmpty) |> Seq.toList + /// The comment that is displayed at the top of generated files. + let internal autogenComment = [ + "//------------------------------------------------------------------------------" + "// " + "// This code was generated by a tool. " + "// Changes to this file may cause incorrect behavior and will be lost if " + "// the code is regenerated. " + "// " + "//------------------------------------------------------------------------------" + ] + // Builds the C# syntaxTree for the Q# elements defined in the given file. let buildSyntaxTree localElements (context : CodegenContext) = let usings = autoNamespaces |> List.map (fun ns -> ``using`` ns) let attributes = localElements |> List.map (snd >> buildDeclarationAttributes) |> List.concat let namespaces = localElements |> List.map (buildNamespace context) - let autogenComment = [ - "//------------------------------------------------------------------------------" - "// " - "// This code was generated by a tool. " - "// Changes to this file may cause incorrect behavior and will be lost if " - "// the code is regenerated. " - "// " - "//------------------------------------------------------------------------------" - ] - ``compilation unit`` attributes usings @@ -1645,7 +1646,3 @@ module SimulationCode = else elements buildSyntaxTree localElements context |> formatSyntaxTree - - - - \ No newline at end of file diff --git a/src/Simulation/QCTraceSimulator.Tests/Tests.Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime.csproj b/src/Simulation/QCTraceSimulator.Tests/Tests.Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime.csproj index 81e1b1776cd..017930d97f3 100644 --- a/src/Simulation/QCTraceSimulator.Tests/Tests.Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime.csproj +++ b/src/Simulation/QCTraceSimulator.Tests/Tests.Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Simulation/QsharpCore/Microsoft.Quantum.QSharp.Core.csproj b/src/Simulation/QsharpCore/Microsoft.Quantum.QSharp.Core.csproj index 23760447b52..6fa9abf9f3b 100644 --- a/src/Simulation/QsharpCore/Microsoft.Quantum.QSharp.Core.csproj +++ b/src/Simulation/QsharpCore/Microsoft.Quantum.QSharp.Core.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Simulation/RoslynWrapper/WhiteNoise.fs b/src/Simulation/RoslynWrapper/WhiteNoise.fs index ba474c795a4..f693a1d614b 100644 --- a/src/Simulation/RoslynWrapper/WhiteNoise.fs +++ b/src/Simulation/RoslynWrapper/WhiteNoise.fs @@ -2,9 +2,7 @@ [] module WhiteNoise = - open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.CSharp - open Microsoft.CodeAnalysis.CSharp.Syntax let ``:`` = None let ``,`` = None @@ -31,3 +29,4 @@ module WhiteNoise = let ``override`` = SyntaxKind.OverrideKeyword let ``static`` = SyntaxKind.StaticKeyword let ``readonly`` = SyntaxKind.ReadOnlyKeyword + let ``const`` = SyntaxKind.ConstKeyword diff --git a/src/Simulation/Simulators.Tests/CoreTests.cs b/src/Simulation/Simulators.Tests/CoreTests.cs index 15e45c68ccc..981c4776169 100644 --- a/src/Simulation/Simulators.Tests/CoreTests.cs +++ b/src/Simulation/Simulators.Tests/CoreTests.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.IO; - +using System.Reflection; +using System.Text; +using Microsoft.Quantum.QsCompiler; using Microsoft.Quantum.Simulation.Common; using Microsoft.Quantum.Simulation.Core; using Microsoft.Quantum.Simulation.Simulators.QCTraceSimulators; @@ -14,8 +17,6 @@ namespace Microsoft.Quantum.Simulation.Simulators.Tests { - using Helper = Microsoft.Quantum.Simulation.Simulators.Tests.OperationsTestHelper; - public class CoreTests { private readonly ITestOutputHelper output; @@ -25,10 +26,27 @@ public CoreTests(ITestOutputHelper output) this.output = output; } + [Fact] + public void BasicExecution() + { + var asmPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var exe = Path.Combine(asmPath, "TestExe", "QsharpExe.dll"); + + ProcessRunner.Run("dotnet", exe, out var _, out StringBuilder error, out int exitCode, out Exception ex); + Assert.Null(ex); + Assert.Equal(1, exitCode); + Assert.Contains("NotImplementedException", error.ToString()); + + ProcessRunner.Run("dotnet", $"{exe} --simulator QuantumSimulator", out var _, out error, out exitCode, out ex); + Assert.Null(ex); + Assert.Equal(0, exitCode); + Assert.Empty(error.ToString().Trim()); + } + [Fact] public void Borrowing() { - Helper.RunWithMultipleSimulators((s) => + OperationsTestHelper.RunWithMultipleSimulators((s) => { var tracker = new StartTracker(s); @@ -44,18 +62,18 @@ public void Borrowing() BorrowingTest.Run(s).Wait(); - var tracer = Helper.GetTracer<(long, Qubit)>(s); + var tracer = OperationsTestHelper.GetTracer<(long, Qubit)>(s); - var testOne = new System.Action((callsCount, variant, info) => + var testOne = new Action((int callsCount, OperationFunctor variant, (int, Qubit) info) => { var (available, q) = info; Assert.Equal(callsCount, tracer.Log.GetNumberOfCalls(variant, (available, q))); }); - var testOneBody = new System.Action((callsCount, info) => testOne(callsCount, OperationFunctor.Body, info)); - var testOneAdjoint = new System.Action((callsCount, info) => testOne(callsCount, OperationFunctor.Adjoint, info)); - var testOneCtrl = new System.Action((callsCount, info) => testOne(callsCount, OperationFunctor.Controlled, info)); - var testOneCtrlAdj = new System.Action((callsCount, info) => testOne(callsCount, OperationFunctor.ControlledAdjoint, info)); + var testOneBody = new Action((callsCount, info) => testOne(callsCount, OperationFunctor.Body, info)); + var testOneAdjoint = new Action((callsCount, info) => testOne(callsCount, OperationFunctor.Adjoint, info)); + var testOneCtrl = new Action((callsCount, info) => testOne(callsCount, OperationFunctor.Controlled, info)); + var testOneCtrlAdj = new Action((callsCount, info) => testOne(callsCount, OperationFunctor.ControlledAdjoint, info)); testOneBody(6, (0, q5)); testOneBody(6, (0, q6)); @@ -180,7 +198,7 @@ void RunOne(IOperationFactory s) Assert.False(File.Exists("()")); } - Helper.RunWithMultipleSimulators((s) => RunOne(s as IOperationFactory)); + OperationsTestHelper.RunWithMultipleSimulators((s) => RunOne(s as IOperationFactory)); RunOne(new QCTraceSimulator()); RunOne(new ResourcesEstimator()); RunOne(new QuantumSimulator()); @@ -215,7 +233,7 @@ void RunOne(IOperationFactory s) } } - Helper.RunWithMultipleSimulators((s) => RunOne(s as IOperationFactory)); + OperationsTestHelper.RunWithMultipleSimulators((s) => RunOne(s as IOperationFactory)); RunOne(new QCTraceSimulator()); RunOne(new ResourcesEstimator()); } @@ -223,9 +241,9 @@ void RunOne(IOperationFactory s) [Fact] public void ZeroQubits() { - Helper.RunWithMultipleSimulators((s) => + OperationsTestHelper.RunWithMultipleSimulators((s) => { - var tracer = Helper.GetTracer(s); + var tracer = OperationsTestHelper.GetTracer(s); ZeroQubitsTest.Run(s).Wait(); @@ -236,7 +254,7 @@ public void ZeroQubits() [Fact] public void InterpolatedStrings() { - Helper.RunWithMultipleSimulators((s) => + OperationsTestHelper.RunWithMultipleSimulators((s) => { Circuits.InterpolatedStringTest.Run(s).Wait(); // Throws if it doesn't succeed }); @@ -245,7 +263,7 @@ public void InterpolatedStrings() [Fact] public void RandomOperation() { - Helper.RunWithMultipleSimulators((s) => + OperationsTestHelper.RunWithMultipleSimulators((s) => { Circuits.RandomOperationTest.Run(s).Wait(); // Throws if it doesn't succeed }); @@ -254,7 +272,7 @@ public void RandomOperation() [Fact] public void BigInts() { - Helper.RunWithMultipleSimulators((s) => + OperationsTestHelper.RunWithMultipleSimulators((s) => { Circuits.BigIntTest.Run(s).Wait(); // Throws if it doesn't succeed }); @@ -282,6 +300,6 @@ public void CatchFail() [Fact] public void InternalCallables() => - Helper.RunWithMultipleSimulators(s => Circuits.InternalCallablesTest.Run(s).Wait()); + OperationsTestHelper.RunWithMultipleSimulators(s => Circuits.InternalCallablesTest.Run(s).Wait()); } } diff --git a/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/Main.qs b/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/Main.qs new file mode 100644 index 00000000000..41a087bad12 --- /dev/null +++ b/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/Main.qs @@ -0,0 +1,16 @@ +namespace QsharpExe { + + open Microsoft.Quantum.Intrinsic; + + @EntryPoint() + operation Main () : Result { + using (q = Qubit()) { + H(q); + let res = M(q); + if (res == One) { + X(q); + } + return res; + } + } +} diff --git a/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/QsharpExe.csproj b/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/QsharpExe.csproj new file mode 100644 index 00000000000..25e28abe68d --- /dev/null +++ b/src/Simulation/Simulators.Tests/TestProjects/QsharpExe/QsharpExe.csproj @@ -0,0 +1,40 @@ + + + + Exe + netcoreapp3.1 + + false + false + ToffoliSimulator + + + + + + + + + + + + + + + + + <_ExeDir>$(MSBuildThisFileDirectory)built + + + + + + + + <_ExeFiles Include="$(OutputPath)*" /> + + + + + + diff --git a/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj b/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj index 5235cdd9332..6e8583c9975 100644 --- a/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj +++ b/src/Simulation/Simulators.Tests/Tests.Microsoft.Quantum.Simulation.Simulators.csproj @@ -1,4 +1,4 @@ - + @@ -11,9 +11,18 @@ false + + + + + + - + + + false + @@ -24,7 +33,18 @@ - + + + + + <_ExeDir>$(MSBuildThisFileDirectory)TestProjects\QsharpExe\built\ + + + <_ExeFiles Include="$(_ExeDir)*" /> + + + + diff --git a/src/Simulation/Simulators/Microsoft.Quantum.Simulation.Simulators.csproj b/src/Simulation/Simulators/Microsoft.Quantum.Simulation.Simulators.csproj index 1cc02f62456..650f7494f49 100644 --- a/src/Simulation/Simulators/Microsoft.Quantum.Simulation.Simulators.csproj +++ b/src/Simulation/Simulators/Microsoft.Quantum.Simulation.Simulators.csproj @@ -1,4 +1,4 @@ - +