From 8bfe0f0179e1c4a92d5511c5c42e623fdbc62b92 Mon Sep 17 00:00:00 2001 From: Cassandra Granade Date: Tue, 22 Feb 2022 22:08:57 -0800 Subject: [PATCH 1/2] Started drafting fix for #594. --- .../IConfigurationSource.cs | 24 +++ src/Jupyter/SymbolResolver.cs | 29 ++- src/Kernel/IQSharpEngine.cs | 13 +- src/Kernel/SymbolEncoders.cs | 178 ++++++++++++++++++ 4 files changed, 234 insertions(+), 10 deletions(-) diff --git a/src/Jupyter/ConfigurationSource/IConfigurationSource.cs b/src/Jupyter/ConfigurationSource/IConfigurationSource.cs index 0def869a90..acb9885bd3 100644 --- a/src/Jupyter/ConfigurationSource/IConfigurationSource.cs +++ b/src/Jupyter/ConfigurationSource/IConfigurationSource.cs @@ -231,5 +231,29 @@ public TEnum GetOptionOrDefault(string optionName, TEnum defaultValue) wh /// public string WorkspaceName => GetOptionOrDefault("azure.quantum.workspace.name", string.Empty); + + // We also want to support some internal-only options that are disabled + // in release builds, making it easier to diagnose some internals + // during local development. When in release mode, we'll disable setting + // these options and always use defaults. + + #if DEBUG + public TEnum GetInternalOptionOrDefault(string optionName, TEnum defaultValue) where TEnum : struct => + GetOptionOrDefault("internal." + optionName, defaultValue, Enum.Parse); + public string GetInternalOptionOrDefault(string optionName, string defaultValue) => + GetOptionOrDefault("internal." + optionName, defaultValue, e => e); + public bool GetInternalOptionOrDefault(string optionName, bool defaultValue) => + GetOptionOrDefault("internal." + optionName, defaultValue, bool.Parse); + #else + public string GetInternalOptionOrDefault(string optionName, string defaultValue) => + defaultValue; + public TEnum GetInternalOptionOrDefault(string optionName, TEnum defaultValue) where TEnum : struct => + defaultValue; + public bool GetInternalOptionOrDefault(string optionName, bool defaultValue) => + defaultValue; + #endif + + public bool InternalHelpShowAllAttributes => + GetInternalOptionOrDefault("help.showAllAttributes", false); } } diff --git a/src/Jupyter/SymbolResolver.cs b/src/Jupyter/SymbolResolver.cs index 9e36434e07..a666d28bec 100644 --- a/src/Jupyter/SymbolResolver.cs +++ b/src/Jupyter/SymbolResolver.cs @@ -72,7 +72,16 @@ public class IQSharpSymbol : ISymbol /// /// [JsonProperty("inputs", NullValueHandling=NullValueHandling.Ignore)] - public ImmutableDictionary? Inputs { get; private set; } = null; + public ImmutableDictionary Inputs { get; private set; } + + /// + /// + [JsonProperty("examples", NullValueHandling=NullValueHandling.Ignore)] + public ImmutableList Examples { get; private set; } + + + [JsonProperty("type_parameters", NullValueHandling=NullValueHandling.Ignore)] + public ImmutableDictionary TypeParameters { get; private set; } // TODO: continue exposing documentation here. @@ -94,12 +103,20 @@ public IQSharpSymbol(OperationInfo op) .Operation .GetStringAttributes("Description") .SingleOrDefault(); - var inputs = this + this.Inputs = this + .Operation + .GetDictionaryAttributes("Input") + .ToImmutableDictionary(); + this.TypeParameters = this + .Operation + .GetDictionaryAttributes("TypeParameter") + .ToImmutableDictionary(); + this.Examples = this .Operation - .GetDictionaryAttributes("Input"); - this.Inputs = inputs.Count >= 0 - ? inputs.ToImmutableDictionary() - : null; + .GetStringAttributes("Example") + .Where(ex => ex != null) + .ToImmutableList(); + } } diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs index 4caca1cfc8..f8b16c556e 100644 --- a/src/Kernel/IQSharpEngine.cs +++ b/src/Kernel/IQSharpEngine.cs @@ -138,6 +138,11 @@ private void AttachCommsListeners() }; } + private void RegisterDisplayEncoder() + where T: IResultEncoder => + RegisterDisplayEncoder(ActivatorUtilities.CreateInstance(services)); + + private async Task StartAsync() { base.Start(); @@ -179,11 +184,11 @@ private async Task StartAsync() var references = await serviceTasks.References; logger.LogDebug("Registering IQ# display and JSON encoders."); - RegisterDisplayEncoder(new IQSharpSymbolToHtmlResultEncoder()); - RegisterDisplayEncoder(new IQSharpSymbolToTextResultEncoder()); + RegisterDisplayEncoder(); + RegisterDisplayEncoder(); RegisterDisplayEncoder(new TaskStatusToTextEncoder()); - RegisterDisplayEncoder(new StateVectorToHtmlResultEncoder(configurationSource)); - RegisterDisplayEncoder(new StateVectorToTextResultEncoder(configurationSource)); + RegisterDisplayEncoder(); + RegisterDisplayEncoder(); RegisterDisplayEncoder(new DataTableToHtmlEncoder()); RegisterDisplayEncoder(new DataTableToTextEncoder()); RegisterDisplayEncoder(new DisplayableExceptionToHtmlEncoder()); diff --git a/src/Kernel/SymbolEncoders.cs b/src/Kernel/SymbolEncoders.cs index a491315bbd..01ef0e856c 100644 --- a/src/Kernel/SymbolEncoders.cs +++ b/src/Kernel/SymbolEncoders.cs @@ -6,9 +6,125 @@ using Microsoft.Jupyter.Core; using Markdig; using Microsoft.Quantum.IQSharp.Jupyter; +using System.Linq; +using Microsoft.Quantum.QsCompiler.SyntaxTree; +using System; +using Microsoft.Quantum.QsCompiler.SyntaxTokens; +using System.Threading.Tasks; +using System.Net.Http; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.Kernel { + using ResolvedTypeKind = QsTypeKind; + + // NB: These are defined in the documentation generation tool in the + // compiler, and should not be duplicated here. These should be removed + // before merging to main. + internal static class SyntaxExtensions + { + internal static List<(string, ResolvedType)> InputDeclarations(this QsTuple> items) => items switch + { + QsTuple>.QsTuple tuple => + tuple.Item.SelectMany( + item => item.InputDeclarations()) + .ToList(), + QsTuple>.QsTupleItem item => + new List<(string, ResolvedType)> + { + ( + item.Item.VariableName switch + { + QsLocalSymbol.ValidName name => name.Item, + _ => "__invalid__", + }, + item.Item.Type), + }, + _ => throw new Exception(), + }; + + internal static string ToSyntax(this ResolvedCharacteristics characteristics) => + characteristics.SupportedFunctors switch + { + { IsNull: true } => "", + { Item: { Count: 0 } } => "", + + // Be sure to add the leading space before is! + { Item: var functors } => $" is {string.Join(" + ", functors.Select(functor => functor.ToSyntax()))}", + }; + + internal static string ToSyntax(this QsFunctor functor) => + functor.Tag switch + { + QsFunctor.Tags.Adjoint => "Adj", + QsFunctor.Tags.Controlled => "Ctl", + _ => "__invalid__", + }; + + // TODO: memoize + internal async static Task TryResolveXref(string xref) + { + var client = new HttpClient(); + try + { + var response = await client.GetStringAsync($"https://xref.docs.microsoft.com/query?uid={xref}"); + var json = JToken.Parse(response); + return json.Value("href"); + } + catch + { + return null; + } + } + + internal async static Task ToLink(string text, string xref, string? fragment = null) + { + var href = await TryResolveXref(xref); + if (href == null) + { + return text; + } + else + { + return $""; + } + } + + internal static async Task ToHtml(this ResolvedType type) => type.Resolution switch + { + ResolvedTypeKind.ArrayType array => $"{await array.Item.ToHtml()}[]", + ResolvedTypeKind.Function function => + $"{await function.Item1.ToHtml()} -> {await function.Item2.ToHtml()}", + ResolvedTypeKind.Operation operation => + $"{await operation.Item1.Item1.ToHtml()} => {await operation.Item1.Item2.ToHtml()} " + + operation.Item2.Characteristics.ToSyntax(), + ResolvedTypeKind.TupleType tuple => "(" + string.Join( + ",", tuple.Item.Select(async type => await type.ToHtml())) + ")", + ResolvedTypeKind.UserDefinedType udt => await udt.Item.ToHtml(), + ResolvedTypeKind.TypeParameter typeParam => + $"'{typeParam.Item.TypeName}", + _ => type.Resolution.Tag switch + { + ResolvedTypeKind.Tags.BigInt => await ToLink("BigInt", "xref:microsoft.quantum.qsharp.valueliterals", "bigint-literals"), + ResolvedTypeKind.Tags.Bool => await ToLink("Bool", "xref:microsoft.quantum.qsharp.valueliterals", "bool-literals"), + ResolvedTypeKind.Tags.Double => await ToLink("Double", "xref:microsoft.quantum.qsharp.valueliterals", "double-literals"), + ResolvedTypeKind.Tags.Int => await ToLink("Int", "xref:microsoft.quantum.qsharp.valueliterals", "int-literals"), + ResolvedTypeKind.Tags.Pauli => await ToLink("Pauli", "xref:microsoft.quantum.qsharp.valueliterals", "pauli-literals"), + ResolvedTypeKind.Tags.Qubit => await ToLink("Qubit", "xref:microsoft.quantum.qsharp.valueliterals", "qubit-literals"), + ResolvedTypeKind.Tags.Range => await ToLink("Range", "xref:microsoft.quantum.qsharp.valueliterals", "range-literals"), + ResolvedTypeKind.Tags.String => await ToLink("String", "xref:microsoft.quantum.qsharp.valueliterals", "string-literals"), + ResolvedTypeKind.Tags.UnitType => await ToLink("Unit", "xref:microsoft.quantum.qsharp.valueliterals", "unit-literal"), + ResolvedTypeKind.Tags.Result => await ToLink("Result", "xref:microsoft.quantum.qsharp.valueliterals", "result-literal"), + ResolvedTypeKind.Tags.InvalidType => "__invalid__", + _ => $"__invalid<{type.Resolution.ToString()}>__", + }, + }; + + internal static async Task ToHtml(this UserDefinedType type) => + await ToLink($"{type.Namespace}.{type.Name}", $"{type.Namespace}.{type.Name}"); + } + /// /// Encodes Q# symbols into plain text, e.g. for printing to the console. /// @@ -39,15 +155,24 @@ public class IQSharpSymbolToTextResultEncoder : IResultEncoder /// public class IQSharpSymbolToHtmlResultEncoder : IResultEncoder { + private readonly IConfigurationSource ConfigurationSource; + /// public string MimeType => MimeTypes.Html; + public IQSharpSymbolToHtmlResultEncoder(IConfigurationSource configurationSource) + { + this.ConfigurationSource = configurationSource; + } + /// /// Checks if a displayable object is an IQ# symbol, and if so, /// returns an encoding of that symbol into HTML. /// public EncodedData? Encode(object displayable) { + var tableEncoder = new TableToHtmlDisplayEncoder(); + if (displayable is IQSharpSymbol symbol) { var codeLink = @@ -58,10 +183,63 @@ public class IQSharpSymbolToHtmlResultEncoder : IResultEncoder var description = symbol.Description != null ? "
Description
" + Markdown.ToHtml(symbol.Description) : string.Empty; + // TODO: Make sure to list + // type parameters even if they're not documented. + var typeParams = symbol.TypeParameters.Count > 0 + ? "
Type Parameters
\n" + + tableEncoder.Encode(new Table> + { + Columns = new List<(string, Func, string>)> + { + ("", input => $"{input.Key}"), + ("", input => Markdown.ToHtml(input.Value)) + }, + Rows = symbol.TypeParameters.ToList() + })!.Value.Data + : string.Empty; + + // TODO: Check if Inputs is empty before formatting, make sure + // to list even if they're not documented. + var inputDecls = symbol.Operation.Header.ArgumentTuple.InputDeclarations().ToDictionary(item => item.Item1, item => item.Item2); + var inputs = symbol.Inputs.Count > 0 + ? "
Inputs
\n" + tableEncoder.Encode(new Table> + { + Columns = new List<(string, Func, string>)> + { + ("", input => $"{input.Key}"), + ("", input => $"{inputDecls[input.Key].ToHtml().Result}"), + ("", input => Markdown.ToHtml(input.Value)) + }, + Rows = symbol.Inputs.ToList() + })!.Value.Data + : string.Empty; + var examples = string.Join("\n", + symbol.Examples.Select(example => $"
Example
\n{Markdown.ToHtml(example)}") + ); + + var attributes = ConfigurationSource.InternalHelpShowAllAttributes + ? tableEncoder.Encode(new Table + { + Columns = new List<(string, Func)> + { + ("Name", attr => attr.TypeId switch + { + { Item: UserDefinedType udt } => $"{udt.Namespace}.{udt.Name}", + _ => "" + }), + ("Value", attr => attr.Argument.ToString()) + }, + Rows = symbol.Operation.Header.Attributes.ToList() + })!.Value.Data + : ""; return $@"

