Developer guide Rule based extensions

Writing rule tests

To make it easy to write tests for transformation rules, we have created two components to ease the burden of writing tests: ConfigurableQirAdaptorFactory and IrManipulationTestHelper. The ConfigurableQirAdaptorFactory is a profile that is dynamically defined when instatiated through a configuration lambda function.

Creating the profile

Creating the profile using the ConfigurableQirAdaptorFactory is done by first defining the lambda function and then instantiating the ConfigurableQirAdaptorFactory with the lambda function to define the profile. Using the RuleFactory, a profile for transforming the single qubit allocations is created as follows:

  auto configure_profile = [](RuleSet &rule_set) {
    auto factory = RuleFactory(rule_set);

    factory.useStaticQubitAllocation();
  }

  auto profile = std::make_shared<ConfigurableQirAdaptorFactory>(std::move(configure_profile));

This profile is intended to transform

  %qubit = call %Qubit* @__quantum__rt__qubit_allocate()
  call void @__quantum__qis__h__body(%Qubit* %qubit)
  call void @__quantum__rt__qubit_release(%Qubit* %qubit)
  ret i8 0

by replacing all allocations with integers and stripping all release calls

  %qubit = inttoptr i64 0 to %Qubit*
  tail call void @__quantum__qis__h__body(%Qubit* %qubit)
  ret i8 0

Creating the IR

In order to assist the testing of the above profile, we create a helper class which defines the IR we want to work on. To this end we make use of IrManipulationTestHelper which provides a number of shorthand functions to generate and test IR transformations. We start by defining the IR:

  auto ir_manip = std::make_shared<IrManipulationTestHelper>();

  ir_manip->declareOpaque("Qubit");

  ir_manip->declareFunction("%Qubit* @__quantum__rt__qubit_allocate()");
  ir_manip->declareFunction("void @__quantum__rt__qubit_release(%Qubit*)");
  ir_manip->declareFunction("void @__quantum__qis__h__body(%Qubit*)");

  std::string script = R"script(
  %qubit = call %Qubit* @__quantum__rt__qubit_allocate()
  call void @__quantum__qis__h__body(%Qubit* %qubit)
  call void @__quantum__rt__qubit_release(%Qubit* %qubit)
  )script";

  assert(ir_manip->fromBodyString(script)); // Will fail if the IR is invalid

If we wish to verify the IR, we can print it by using the member function toString or by accessing the module directly:

  std::cout << ir_manip->toString() << std::endl;
  // OR
  llvm::errs() << *ir_manip->module() << "\n";

Applying the profile to the IR

The IrManipulationTestHelper contains a member function to run the profile on the IR to transform the module. The default behaviour of this helper function is to run without debug output at a O0 level to ensure that LLVM does not interfere with the intended test. The optimization level and debug mode can be changed through the function calls second and third argument, but for the sake of simplicity, we will assume we are using O0 here:

  ir_manip->applyProfile(profile);

This will run the above generated rule set on the IR we have supplied. At this point, we could print the IR to the screen and use LIT to perform that actually transformation test. However, to keep this test framework self-contained and easy to use, we supply LIT-like functionality. This has the benefit that the tests do not rely on Python and the LIT framework and that the tooling around the test is substantially simpler.

Testing the modified IR

Like before, we can investigate the IR by printing it and as such we could write a test that compared the full IR against an expected string. However, even minor changes in the IR (such as interchanged declarations) would break the test even if the changes would not change the semantics of the code. Instead, the IrManipulationTestHelper has another helper function hasInstructionSequence which allow us to scan for a sequence of instructions in the body of the main function. In our case, we expect following two instructions (in order):

  %qubit = inttoptr i64 0 to %Qubit*
  tail call void @__quantum__qis__h__body(%Qubit* %qubit)

The corresponding test code is as follows:

  EXPECT_TRUE(ir_manip->hasInstructionSequence({
    "%qubit = inttoptr i64 0 to %Qubit*",
    "tail call void @__quantum__qis__h__body(%Qubit* %qubit)"
  }));

By design, the test would pass as long as these two instructions are found (in order) within the full set of instructions of the function body. For instance, a valid IR for this test is

  call void printHelloWorld()
  %qubit = inttoptr i64 0 to %Qubit*
  %q2 = inttoptr i64 1 to %Qubit*
  %q3 = inttoptr i64 2 to %Qubit*
  tail call void @__quantum__qis__h__body(%Qubit* %q3)
  tail call void @__quantum__qis__h__body(%Qubit* %qubit)
  tail call void @__quantum__qis__h__body(%Qubit* %q2)

but would fail

  tail call void @__quantum__qis__h__body(%Qubit* %qubit)
  %qubit = inttoptr i64 0 to %Qubit*

and

  %qubit = inttoptr i64 0 to %Qubit*

as the first has the wrong order of the calls and the second is missing one instruction.