Source code for qblox_scheduler.backends.qblox.control_flow_graph
# Repository: https://gitlab.com/qblox/packages/software/qblox-scheduler
# Licensed according to the LICENSE file on the main branch
#
# Copyright 2026, Qblox B.V.
"""Control-flow graph definition."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from qblox_scheduler.backends.qblox.helpers import to_grid_time
from qblox_scheduler.backends.qblox.operation_handling.base import IOperationStrategy
from qblox_scheduler.backends.qblox.operation_handling.virtual import (
ConditionalStrategy,
ControlFlowReturnStrategy,
LoopStrategy,
)
from qblox_scheduler.helpers.linked_list import DLinkedList
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
[docs]
class BasicBlock(DLinkedList[IOperationStrategy]):
"""
A sequence of instructions that does not branch.
The BasicBlock is constructed with pointers to its first and its last instruction in a
`DLinkedList` of instructions.
A basic block does not contain any control-flow between its first and last instruction, meaning
the flow of execution can only enter at the first instruction, and exit at the last.
"""
def __init__(self, unfinished_basic_block: _UnfinishedBasicBlock) -> None:
super().__init__(unfinished_basic_block)
[docs]
self.start_time_ns = unfinished_basic_block.start_time_ns
"""
The start time of this block. This is helpful information as we do not have explicit wait
operations.
"""
[docs]
self.end_time_ns = unfinished_basic_block.end_time_ns
"""
The end time of this block. This is helpful information as we do not have explicit wait
operations.
If the block is a loop, this is the time after 1 repetition finished, not the whole loop.
"""
[docs]
self.successors: list[BasicBlock] = []
"""The basic blocks that can directly succeed this block."""
@dataclass
[docs]
class _UnfinishedBasicBlock(DLinkedList):
"""An unfinished basic block, that is used during control-flow graph construction."""
def __init__(self, start_time_ns: int, predecessors: list[_UnfinishedBasicBlock]) -> None:
super().__init__()
[docs]
self.successors: list[_UnfinishedBasicBlock] = []
[docs]
self.predecessors: list[_UnfinishedBasicBlock] = predecessors
[docs]
self.start_time_ns: int = start_time_ns
[docs]
self.end_time_ns: int | None = None
[docs]
class InstructionsIteratorFactory:
"""Iterator factory for instructions built from an iterator of basic blocks."""
def __init__(self, basic_blocks: Iterable[BasicBlock]) -> None:
[docs]
self._basic_blocks = basic_blocks
def __iter__(self) -> Iterator[IOperationStrategy]:
for basic_block in self._basic_blocks:
yield from basic_block
@dataclass
[docs]
class ControlFlowGraph:
"""
An intermediate representation of the instructions for one sequencer, with explicit control-flow
information.
It consists of two doubly linked lists: one for all instructions in chronological order, and one
for basic blocks with pointers into the list of instructions.
"""
[docs]
basic_blocks: list[BasicBlock] = field(default_factory=list)
@property
[docs]
def instructions(self) -> Iterable[IOperationStrategy]:
"""Iterator for instructions."""
return InstructionsIteratorFactory(self.basic_blocks)
@property
[docs]
def instructions_len(self) -> int:
"""Number of instructions."""
return sum(len(b) for b in self.basic_blocks)
@classmethod
[docs]
def __parse_operations_recursively(
cls,
instructions_iter: Iterator[IOperationStrategy],
current_block: _UnfinishedBasicBlock,
unfinished_blocks: list[_UnfinishedBasicBlock],
) -> _UnfinishedBasicBlock:
start_basic_block = current_block
while (instr := next(instructions_iter, None)) is not None:
instr_start_time_ns = to_grid_time(instr.operation_info.timing)
if isinstance(instr, (LoopStrategy, ConditionalStrategy)):
# Finish off block, if this is not the very first instruction.
next_blocks_predecessors = []
if len(current_block) != 0:
if current_block.end_time_ns is None:
current_block.end_time_ns = instr_start_time_ns
unfinished_blocks.append(current_block)
next_blocks_predecessors.append(current_block)
# Create new unfinished block.
current_block = _UnfinishedBasicBlock(
start_time_ns=instr_start_time_ns,
predecessors=next_blocks_predecessors,
)
current_block.append(instr)
current_block = cls.__parse_operations_recursively(
instructions_iter=instructions_iter,
current_block=current_block,
unfinished_blocks=unfinished_blocks,
)
elif isinstance(instr, ControlFlowReturnStrategy):
# Process the control-flow we're ending.
cf_start_instr = start_basic_block[0]
next_block_start_time_ns = instr_start_time_ns
if isinstance(cf_start_instr, LoopStrategy):
# Create loopback link: add to successors.
current_block.successors.append(start_basic_block)
loop_body_duration = instr_start_time_ns - to_grid_time(
cf_start_instr.operation_info.timing
)
reps = cf_start_instr.operation_info.data["repetitions"]
next_block_start_time_ns += loop_body_duration * (reps - 1)
# Finish off block.
current_block.append(instr)
current_block.end_time_ns = instr_start_time_ns
unfinished_blocks.append(current_block)
# Create new unfinished block.
current_block = _UnfinishedBasicBlock(
start_time_ns=next_block_start_time_ns,
predecessors=[current_block],
)
return current_block
else:
current_block.append(instr)
return current_block
@classmethod
[docs]
def from_instructions(cls, instructions: list[IOperationStrategy]) -> ControlFlowGraph:
"""
Construct a control-flow graph from a chronologically ordered `IOperationStrategy` list.
Parameters
----------
instructions : list[IOperationStrategy]
A chronologically ordered list of `IOperationStrategy` objects.
Returns
-------
ControlFlowGraph
The control-flow graph representation of the instructions.
"""
unfinished_blocks = []
current_block = _UnfinishedBasicBlock(
start_time_ns=to_grid_time(instructions[0].operation_info.timing), predecessors=[]
)
current_block = cls.__parse_operations_recursively(
iter(instructions), current_block, unfinished_blocks
)
if len(current_block) != 0 and (
len(unfinished_blocks) == 0 or unfinished_blocks[-1] is not current_block
):
# Handle last basic block
# Could only happen if we were not in a loop or conditional
instr = current_block[-1]
current_block.end_time_ns = to_grid_time(
instr.operation_info.timing + instr.operation_info.duration
)
unfinished_blocks.append(current_block)
return cls.__finish_basic_blocks(unfinished_blocks)
@classmethod
[docs]
def __finish_basic_blocks(
cls, unfinished_blocks: list[_UnfinishedBasicBlock]
) -> ControlFlowGraph:
"""
The unfinished blocks reference other unfinished blocks in the predecessors and successors.
These must be corrected to point to already finished block objects.
"""
cfg = cls()
unfinished_to_finished_map: dict[int, BasicBlock] = {}
for unfinished_block in unfinished_blocks:
if unfinished_block.start_time_ns is None or unfinished_block.end_time_ns is None:
raise RuntimeError("Error creating control-flow graph.")
finished_block = BasicBlock(unfinished_block)
cfg.basic_blocks.append(finished_block)
unfinished_to_finished_map[id(unfinished_block)] = finished_block
for unfinished_block in unfinished_blocks:
finished_block = unfinished_to_finished_map[id(unfinished_block)]
for unfinished_successor in unfinished_block.successors:
finished_successor = unfinished_to_finished_map[id(unfinished_successor)]
if finished_successor not in finished_block.successors:
finished_block.successors.append(finished_successor)
for unfinished_predecessor in unfinished_block.predecessors:
finished_predecessor = unfinished_to_finished_map[id(unfinished_predecessor)]
if unfinished_block not in finished_predecessor.successors:
finished_predecessor.successors.append(finished_block)
return cfg