CIRCT

Circuit IR Compilers and Tools

Random Test Generation (RTG) Rationale

This dialect provides types, operations, and interfaces modeling randomization constructs for random test generation. Furthermore, it contains passes to perform and analyze the represented randomization.

Rationale 

This dialect aims to provide a unified representation for randomized tests, more precisely the parts of the tests that encode the randomization constructs (e.g., picking a random resource from a set of resources). This means, this dialect is only useful in combination with at least one other dialect that represents the actual test constructs, i.e., the parts that get randomized. After all the randomization constructs are fully elaborated, the resulting test will only consist of those dialects.

Examples for tests that can be randomized, and thus candidates for companion dialects, are instruction sequences of any ISA (Instruction Set Architecture), transaction sequences over a latency insensitive (ready-valid) channel, input sequences to an FSM (Finite State Machine), transaction sequences for potentially any other protocol. Albeit, the initial motivation is ISA tests and will thus be the best supported, at least initially.

While it should be valid to add constructs to this dialect that are special to any of the above mentioned use-cases, the dialect should generally be designed such that all of them can be supported.

Interfacing with the RTG dialect 

The RTG dialect is only useful in combination with at least one other dialect representing the test. Such a dialect has to be aware of the RTG dialect and has to implement various interfaces (depending on which parts of the RTG infrastructure it intends to use).

A test can be declared with the rtg.test operation which takes an !rtg.target type attribute to specify requirements for the test to be able to be generated and executed. In addition to the target having to provide an !rtg.target typed value that is a refinement of the required target type, the test can specify additional requirements using the rtg.require operation.

Targets can be defined using the rtg.target operation. Certain operations are only allowed inside the target operation but not the test operation to guarantee that certain resources (specifically contexts) cannot be generated on-the-fly.

The RTG framework will match all tests with all targets that fulfill their requirements. The user can specify which targets and tests should be included in the test generation process and how many tests should be generated per target or in total.

Currently, there are three concepts in the RTG dialect the companion dialects can map their constructs to:

  • Instructions: instructions/operations intended to be performed in series by the test, such as ISA instructions, or protocol transactions
  • Resources: resources the instructions operate on, such as registers or memories in ISA tests, or channels in hardware transaction tests
  • Contexts: the context or environment the instuctions are performed in, e.g., on which CPU and in which mode for ISA test.

To express their mapping, the companion dialects must implement the the interfaces as follows.

Instructions

They implement InstructionOpInterface on all operations that represent an instruction, transaction, or similar. Those operations are intended to be statements and not define SSA values.

Resources

Operations that create, declare, define, or produce a handle to a resource instance must implement the ResourceOpInterface interface and define at least one SSA value of a type that implements the ResourceTypeInterface interface. (TODO: maybe we don’t actually need the ResourceTypeInterface?)

The RegisterOpInterface can be implemented in addition to the ResourceOpInterface if the resource is a register to become supported by the register allocation and assembly emission pass.

Contexts

The ContextResourceType is used to represent contexts. Instructions can be placed in specific context using the rtg.on_context operation (Note: we might promote this to a type interface in the future). Operations that define contexts (i.e., create new contexts) must implement the ContextResourceOpInterface interface. Operations implementing the ContextResourceOpInterface interface are only allowed inside the rtg.target operation.

Randomization 

This dialect aims to provide utilities that allow users to generate tests anywhere on the spectrum from directed tests to (almost) fully random tests (refer to fuzzing).

  • Constraints: allow the user to specify constraints to avoid generating illegal or useless tests, e.g., by
    • allowing to specify a sequence of instructions that always have to be picked in exactly that order and form (see rtg.sequence operation)
    • dependencies between resources or resource usages
    • etc.
  • Probabilities and Biases: allow certain tests (or parts of a test) to be picked more likely than others (can be seen as ‘soft-constraints’) (see !rtg.bag type and associated operations)
  • Enumerations: allow to enumerate all tests that can be produced by the current randomness constraints, possibly in a way that places the more likely tests to occur earlier in the enumeration

(TODO: expand here once these things are built out)

Main IR constructs to introduce randomness:

  • Sets: a set of elements of the same type; the usual set operations apply as well as uniformly at random picking one element of the set
  • Bags/Biased Sets: a generalization of sets that allows one element to occur multiple times and thus make it more likely to be picked (i.e., models non-uniform distributions)

Example 

This section provides an (almost) E2E walkthrough of a simple example starting at a Python implmentation using the library wrapping around the python bindings, showing the generated RTG IR, and the fully elaborated IR. This compilation process can be performed in one go using the rtgtool.py driver script.

# Define a test target (essentially a design/machine with 4 CPUs)
@rtg.target([('cpus', rtg.set_of(rtg.context_resource()))])
def example_target():
  # return a set containing 4 CPUs which the test can schedule instruction
  # sequences on
  return [rtg.Set.create([rv64.core(0), rv64.core(1), rv64.core(2), rv64.core(3)])]

