// routines related to tracking the editor state /// <summary> /// To be called whenever a file is opened in the editor. /// Does nothing if the given file is listed as to be ignored. /// Otherwise publishes suitable diagnostics for it. /// Invokes the given Action showError with a suitable message if the given file cannot be loaded. /// Invokes the given Action logError with a suitable message if the given file cannot be associated with a compilation unit, /// or if the given file is already listed as being open in the editor. /// Throws an ArgumentException if the uri of the given text document identifier is null or not an absolute file uri. /// Throws an ArgumentNullException if the given content is null. /// </summary> internal Task OpenFileAsync(TextDocumentItem textDocument, Action <string, MessageType> showError = null, Action <string, MessageType> logError = null) { if (!ValidFileUri(textDocument?.Uri)) { throw new ArgumentException("invalid text document identifier"); } if (textDocument.Text == null) { throw new ArgumentNullException(nameof(textDocument.Text)); } _ = this.Projects.ManagerTaskAsync(textDocument.Uri, (manager, associatedWithProject) => { if (IgnoreFile(textDocument.Uri)) { return; } var newManager = CompilationUnitManager.InitializeFileManager(textDocument.Uri, textDocument.Text, this.Publish, ex => { showError?.Invoke($"Failed to load file '{textDocument.Uri.LocalPath}'", MessageType.Error); manager.LogException(ex); }); // Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail. // To mitigate the impact of failures we choose to just log them as info. var file = this.OpenFiles.GetOrAdd(textDocument.Uri, newManager); if (file != newManager) // this may be the case (depending on the editor) e.g. when opening a version control diff ... { showError?.Invoke($"Version control and opening multiple versions of the same file in the editor are currently not supported. \n" + $"Intellisense has been disable for the file '{textDocument.Uri.LocalPath}'. An editor restart is required to enable intellisense again.", MessageType.Error); #if DEBUG if (showError == null) { logError?.Invoke("Attempting to open a file that is already open in the editor.", MessageType.Error); } #endif this.IgnoreEditorUpdatesFor(textDocument.Uri); this.OpenFiles.TryRemove(textDocument.Uri, out FileContentManager _); if (!associatedWithProject) { _ = manager.TryRemoveSourceFileAsync(textDocument.Uri); } this.Publish(new PublishDiagnosticParams { Uri = textDocument.Uri, Diagnostics = new Diagnostic[0] }); return; } if (!associatedWithProject) { logError?.Invoke( $"The file {textDocument.Uri.LocalPath} is not associated with a compilation unit. Only syntactic diagnostics are generated." , MessageType.Info); } _ = manager.AddOrUpdateSourceFileAsync(file); }); // reloading from disk in case we encountered a file already open error above return(this.Projects.SourceFileChangedOnDiskAsync(textDocument.Uri, GetOpenFile)); // NOTE: relies on that the manager task is indeed executed first! }
/// <summary> /// Used to reload the file content when a file is saved. /// Does nothing if the given file is listed as to be ignored. /// Expects to get the entire content of the file at the time of saving as argument. /// Throws an ArgumentException if the uri of the given text document identifier is null or not an absolute file uri. /// Throws an ArgumentNullException if the given content is null. /// </summary> internal Task SaveFileAsync(TextDocumentIdentifier textDocument, string fileContent) { if (!ValidFileUri(textDocument?.Uri)) { throw new ArgumentException("invalid text document identifier"); } if (fileContent == null) { throw new ArgumentNullException(nameof(fileContent)); } return(this.Projects.ManagerTaskAsync(textDocument.Uri, (manager, __) => { if (IgnoreFile(textDocument.Uri)) { return; } // Currently it is not possible to handle both the behavior of VS and VS Code for changes on disk in a manner that will never fail. // To mitigate the impact of failures we choose to ignore them silently and do our best to recover. if (!this.OpenFiles.TryGetValue(textDocument.Uri, out var file)) { file = CompilationUnitManager.InitializeFileManager(textDocument.Uri, fileContent, this.Publish, manager.LogException); this.OpenFiles.TryAdd(textDocument.Uri, file); _ = manager.AddOrUpdateSourceFileAsync(file); } else { _ = manager.AddOrUpdateSourceFileAsync(file, fileContent); // let's reload the file content on saving } })); }
/// <summary> /// Loads the Q# data structures in a referenced assembly given the Uri to that assembly, /// and returns the loaded content as out parameter. /// Returns false if some of the content could not be loaded successfully, /// possibly because the referenced assembly has been compiled with an older compiler version. /// If onDeserializationException is specified, invokes the given action on any exception thrown during deserialization. /// Throws the corresponding exceptions if the information cannot be extracted. /// </summary> /// <exception cref="FileNotFoundException"><paramref name="asm"/> does not exist.</exception> /// <exception cref="ArgumentException"><paramref name="asm"/> is not an absolute file URI.</exception> public static bool LoadReferencedAssembly(Uri asm, out References.Headers headers, bool ignoreDllResources = false, Action <Exception>?onDeserializationException = null) { var id = CompilationUnitManager.GetFileId(asm); if (!File.Exists(asm.LocalPath)) { throw new FileNotFoundException($"The file '{asm.LocalPath}' given to the assembly loader does not exist."); } using var stream = File.OpenRead(asm.LocalPath); using var assemblyFile = new PEReader(stream); if (ignoreDllResources || !FromResource(assemblyFile, out var compilation, onDeserializationException)) { PerformanceTracking.TaskStart(PerformanceTracking.Task.HeaderAttributesLoading); var attributes = LoadHeaderAttributes(assemblyFile); PerformanceTracking.TaskEnd(PerformanceTracking.Task.HeaderAttributesLoading); PerformanceTracking.TaskStart(PerformanceTracking.Task.ReferenceHeadersCreation); headers = new References.Headers(id, attributes); PerformanceTracking.TaskEnd(PerformanceTracking.Task.ReferenceHeadersCreation); return(ignoreDllResources || !attributes.Any()); // just means we have no references } PerformanceTracking.TaskStart(PerformanceTracking.Task.ReferenceHeadersCreation); headers = new References.Headers(id, compilation?.Namespaces ?? ImmutableArray <QsNamespace> .Empty); PerformanceTracking.TaskEnd(PerformanceTracking.Task.ReferenceHeadersCreation); return(true); }
private QsCompilation CreateCompilation(params string[] fileNames) { var mgr = new CompilationUnitManager(); var files = CreateFileManager(fileNames); mgr.AddOrUpdateSourceFilesAsync(files).Wait(); return(mgr.Build().BuiltCompilation); }
/// <summary> /// Compiles the given code. /// If the operations defined in this code are already defined /// in existing Snippets, those Snippets are skipped. /// If successful, this updates the AssemblyInfo /// with the new operations found in the Snippet. /// If errors are found during compilation, a `CompilationErrorsException` is triggered /// with the list of errors found. /// If successful, the list of snippets is updated to include those that were part of the /// compilation and it will return a new Snippet with the warnings and Q# elements /// reported by the compiler. /// </summary> public Snippet Compile(string code) { if (string.IsNullOrWhiteSpace(code)) { throw new ArgumentNullException(nameof(code)); } var duration = Stopwatch.StartNew(); // We add exactly one line of boilerplate code at the beginning of each snippet, // so tell the logger to subtract one from all displayed line numbers. var logger = new QSharpLogger(Logger, lineNrOffset: -1); try { var snippets = SelectSnippetsToCompile(code).ToArray(); var assembly = Compiler.BuildSnippets(snippets, _metadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__snippets__.dll")); if (logger.HasErrors) { throw new CompilationErrorsException(logger.Errors.ToArray()); } foreach (var entry in Compiler.IdentifyOpenedNamespaces(code)) { Compiler.AutoOpenNamespaces[entry.Key] = entry.Value; } // populate the original snippet with the results of the compilation: Snippet populate(Snippet s) => new Snippet() { id = string.IsNullOrWhiteSpace(s.id) ? Guid.NewGuid().ToString() : s.id, code = s.code, warnings = logger.Logs .Where(m => m.Source == CompilationUnitManager.GetFileId(s.Uri).Value) .Select(logger.Format) .ToArray(), Elements = assembly?.SyntaxTree? .SelectMany(ns => ns.Elements) .Where(c => c.SourceFile() == CompilationUnitManager.GetFileId(s.Uri).Value) .ToArray() }; AssemblyInfo = assembly; Items = snippets.Select(populate).ToArray(); return(Items.Last()); } finally { duration.Stop(); var status = logger.HasErrors ? "error" : "ok"; var errorIds = logger.ErrorIds.ToArray(); SnippetCompiled?.Invoke(this, new SnippetCompiledEventArgs(status, errorIds, Compiler.AutoOpenNamespaces.Keys.ToArray(), duration.Elapsed)); } }
/// <summary> /// Builds the corresponding .net core assembly from the code in the given files. /// </summary> public AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName) { var syntaxTree = BuildQsSyntaxTree(files, metadatas.QsMetadatas, logger); Uri FileUri(string f) => CompilationUnitManager.TryGetUri(NonNullable <string> .New(f), out var uri) ? uri : null; var assembly = BuildAssembly(files.Select(FileUri).ToArray(), syntaxTree, metadatas.RoslynMetadatas, logger, dllName); return(assembly); }
/// <summary> /// Compiles the given code. /// If the operations defined in this code are already defined /// in existing Snippets, those Snippets are skipped. /// If successful, this updates the AssemblyInfo /// with the new operations found in the Snippet. /// If errors are found during compilation, a `CompilationErrorsException` is triggered /// with the list of errors found. /// If successful, the list of snippets is updated to include those that were part of the /// compilation and it will return a new Snippet with the warnings and Q# elements /// reported by the compiler. /// </summary> public Snippet Compile(string code) { if (string.IsNullOrWhiteSpace(code)) { throw new ArgumentNullException(nameof(code)); } var duration = Stopwatch.StartNew(); var logger = new QSharpLogger(Logger); try { var snippets = SelectSnippetsToCompile(code).ToArray(); var assembly = Compiler.BuildSnippets(snippets, _metadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__snippets__.dll")); if (logger.HasErrors) { throw new CompilationErrorsException(logger.Errors.ToArray()); } // populate the original snippet with the results of the compilation: Snippet populate(Snippet s) => new Snippet() { id = string.IsNullOrWhiteSpace(s.id) ? Guid.NewGuid().ToString() : s.id, code = s.code, warnings = logger.Logs .Where(m => m.Source == CompilationUnitManager.GetFileId(s.Uri).Value) .Select(logger.Format) .ToArray(), Elements = assembly?.SyntaxTree? .SelectMany(ns => ns.Elements) .Where(c => c.SourceFile() == CompilationUnitManager.GetFileId(s.Uri).Value) .ToArray() }; AssemblyInfo = assembly; Items = snippets.Select(populate).ToArray(); return(Items.Last()); } finally { duration.Stop(); var status = logger.HasErrors ? "error" : "ok"; var errorIds = logger.ErrorIds.ToArray(); SnippetCompiled?.Invoke(this, new SnippetCompiledEventArgs(status, errorIds, duration.Elapsed)); } }
private FileContentManager InitializeFileManager(IEnumerable <string> examples, QsCompilation compilation, string nsName = null) { var(pre, post) = ($"namespace {nsName ?? DefaultNamespaceName}{{ {Environment.NewLine}", $"{Environment.NewLine}}}"); var openDirs = String.Join(Environment.NewLine, OpenedForTesting .Where(nsName => ContainsNamespace(compilation, nsName)) .Select(nsName => $"open {nsName};")); string WrapInNamespace(string example) => pre + openDirs + example + post; examples = examples.Where(ex => !String.IsNullOrWhiteSpace(ex)); var sourceCode = String.Join(Environment.NewLine, examples.Select(WrapInNamespace)); var sourceName = NonNullable <string> .New(Path.GetFullPath($"{nsName}{CodeSource}")); return(CompilationUnitManager.TryGetUri(sourceName, out var sourceUri) ? CompilationUnitManager.InitializeFileManager(sourceUri, sourceCode) : null); }
/// <summary> /// Loads the Q# data structures in a referenced assembly given the Uri to that assembly, /// and returns the loaded content as out parameter. /// Returns false if some of the content could not be loaded successfully, /// possibly because the referenced assembly has been compiled with an older compiler version. /// Throws an ArgumentNullException if the given uri is null. /// Throws a FileNotFoundException if no file with the given name exists. /// Throws the corresponding exceptions if the information cannot be extracted. /// </summary> public static bool LoadReferencedAssembly(Uri asm, out References.Headers headers, bool ignoreDllResources = false) { if (asm == null) { throw new ArgumentNullException(nameof(asm)); } if (!CompilationUnitManager.TryGetFileId(asm, out var id) || !File.Exists(asm.LocalPath)) { throw new FileNotFoundException($"the uri '{asm}' given to the assembly loader is invalid or the file does not exist"); } using var stream = File.OpenRead(asm.LocalPath); using var assemblyFile = new PEReader(stream); if (ignoreDllResources || !FromResource(assemblyFile, out var syntaxTree)) { var attributes = LoadHeaderAttributes(assemblyFile); headers = new References.Headers(id, attributes); return(ignoreDllResources || !attributes.Any()); // just means we have no references } headers = new References.Headers(id, syntaxTree?.Namespaces ?? ImmutableArray <QsNamespace> .Empty); return(true); }
/// <summary> /// Returns the workspace edit that describes the changes to be done if the symbol at the given position - if any - is renamed to the given name. /// Returns null if no symbol exists at the specified position, /// or if some parameters are unspecified (null), /// or if the specified position is not a valid position within the file. /// </summary> public static WorkspaceEdit?Rename(this FileContentManager file, CompilationUnit compilation, Position position, string newName) { if (newName == null || file == null) { return(null); } var found = file.TryGetReferences(compilation, position, out var declLocation, out var locations); if (!found) { return(null); } if (declLocation != null) { locations = new[] { declLocation }.Concat(locations); } var changes = locations.ToLookup(loc => loc.Uri, loc => new TextEdit { Range = loc.Range, NewText = newName }); return(new WorkspaceEdit { DocumentChanges = changes .Select(change => new TextDocumentEdit { TextDocument = new VersionedTextDocumentIdentifier { Uri = change.Key, Version = 1 }, // setting version to null here won't work in VS Code ... Edits = change.ToArray() }) .ToArray(), Changes = changes.ToDictionary( items => CompilationUnitManager.GetFileId(items.Key), items => items.ToArray()) }); }
/// <summary> /// Builds the corresponding .net core assembly from the Q# syntax tree. /// </summary> private static AssemblyInfo BuildAssembly(Uri[] fileNames, QsCompiler.SyntaxTree.QsNamespace[] syntaxTree, IEnumerable <MetadataReference> references, QSharpLogger logger, string targetDll) { if (logger.HasErrors) { return(null); } logger.LogDebug($"Compiling the following Q# files: {string.Join(",", fileNames.Select(f => f.LocalPath))}"); try { // Generate C# simulation code from Q# syntax tree and convert it into C# syntax trees: var trees = new List <SyntaxTree>(); NonNullable <string> GetFileId(Uri uri) => CompilationUnitManager.TryGetFileId(uri, out var id) ? id : NonNullable <string> .New(uri.AbsolutePath); foreach (var file in fileNames) { var sourceFile = GetFileId(file); var code = SimulationCode.generate(sourceFile, syntaxTree); var tree = CSharpSyntaxTree.ParseText(code, encoding: UTF8Encoding.UTF8); trees.Add(tree); logger.LogDebug($"Generated the following C# code for {sourceFile.Value}:\n=============\n{code}\n=============\n"); } // Compile the C# syntax trees: var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Debug); var compilation = CSharpCompilation.Create( Path.GetFileNameWithoutExtension(targetDll), trees, references, options); // Generate the assembly from the C# compilation: using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); if (!result.Success) { IEnumerable <Diagnostic> failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); logger.LogError("IQS000", "Could not compile Roslyn dll from working folder."); foreach (Diagnostic diagnostic in failures) { logger.LogError(diagnostic.Id, diagnostic.GetMessage()); } return(null); } else { logger.LogDebug($"Assembly successfully generated. Caching at {targetDll}."); var data = ms.ToArray(); try { File.WriteAllBytes(targetDll, data); } catch (Exception e) { logger.LogError("IQS001", $"Unable to save assembly cache: {e.Message}."); } return(new AssemblyInfo(Assembly.Load(data), targetDll, syntaxTree)); } } } catch (Exception e) { logger.LogError("IQS002", $"Unexpected error compiling assembly: {e.Message}."); return(null); } }
/// <summary> /// Builds the corresponding .net core assembly from the Q# syntax tree. /// </summary> private AssemblyInfo BuildAssembly(ImmutableDictionary <Uri, string> sources, CompilerMetadata metadata, QSharpLogger logger, string dllName) { logger.LogDebug($"Compiling the following Q# files: {string.Join(",", sources.Keys.Select(f => f.LocalPath))}"); var qsCompilation = this.UpdateCompilation(sources, metadata.QsMetadatas, logger); if (logger.HasErrors) { return(null); } try { // Generate C# simulation code from Q# syntax tree and convert it into C# syntax trees: var trees = new List <SyntaxTree>(); NonNullable <string> GetFileId(Uri uri) => CompilationUnitManager.TryGetFileId(uri, out var id) ? id : NonNullable <string> .New(uri.AbsolutePath); foreach (var file in sources.Keys) { var sourceFile = GetFileId(file); var code = SimulationCode.generate(sourceFile, CodegenContext.Create(qsCompilation.Namespaces)); var tree = CSharpSyntaxTree.ParseText(code, encoding: UTF8Encoding.UTF8); trees.Add(tree); logger.LogDebug($"Generated the following C# code for {sourceFile.Value}:\n=============\n{code}\n=============\n"); } // Compile the C# syntax trees: var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Debug); var compilation = CSharpCompilation.Create( Path.GetFileNameWithoutExtension(dllName), trees, metadata.RoslynMetadatas, options); // Generate the assembly from the C# compilation: using (var ms = new MemoryStream()) using (var bsonStream = new MemoryStream()) { using var writer = new BsonDataWriter(bsonStream) { CloseOutput = false }; var fromSources = qsCompilation.Namespaces.Select(ns => FilterBySourceFile.Apply(ns, s => s.Value.EndsWith(".qs"))); Json.Serializer.Serialize(writer, new QsCompilation(fromSources.ToImmutableArray(), qsCompilation.EntryPoints)); var resourceDescription = new ResourceDescription ( resourceName: QsCompiler.ReservedKeywords.DotnetCoreDll.ResourceName, dataProvider: () => new MemoryStream(bsonStream.ToArray()), isPublic: true ); var result = compilation.Emit(ms, manifestResources: new[] { resourceDescription }); if (!result.Success) { IEnumerable <Diagnostic> failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); logger.LogError("IQS000", "Could not compile Roslyn dll from working folder."); foreach (Diagnostic diagnostic in failures) { logger.LogError(diagnostic.Id, diagnostic.GetMessage()); } return(null); } else { logger.LogDebug($"Assembly successfully generated. Caching at {dllName}."); var data = ms.ToArray(); try { File.WriteAllBytes(dllName, data); } catch (Exception e) { logger.LogError("IQS001", $"Unable to save assembly cache: {e.Message}."); } return(new AssemblyInfo(Assembly.Load(data), dllName, fromSources.ToArray())); } } } catch (Exception e) { logger.LogError("IQS002", $"Unexpected error compiling assembly: {e.Message}."); return(null); } }
// interface methods public bool Transformation(QsCompilation compilation, out QsCompilation transformed) { transformed = FilterSourceFiles.Apply(compilation); var manager = new CompilationUnitManager(); // get source code from examples var fileManagers = ExamplesInDocs.Extract(transformed) .Select(g => InitializeFileManager(g, compilation, g.Key)) .Where(m => m != null).ToImmutableHashSet(); manager.AddOrUpdateSourceFilesAsync(fileManagers, suppressVerification: true); var sourceFiles = fileManagers.Select(m => m.FileName).ToImmutableHashSet(); bool IsGeneratedSourceFile(NonNullable <string> source) => sourceFiles.Contains(source); // get everything contained in the compilation as references var refName = NonNullable <string> .New(Path.GetFullPath(ReferenceSource)); var refHeaders = new References.Headers(refName, DllToQs.Rename(compilation).Namespaces); var refDict = new Dictionary <NonNullable <string>, References.Headers> { { refName, refHeaders } }; var references = new References(refDict.ToImmutableDictionary()); manager.UpdateReferencesAsync(references); // compile the examples in the doc comments and add any diagnostics to the list of generated diagnostics var built = manager.Build(); var diagnostics = built.Diagnostics(); this.Diagnostics.AddRange(diagnostics.Select(d => IRewriteStep.Diagnostic.Create(d, IRewriteStep.Stage.Transformation))); if (diagnostics.Any(d => d.Severity == VS.DiagnosticSeverity.Error)) { return(false); } // add the extracted namespace elements from doc comments to the transformed compilation var toBeAdded = built.BuiltCompilation.Namespaces.ToImmutableDictionary( ns => ns.Name, ns => FilterBySourceFile.Apply(ns, IsGeneratedSourceFile)); var namespaces = compilation.Namespaces.Select(ns => toBeAdded.TryGetValue(ns.Name, out var add) ? new QsNamespace(ns.Name, ns.Elements.AddRange(add.Elements), ns.Documentation) : ns); var addedNamespaces = toBeAdded.Values.Where(add => !compilation.Namespaces.Any(ns => ns.Name.Value == add.Name.Value)); transformed = new QsCompilation(namespaces.Concat(addedNamespaces).ToImmutableArray(), compilation.EntryPoints); // mark all newly created callables that take unit as argument as unit tests to run on the QuantumSimulator and ResourcesEstimator bool IsSuitableForUnitTest(QsCallable c) => IsGeneratedSourceFile(c.SourceFile) && c.Signature.ArgumentType.Resolution.IsUnitType; var qsimAtt = AttributeUtils.BuildAttribute(BuiltIn.Test.FullName, AttributeUtils.StringArgument(Constants.QuantumSimulator)); var restAtt = AttributeUtils.BuildAttribute(BuiltIn.Test.FullName, AttributeUtils.StringArgument(Constants.ResourcesEstimator)); transformed = AttributeUtils.AddToCallables(transformed, (qsimAtt, IsSuitableForUnitTest), (restAtt, IsSuitableForUnitTest)); return(true); }