Creating Q# Compiler Rewrite Steps

18 minute read

🎄 This post is part of the Q# Advent Calendar 2020. 🎅

Q# compiler exposes a very powerful, yet little documented extensibility point called rewrite steps, allowing developers to broaden the capabilities of the compiler beyond what comes “in the box”. In this post, we will look at the IRewriteStep interface and learn how you can write your own rewrite steps using some basic examples.

Background

IRewriteStep interface serves as a mechanism enabling three primary scenarios:

  • read-only inspection of Q# compilation at build time, e.g. to create external documentation. In those cases, the compilation is left intact and the extensibility point serves as a hook to traverse the syntax trees of the compilation only
  • active transformation a Q# compilation form one state to another e.g. by generating additional code. In those cases, the compilation gets modified, allowing the user to engage in metaprogramming or other source generation activities
  • emitting of diagnostics, e.g. to prevent the build process from completing unless certain criteria is met

The interface definition is as follows (the comments in the code below come directly from the Q# compiler source code:

public interface IRewriteStep
{
    /// <summary>
    /// User facing name identifying the rewrite step used for logging and in diagnostics.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// The priority of the transformation relative to other transformations within the same dll or package.
    /// Steps with a larger priority number have higher priority and will be executed first.
    /// </summary>
    public int Priority { get; }

    /// <summary>
    /// Dictionary that will be populated by the Q# compiler when the rewrite step is loaded.
    /// It contains the assembly constants for the Q# compilation unit on which the rewrite step is acting.
    /// </summary>
    public IDictionary<string, string?> AssemblyConstants { get; }

    /// <summary>
    /// Contains diagnostics generated by the rewrite step and intended for display to the user.
    /// Depending on the specified build configuration, the generated diagnostics may be queried
    /// after all implemented interface methods have been executed.
    /// </summary>
    public IEnumerable<Diagnostic> GeneratedDiagnostics { get; }

    /// <summary>
    /// If a precondition verification is implemented, that verification is executed prior to executing anything else.
    /// If the verification fails, nothing further is executed and the rewrite step is terminated.
    /// </summary>
    public bool ImplementsPreconditionVerification { get; }

    /// <summary>
    /// Indicates whether or not the rewrite step intends to modify the compilation in any form.
    /// If a transformation is implemented, then that transformation will be executed only if either
    /// no precondition verification is implemented, or the implemented precondition verification succeeds.
    /// </summary>
    public bool ImplementsTransformation { get; }

    /// <summary>
    /// A postcondition verification provides the means for diagnostics generation and detailed checks after transformation.
    /// The verification is executed only if the precondition verification passes and after applying the implemented transformation (if any).
    /// </summary>
    public bool ImplementsPostconditionVerification { get; }

    /// <summary>
    /// Verifies whether a given compilation satisfies the precondition for executing this rewrite step.
    /// <see cref="ImplementsPreconditionVerification"/> indicates whether or not this method is implemented.
    /// If the precondition verification succeeds, then the invocation of an implemented transformation (if any)
    /// with the given compilation should complete without throwing an exception.
    /// The precondition verification should never throw an exception,
    /// but instead indicate if the precondition is satisfied via the returned value.
    /// More detailed information can be provided via logging.
    /// </summary>
    /// <param name="compilation">Q# compilation for which to verify the precondition.</param>
    /// <returns>Whether or not the given compilation satisfies the precondition.</returns>
    public bool PreconditionVerification(QsCompilation compilation);

    /// <summary>
    /// Implements a rewrite step transforming a Q# compilation.
    /// <see cref="ImplementsTransformation"/> indicates whether or not this method is implemented.
    /// The transformation should complete without throwing an exception
    /// if no precondition verification is implemented or the implemented verification passes.
    /// </summary>
    /// <param name="compilation">Q# compilation that satisfies the implemented precondition, if any.</param>
    /// <param name="transformed">Q# compilation after transformation. This value should not be null if the transformation succeeded.</param>
    /// <returns>Whether or not the transformation succeeded.</returns>
    public bool Transformation(QsCompilation compilation, [NotNullWhen(true)] out QsCompilation? transformed);

    /// <summary>
    /// Verifies whether a given compilation satisfies the postcondition after executing the implemented transformation (if any).
    /// <see cref="ImplementsPostconditionVerification"/> indicates whether or not this method is implemented.
    /// The verification may be omitted for performance reasons depending on the build configuration.
    /// The postcondition verification should never throw an exception,
    /// but instead indicate if the postcondition is satisfied via the returned value.
    /// More detailed information can be displayed to the user by generating suitable diagnostics.
    /// </summary>
    /// <param name="compilation">Q# compilation after performing the implemented transformation.</param>
    /// <returns>Whether or not the given compilation satisfies the postcondition of the transformation.</returns>
    public bool PostconditionVerification(QsCompilation compilation);
}

The three key methods to implement when building a rewrite step are:

  • bool PreconditionVerification(QsCompilation compilation) - for precondition checks, typically used to emit diagnostics and check the compilation before a transformation is applied
  • bool PostconditionVerification(QsCompilation compilation) - for postcondition checks, typically used to emit diagnostics and check the compilation after a transformation is applied
  • bool Transformation(QsCompilation compilation, out QsCompilation transformed) - for the actual transformation (syntax tree rewriting) of the Q# code base

Each of the above has a control flag represented by three properties - ImplementsPreconditionVerification, ImplementsPostconditionVerification and ImplementsTransformation.

Internally, the Q# compiler builds plenty of its own features around the IRewriteStep infrastructure. The one that stands out the most, and is used by every Q# developer on each local build, is the process of generating C# code to execute the Q# program in a simulated environment - on a classical computer. The QDK ships with a built-in step called CSharpGeneration, which is responsible for generating C# simulation code and is nothing more than an IRewriteStep too. It is also worth noting, that in a remarkably polyglot fashion, the generation of C# out of Q# is actually done by code written in F#. Many other compiler features are based on rewrite steps too - documentation generation, monomorphization, control flow substitution with quantum operations or merging of environment-specific syntax trees.

Rewrite steps are discovered by the compiler by scanning through all of the loaded assemblies and looking for implementations of the IRewriteStep interface. The relevant discovered types are then incorporated into the compiler pipeline. Because of that, using a custom rewrite step is very simple - it is enough to have a rewrite step accessible publicly in one of the referenced assemblies - regardless whether it is a project-to-project reference within a user’s program or a NuGet package reference. For more advanced users, invoking the compiler programmatically, I have recently contributed a more sophisticated way of controlling rewrite steps in the Q# compiler. This allows users to pass in rewrite steps as already instantiated objects or type definitions, skipping the assembly scanning discovery process altogether.

One final thing worth mentioning is that depending on how the compiler is invoked, the rewrite steps may or may not participate in the compilation pipeline. They get included during command line build invocations, however for performance reasons they are not participating in design time builds. This means that their effects are not visible in the IDE in real time. This is something to keep in mind - especially when considering the diagnostics you’d want to emit from a rewrite step.

Example - convention based access modifiers

Of course the real power of rewrite steps is that they are exposed to us, users of the Q# language and the QDK. This allows us to build very powerful add-ons to the language, tailoring it our needs and demands. Let’s build a sample rewrite step together - but first, we need a little background on access modifiers.

In Q#, the default visibility of operations, functions and types is public. In fact, there is no such access modifier in the language - it is actually the lack of an access modifier that gets inferred to treat a given callable or type as public. The only access modifier currently available in the language is internal, and it can be used to hide a type/callable from external consumers of our Q# code - be it another Q# program (in case of Q# libraries) or an application written in a general purpose language such as C#, that integrates quantum features via calls into Q#. Such approach to access modifiers is more in-line with F# language design (which also uses a public-by-default paradigm) than with C# (internal-by-default), and therefore may be something that needs getting used to if you come from a strong C# background.

Let’s use the IRewriteStep extensibility infrastructure to create a custom approach to access modifiers. Instead of having to choose between the default behavior (public) and the presence of the internal keyword, we will want to use convention based accessibility using the following rules:

  • if the callable’s first letter is capitalized, it should always be public
  • if the callable’s first letter is not-capitalized, it should always be internal

For the case of simplicity of this example, we will restrict ourselves to callables only, but the logic discussed could easily be extrapolated onto handling of user-defined types too. In order to achieve all of that, we will create a new netstandard2.1 C# library project, referencing the quantum compiler.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Quantum.Compiler" Version="0.14.2011120240" />
  </ItemGroup>

</Project>

Notice that contrary to writing Q# code, the reference to the QDK is not necessary. Next, we will create a new rewrite step with a name CaseBasedModifiersRewriteStep, naturally implementing the IRewriteStep interface, that will have the following outline:

public class CaseBasedModifiersRewriteStep : IRewriteStep
{
    private readonly List<IRewriteStep.Diagnostic> _diagnostics = new List<IRewriteStep.Diagnostic>();
    public string Name => "Case-based Access Modifiers";
    public int Priority => 0;
    public IDictionary<string, string?> AssemblyConstants { get; } = new Dictionary<string, string?>();
    public IEnumerable<IRewriteStep.Diagnostic> GeneratedDiagnostics => _diagnostics;
    
    public bool ImplementsPreconditionVerification => false;
    public bool ImplementsTransformation => true;
    public bool ImplementsPostconditionVerification => false;

    public bool PreconditionVerification(QsCompilation compilation)
        throw new NotImplementedException();

    public bool Transformation(QsCompilation compilation, out QsCompilation transformed)
    {
        // TO DO
        return true;
    }

    public bool PostconditionVerification(QsCompilation compilation) =>
        throw new NotImplementedException();
}

For the time being, we shall ignore both PreconditionVerification and PostconditionVerification and focus on implementing the Transformation method. The premise is simple. We will need to find all of the user defined callables in the Q# compilation, check their names and - if needed - modify the syntax trees, so that the modifier of the callable is adjusted according to the rules that we laid out above.

To achieve that, we will create a helper type, that will act as a syntax tree walker through our Q# code. It will subclass the SyntaxTreeTransformation<T> type which is part of Microsoft.Quantum.QsCore. It is an extremely useful built-in feature, implementing the classical visitor pattern, commonly found in compilers. By creating our own SyntaxTreeTransformation<T>, we are going to be able to run custom code inspecting and rewriting Q# statements, expressions, types or namespaces. Since walking through the syntax tree is a non-linear activity, there needs to be a separate way to hold the state accumulated during the walk - should we require to persist or extract certain information out of the syntax nodes. This is exactly the role of the T parameter - as it represents any state holding type (we could refer to it as TState). SyntaxTreeTransformation<T> exposes a single method OnCompilation, which takes in a Q# compilation object and returns a new, updated Q# compilation, containing the rewritten syntax trees.

We’ll call our type RewriteAccessModifiers, and its outline is shown below.

public class RewriteAccessModifiers : SyntaxTreeTransformation<RewriteAccessModifiers.EmptyState>
{
    public RewriteAccessModifiers() : base(new EmptyState())
    {
        Namespaces = new NamespaceTransformation(this);
    }

    private class NamespaceTransformation : NamespaceTransformation<EmptyState>
    {
        public NamespaceTransformation(SyntaxTreeTransformation<EmptyState> parent)
            : base(parent, TransformationOptions.Default)
        {
        }

	    public override QsCallable OnCallableDeclaration(QsCallable c)
	    {
	        // TO DO
	    }
	}
}	

public class EmptyState { }

Since we do not need to care about any state in this particular use case, our state holder can be an empty type that I called EmptyState. Our RewriteAccessModifiers will need to find callables only and as such will only supply logic to deal with namespaces, ignoring any syntax tree traversal of e.g. expressions or statements. We engage into namespace inspection by setting the Namespaces property to an instance of NamespaceTransformation<TState>. This is a specialized syntax tree walker that’s part of the QDK, that will find for us all of callables and expose them using its OnCallableDeclaration method which is overridable. In order to run any logic for a discovered callable, we need to override OnCallableDeclaration, which means we need to subclass NamespaceTransformation<TState> too, which is exactly what we did in the snippet above. The class doesn’t necessarily have to be private as it was done here, but that is a de facto convention that has been established by the various rewrite steps in the Q# compiler already.

The next piece needed to be added here, is to perform actual transformation of the callables inside OnCallableDeclaration. To do so, we need to be aware of one particularly interesting aspect of this rewriting process. Using the above syntax tree walkers, we will not only gain to access our own callables - the ones defined in our Q# code - but also all of the ones defined in all of the runtime-level Q# libraries too. This is due to how Q# compiler translates Q# code, including Q# libraries, but it’s probably a topic for a separate post. Overall, however, this is a very powerful premise, as it gives you a chance to rewrite not only your own code, but the core libraries as well! In order to restrict the processing to our own declared Q# code, we will look at files with .qs extension only - all the other core library code would come from files loaded as .dll.

public override QsCallable OnCallableDeclaration(QsCallable c)
{
	if (!c.SourceFile.EndsWith(".qs", StringComparison.OrdinalIgnoreCase)) return c;
	Console.WriteLine($"Callable: {c.FullName} in {c.SourceFile}");
	var transformed = c.ToCaseDriveAccessModifier();
	Console.WriteLine($" modifier: {c.Modifiers.Access} -> modifier: {transformed.Modifiers.Access}");
	return transformed;
}

The actual callable transformation happens inside a custom extension method ToCaseDriveAccessModifier. Additionally, the code does some basic console logging which will help us see and track the work done by the rewrite step easier.

public static QsCallable ToCaseDriveAccessModifier(this QsCallable c) 
{
	var accessModifier = char.IsUpper(c.FullName.Name[0]) ? AccessModifier.DefaultAccess : AccessModifier.Internal;
	if (accessModifier.IsDefaultAccess == c.Modifiers.Access.IsDefaultAccess) return c;

	return new QsCallable(
		c.Kind, c.FullName, c.Attributes, new Modifiers(accessModifier),
		c.SourceFile, c.Location,
		c.Signature, c.ArgumentTuple, c.Specializations,
		c.Documentation, c.Comments);
}

ToCaseDriveAccessModifier checks for the first character in the callable’s name, and if it is upper-cased, it assigns a mandatory AccessModifier.DefaultAccess (so, public) modifier to it, otherwise it will select AccessModifier.Internal. If there is a need to rewrite a callable, or, to put it differently, if we notice a need to switch the modifier to a different one, a new callable is created, where all the properties of the old one are copied over, except for the modifiers.

The final step is to wire in the RewriteAccessModifiers into the rewrite step we created earlier - CaseBasedModifiersRewriteStep. At this point it’s very simple - all we need to do, is to new up our RewriteAccessModifiers and invoke the OnCompilation method that we mentioned earlier on. This is shown below.

public bool Transformation(QsCompilation compilation, out QsCompilation transformed)
{
	var rewriter = new RewriteAccessModifiers();
	transformed = rewriter.OnCompilation(compilation);

	return true;
}

Trying it out

Let’s try out our rewrite step on a tiny little Q# project. It won’t do anything particularly interesting - but it will declare some callables so that we can see the transformation in action. The code is shown below.

namespace Strathweb.QSharp.Rewrite.Demos {
    
    open Microsoft.Quantum.Intrinsic;

    @EntryPoint()
    operation Main() : Unit {
        let message = "World";
        Message($"Hello {message}");
    }

    function add(n : Int, m : Int) : Int {
        return n + m;
    }

    operation bellState(q1 : Qubit, q2 : Qubit) : Unit is Adj {
        H(q1);
        CNOT(q1, q2);
    }
}

The three callables are:

  • Main operation, marked as entry point
  • add function
  • bellState operation

All three use the default (public) modifier, however the last two should become internal based on our rewrite convention.

As we mentioned before, we can reference a rewrite step from a Q# project as a NuGet package or using a project reference. We’ll use the latter here. The project file for our demo application is shown below.

<Project Sdk="Microsoft.Quantum.Sdk/0.14.2011120240">

  <PropertyGroup>
    <QscVerbosity>Diagnostic</QscVerbosity>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\RewritePlugin\RewritePlugin.csproj" IsQscReference="true" />
  </ItemGroup>
 
</Project>

As a reminder, in order to use project-to-project from a Q# project, we mark them as IsQscReference="true". Setting QscVerbosity to diagnostic ensures that we will have detailed output from the compiler to look at.

At this point, we can simply invoke the compiler by running dotnet build (it will just compile the code). As a result, we should see the following Q# compiler output:

  Determining projects to restore...
  All projects are up-to-date for restore.
  RewritePlugin -> /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/RewritePlugin/bin/Debug/netstandard2.1/RewritePlugin.dll
  Resolved qsc reference: /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/RewritePlugin/bin/Debug/netstandard2.1/RewritePlugin.dll (priority )
  Resolved qsc reference: /Users/filip/.nuget/packages/microsoft.quantum.csharpgeneration/0.14.2011120240/build//../lib/netstandard2.1/Microsoft.Quantum.CsharpGeneration.dll (priority -1)
  : info: Compiling with command line arguments
      ResponseFiles: obj/qsharp/config/qsc.rsp
      OutputFolder: (null)
      ProjectName: (null)
      EmitDll: False
      PerfFolder: (null)
      TrimLevel: 1
      Plugins: 
      TargetSpecificDecompositions: 
      ExposeReferencesViaTestNames: False
      AdditionalAssemblyProperties: 
      RuntimeCapabilites: Unknown
      MakeExecutable: False
      Verbosity: Diagnostic
      OutputFormat: MsBuild
      Input: 
      CodeSnippet: (null)
      WithinFunction: False
      References: 
      NoWarn: System.Int32[]
      PackageLoadFallbackFolders: 
  : info: Loaded rewrite steps that are executing as part of the compilation process
      Case-based Access Modifiers (file:///Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/RewritePlugin/bin/Debug/netstandard2.1/RewritePlugin.dll)
      CsharpGeneration (file:///Users/filip/.nuget/packages/microsoft.quantum.csharpgeneration/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.CsharpGeneration.dll)
  : info: Compiling source files
      /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
  : info: Compiling with referenced assemblies
      /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/RewritePlugin/bin/Debug/netstandard2.1/RewritePlugin.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsCompilationManager.dll
      /Users/filip/.nuget/packages/microsoft.quantum.qsharp.core/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QSharp.Core.dll
      /Users/filip/.nuget/packages/azure.storage.blobs/12.2.0/lib/netstandard2.0/Azure.Storage.Blobs.dll
      /Users/filip/.nuget/packages/azure.identity/1.1.0/lib/netstandard2.0/Azure.Identity.dll
      /Users/filip/.nuget/packages/numsharp/0.20.5/lib/netstandard2.0/NumSharp.Core.dll
      /Users/filip/.nuget/packages/microsoft.quantum.simulators/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.Simulators.dll
      /Users/filip/.nuget/packages/microsoft.quantum.runtime.core/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.Runtime.Core.dll
      /Users/filip/.nuget/packages/microsoft.azure.quantum.client/0.14.2011120240/lib/netstandard2.1/Microsoft.Azure.Quantum.Client.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsOptimizations.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsDataStructures.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.BondSchemas.dll
      /Users/filip/.nuget/packages/microsoft.bcl.asyncinterfaces/1.1.0/ref/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll
      /Users/filip/.nuget/packages/microsoft.quantum.simulators/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.Simulation.Common.dll
      /Users/filip/.nuget/packages/windowsazure.storage/9.3.3/lib/netstandard1.3/Microsoft.WindowsAzure.Storage.dll
      /Users/filip/.nuget/packages/microsoft.identity.client/4.13.0/ref/netcoreapp2.1/Microsoft.Identity.Client.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsCompiler.dll
      /Users/filip/.nuget/packages/microsoft.quantum.entrypointdriver/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.EntryPointDriver.dll
      /Users/filip/.nuget/packages/microsoft.rest.clientruntime/2.3.21/lib/netstandard2.0/Microsoft.Rest.ClientRuntime.dll
      /Users/filip/.nuget/packages/azure.core/1.0.1/lib/netstandard2.0/Azure.Core.dll
      /Users/filip/.nuget/packages/microsoft.quantum.simulators/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.Simulation.QCTraceSimulatorRuntime.dll
      /Users/filip/.nuget/packages/microsoft.identity.client.extensions.msal/2.10.0-preview/lib/netcoreapp2.1/Microsoft.Identity.Client.Extensions.Msal.dll
      /Users/filip/.nuget/packages/microsoft.quantum.standard/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.Standard.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsSyntaxProcessor.dll
      /Users/filip/.nuget/packages/microsoft.rest.clientruntime.azure/3.3.19/lib/netstandard2.0/Microsoft.Rest.ClientRuntime.Azure.dll
      /Users/filip/.nuget/packages/azure.storage.common/12.1.1/lib/netstandard2.0/Azure.Storage.Common.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsTextProcessor.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsDocumentationParser.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsCore.dll
      /Users/filip/.nuget/packages/microsoft.quantum.compiler/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.QsTransformations.dll
  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.add in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: Internal
  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.bellState in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: Internal
  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.Main in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: DefaultAccess
  
  ____________________________________________
  
  Q#: Success! (0 errors, 0 warnings) 
  
  C# files to compile: obj/qsharp/src/Program.EntryPoint.g.cs;obj/qsharp/src/Program.g.cs
  Demo -> /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/bin/Debug/netcoreapp3.1/Demo.dll

The output is quite verbose, so let’s filter it a little. For us, the most relevant parts are here:

 : info: Loaded rewrite steps that are executing as part of the compilation process
      Case-based Access Modifiers (file:///Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/RewritePlugin/bin/Debug/netstandard2.1/RewritePlugin.dll)
      CsharpGeneration (file:///Users/filip/.nuget/packages/microsoft.quantum.csharpgeneration/0.14.2011120240/lib/netstandard2.1/Microsoft.Quantum.CsharpGeneration.dll)

This shows us that our rewrite step was successfully loaded, along the built-in CsharpGeneration step. The specific output from our step also shows up:

  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.add in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: Internal
  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.bellState in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: Internal
  Callable declaration found: Strathweb.QSharp.Rewrite.Demos.Main in /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs
   old modifier: DefaultAccess, new modifier: DefaultAccess

In other words - everything worked as expected! Both add and bellState were transformed into internals based on the naming convention, while Main was left intact. We can even peek into the generated C# code (into the Program.g.cs class), which should show us that indeed an internal access modifier was applied upon C# generation for the simulator. For the sake of brevity, we don’t need to show all of it here, but the important parts are the generated type declarations:

public partial class Main : Operation<QVoid, QVoid>, ICallable
...
internal partial class add : Function<(Int64,Int64), Int64>, ICallable
...
internal partial class bellState : Adjointable<(Qubit,Qubit)>, ICallable

Precondition verification

We have seen rewrite steps working nicely with the transformation flow. Let’s now additionally incorporate precondition verification. With that, our rewrite step will be able to not only transform Q# code, but also emit diagnostics, that will be surfaced to the user at build time.

Going back to our previous example, recall that we have rewritten the visibility modifiers of the callables based on the predefined conventions. Since this happens silently, the user doesn’t really know about this - except for our console logging, which is probably not the most elegant way of handling this. Therefore, let’s now add warning diagnostics to be clearly emitted to the user in the precondition verification step. We’ll inform the user that the modifiers do not match the convention in some places, and that they will be automatically transformed.

To achieve that, we need to update our CaseBasedModifiersRewriteStep so that ImplementsPreconditionVerification now returns true. At this point, we are now free to implement the PreconditionVerification step, which is done below.

public bool ImplementsPreconditionVerification => true;

public bool PreconditionVerification(QsCompilation compilation)
{
     var callablesWithInvalidAccessibility = compilation.Namespaces.Callables()
        .Where(c => c.SourceFile.EndsWith(".qs"))
        .Where(c => c.Modifiers.Access.IsDefaultAccess && !char.IsUpper(c.FullName.Name[0]) || 
                            c.Modifiers.Access.IsInternal && char.IsUpper(c.FullName.Name[0]));

     foreach (var callable in callablesWithInvalidAccessibility)
     {
          _diagnostics.Add(new IRewriteStep.Diagnostic
          {
               Severity = DiagnosticSeverity.Warning,
               Message = $@"Callable '{callable.FullName}' should be {(callable.Modifiers.Access.IsDefaultAccess ? "internal" : "public" )}. This will be auto-corrected.",
               Stage = IRewriteStep.Stage.PreconditionVerification,
               Source = callable.SourceFile,
               Range = callable.Location.IsValue ? callable.Location.Item.Range : null
          });
     }

     return true;
}

What we do here is we use compilation.Namespaces.Callables() method to locate all callables within the compilation, filter them by the .qs extension (the reasoning is same as earlier, to restrict this to our own files only) and then locate those callables that violate our modifiers convention. We then proceed to create a diagnostic at a warning level for each of the matched callable, informing the user that they violate the convention rules and will be re-written automatically.

Everything else in the rewrite step can stay the same - we do not need to touch the transformation logic. If we now execute the Q# project build again, we should not only see that the emitted output was already transformed according to our requirements, but that the warning diagnostics appear too, at the end of the compiler’s output.

/Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs(1,10): warning : [PreconditionVerification] Callable 'Strathweb.QSharp.Rewrite.Demos.add' should be internal. This will be auto-corrected. [/Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Demo.csproj]
/Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Program.qs(1,11): warning : [PreconditionVerification] Callable 'Strathweb.QSharp.Rewrite.Demos.bellState' should be internal. This will be auto-corrected. [/Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/Demo.csproj]
  
  ____________________________________________
  
  Q#: Success! (0 errors, 2 warnings) 
  
  C# files to compile: obj/qsharp/src/Program.EntryPoint.g.cs;obj/qsharp/src/Program.g.cs
  Demo -> /Users/filip/Documents/dev/Strathweb.QSharp.Rewrite.Demos/Demo/bin/Debug/netcoreapp3.1/Demo.dll

Build succeeded.

Summary

In this blog post we discussed how to implement a custom rewrite step for the Q# compiler. It is a bit more advanced feature of the compiler, but one that can really facilitate the growth of the tooling landscape around Q#, as the possibilities with rewrite steps and the plugins that could be built with it are almost limitless. Hopefully I have managed to encourage you to try this extensibility point out, and extend the QDK with your own custom features.

All the code for this article is available on Github.