{symbol.Name} {codeLink}

{summary} {description} + {typeParams} + {inputs} + {examples} + {attributes} ".ToEncodedData(); } From 44772832ba889dc3ca42356c67387d9bc92b4d0b Mon Sep 17 00:00:00 2001 From: Cassandra Granade Date: Tue, 15 Mar 2022 15:15:34 -0700 Subject: [PATCH 2/2] Adapt telemetry tests to use async. --- src/Tests/TelemetryTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tests/TelemetryTests.cs b/src/Tests/TelemetryTests.cs index 1fd6f7bdfb..c679dc3c71 100644 --- a/src/Tests/TelemetryTests.cs +++ b/src/Tests/TelemetryTests.cs @@ -140,7 +140,7 @@ private static MockTelemetryService.MockAppLogger GetAppLogger(ServiceProvider s } [TestMethod] - public void WorkspaceReload() + public async Task WorkspaceReload() { var workspace = "Workspace"; var services = Startup.CreateServiceProvider(workspace); @@ -151,7 +151,7 @@ public void WorkspaceReload() logger.Events.Clear(); Assert.AreEqual(0, logger.Events.Count); - ws.Reload(); + await ws.Reload(); Assert.AreEqual(1, logger.Events.Count); Assert.AreEqual("Quantum.IQSharp.WorkspaceReload", logger.Events[0].Name); Assert.AreEqual(PiiKind.GenericData, logger.Events[0].PiiProperties["Quantum.IQSharp.Workspace"]); @@ -175,7 +175,7 @@ public void InvalidWorkspaceReload() logger.Events.Clear(); Assert.AreEqual(0, logger.Events.Count); - ws.Reload(); + await ws.Reload(); Assert.AreEqual(1, logger.Events.Count); Assert.AreEqual("Quantum.IQSharp.WorkspaceReload", logger.Events[0].Name); Assert.AreEqual(PiiKind.GenericData, logger.Events[0].PiiProperties["Quantum.IQSharp.Workspace"]); @@ -200,7 +200,7 @@ public void CompileCode() Assert.AreEqual(0, logger.Events.Count); var count = 0; - snippets.Compile(SNIPPETS.HelloQ); + await snippets.Compile(SNIPPETS.HelloQ); Assert.AreEqual(count + 1, logger.Events.Count); Assert.AreEqual("Quantum.IQSharp.Compile", logger.Events[count].Name); Assert.AreEqual("ok", logger.Events[count].Properties["Quantum.IQSharp.Status"]); @@ -287,7 +287,7 @@ public void LoadProjects() logger.Events.Clear(); Assert.AreEqual(0, logger.Events.Count); - ws.Reload(); + await ws.Reload(); Assert.AreEqual(5, logger.Events.Count); Assert.AreEqual("Quantum.IQSharp.PackageLoad", logger.Events[0].Name);