# Define a sequence (for simplicity it only contains one instruction)
# Note: not adding the sequence decorator is also valid in this example  but
# means the function is fully inlined at python execution time. It is not valid,
# however, if the sequence is added to a set or bag to be selected at random.
@rtg.sequence
def seq(register):
  # ADD Immediate instruction adding 4 to to the given register
  rtgtest.addi(register, rtgtest.imm(4, 12))

# Define a test that requires a target with CPUs to schedule instruction
# sequences on
@rtg.test([('cpus', rtg.set_of(rtg.context_resource()))])
def example(cpus):
  # Pick a random CPU and schedule the ADD operation on it
  with rtg.context(cpus.get_random_and_exclude()):
    rtg.label('label0')
    seq(rtgtest.sp())

  # Pick a random CPU that was not already picked above and zero out the stack
  # pointer register on it
  with rtg.context(cpus.get_random()):
    ra = rtgtest.sp()
    rtgtest.xor(ra, ra)

The driver script will elaborate this python file and produce the following RTG IR as an intermediate step:

rtg.target @example_target : !rtg.target<cpus: !rtg.set<!rtg.context_resource>> {
  %0 = rtgtest.coreid 0
  %1 = rtgtest.coreid 1
  %2 = rtgtest.coreid 2
  %3 = rtgtest.coreid 3
  %4 = rtg.set_create %0, %1, %2, %3 : !rtg.context_resource
  rtg.yield %4 : !rtg.set<!rtg.context_resource>
}
rtg.sequence @seq {
^bb0(%reg: !rtgtest.reg):
  %c4_i12 = arith.constant 4 : i12
  rtgtest.addi %reg, %c4_i12
}
rtg.sequence @context0 {
  // Labels are declared before being placed in the instruction stream such that
  // we can insert jump instructions before the jump target.
  %0 = rtg.label.decl "label0" -> index
  rtg.label %0 : index
  %sp = rtgtest.sp
  // Construct a closure such that it can be easily passed around, e.g.,
  // inserted into a set with other sequence closures to be selected at random.
  %1 = rtg.sequence_closure @seq(%sp) : !rtgtest.reg
  // Place the sequence here (i.e., inline it here with the arguments passed to
  // the closure).
  // This is essentially the same as an `rtg.on_context` with the context
  // operand matching the one of the parent `on_context`.
  rtg.sequence_invoke %1
}
rtg.sequence @context1 {
  %0 = rtgtest.sp
  rtgtest.xor %sp, %sp
}
rtg.test @example : !rtg.target<cpus: !rtg.set<!rtg.context_resource>> {
^bb0(%arg0: !rtg.set<!rtg.context_resource>):
  // Select an element from the set uniformly at random
  %0 = rtg.set_select_random %arg0 : !rtg.set<!rtg.context_resource>
  %3 = rtg.sequence_closure @context0
  // Place the sequence closure on the given context. In this example, there
  // will be guards inserted that make sure the inlined sequence is only
  // executed by the CPU specified by the selected coreid.
  rtg.on_context %0, %3 : !rtg.context_resource
  // Construct a new set that doesn't contain the selected element (RTG sets are
  // immutable) and select another element randomly from this new set.
  %1 = rtg.set_create %0 : !rtg.context_resource
  %2 = rtg.set_difference %arg0, %1 : !rtg.set<!rtg.context_resource>
  %7 = rtg.set_select_random %2 : !rtg.set<!rtg.context_resource>
  %8 = rtg.sequence_closure @context1
  rtg.on_context %7, %8 : !rtg.context_resource
}

Once all the RTG randomization passes were performed, the example looks like this:

// Two regions, the first one to be executed on CPU with coreid 0 and the second
// one on CPU with coreid 2
rtg.rendered_context [0,2] {
  %0 = rtg.label.decl "label0" -> index
  rtg.label %0 : index
  %reg = rtgtest.sp
  %c4 = arith.constant 4 : i12
  rtgtest.addi %sp, %c4
  // Is emitted to assembly looking something like:
  // label0:
  // addi sp, 4
}, {
  %sp = rtgtest.sp
  rtgtest.xor %sp, %sp
}

The last step to run this test is to print it in assembly format and invoke the assembler. This also includes the insertion of a considerable amount of boilerplate setup code to run the above instructions on the right CPUs which is omitted in this example for clarity.

Use-case-specific constructs 

This section provides an overview of operations/types/interfaces added with the intend to be only used for one (or a few) specific use-cases/test-targets.

ISA Tests 

  • Labels: Handling of labels is added to the RTG dialect because they are common across ISAs. Leaving them to the ISA specific companion dialects would likely lead to frequent code duplication (once for each ISA).
  • Register Allocation Pass
  • Assembly Emission Pass

Frontends 

Any dialect or entry point is allowed to generate valid RTG IR with a companion dialect. The dialect already comes with an extensive CAPI, Python Bindings, and a small Python library that simplify usage over the pure Python Bindings.

Backends 

The RTG dialect does not have a backend itself. It is fully lowered by its dialect transformation passes that perform the randomization. The result will be IR consisting purely of the companion dialect and thus it is up to this companion dialect to define any backends.

Operations 

[include “Dialects/RTGOps.md]

Types 

[include “Dialects/RTGTypes.md]