Merge pull request #117 from pleroy/Load2

Properly track the dependencies of the LOAD phase of instructions
This commit is contained in:
Jan
2025-08-12 16:14:34 +02:00
committed by GitHub
3 changed files with 136 additions and 74 deletions

View File

@@ -80,6 +80,8 @@ class Frontend(object):
s += lineno_filler + self._get_port_number_line(port_len) + "\n" s += lineno_filler + self._get_port_number_line(port_len) + "\n"
s += separator + "\n" s += separator + "\n"
for instruction_form in kernel: for instruction_form in kernel:
if KernelDG.is_load_line_number(instruction_form.line_number):
continue
line = "{:4d} {} {} {}".format( line = "{:4d} {} {} {}".format(
instruction_form.line_number, instruction_form.line_number,
self._get_port_pressure( self._get_port_pressure(
@@ -112,6 +114,8 @@ class Frontend(object):
""" """
s = "\n\nLatency Analysis Report\n-----------------------\n" s = "\n\nLatency Analysis Report\n-----------------------\n"
for instruction_form in cp_kernel: for instruction_form in cp_kernel:
if KernelDG.is_load_line_number(instruction_form.line_number):
continue
s += ( s += (
"{:4d} {} {:4.1f} {}{}{} {}".format( "{:4d} {} {:4.1f} {}{}{} {}".format(
instruction_form.line_number, instruction_form.line_number,
@@ -147,8 +151,11 @@ class Frontend(object):
) )
# TODO find a way to overcome padding for different tab-lengths # TODO find a way to overcome padding for different tab-lengths
for dep in sorted(dep_dict.keys()): for dep in sorted(dep_dict.keys()):
s += "{:4d} {} {:4.1f} {} {:36}{} {}\n".format( dep0 = float(dep.split("-")[0])
int(dep.split("-")[0]), if KernelDG.is_load_line_number(dep0):
continue
s += "{:4.0f} {} {:4.1f} {} {:36}{} {}\n".format(
dep0,
separator, separator,
dep_dict[dep]["latency"], dep_dict[dep]["latency"],
separator, separator,
@@ -356,6 +363,8 @@ class Frontend(object):
if show_cmnts is False and self._is_comment(instruction_form): if show_cmnts is False and self._is_comment(instruction_form):
continue continue
line_number = instruction_form.line_number line_number = instruction_form.line_number
if KernelDG.is_load_line_number(line_number):
continue
used_ports = [list(uops[1]) for uops in instruction_form.port_uops] used_ports = [list(uops[1]) for uops in instruction_form.port_uops]
used_ports = list(set([p for uops_ports in used_ports for p in uops_ports])) used_ports = list(set([p for uops_ports in used_ports for p in uops_ports]))
s += "{:4d} {}{} {} {}\n".format( s += "{:4d} {}{} {} {}\n".format(

View File

@@ -1,12 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import copy import copy
from enum import Enum
import time import time
from itertools import chain, groupby from itertools import chain, groupby
from multiprocessing import Manager, Process, cpu_count from multiprocessing import Manager, Process, cpu_count
import networkx as nx import networkx as nx
from osaca.semantics import INSTR_FLAGS, ArchSemantics, MachineModel from osaca.semantics import INSTR_FLAGS, ArchSemantics, MachineModel
from osaca.parser.instruction_form import InstructionForm
from osaca.parser.memory import MemoryOperand from osaca.parser.memory import MemoryOperand
from osaca.parser.register import RegisterOperand from osaca.parser.register import RegisterOperand
from osaca.parser.immediate import ImmediateOperand from osaca.parser.immediate import ImmediateOperand
@@ -17,6 +19,11 @@ class KernelDG(nx.DiGraph):
# threshold for checking dependency graph sequential or in parallel # threshold for checking dependency graph sequential or in parallel
INSTRUCTION_THRESHOLD = 50 INSTRUCTION_THRESHOLD = 50
class ReadKind(Enum):
NOT_A_READ = 0
READ_FOR_LOAD = 1
OTHER_READ = 2
def __init__( def __init__(
self, self,
parsed_kernel, parsed_kernel,
@@ -46,6 +53,25 @@ class KernelDG(nx.DiGraph):
dst_list.extend(tmp_list) dst_list.extend(tmp_list)
# print('Thread [{}-{}] done'.format(kernel[0]['line_number'], kernel[-1]['line_number'])) # print('Thread [{}-{}] done'.format(kernel[0]['line_number'], kernel[-1]['line_number']))
@staticmethod
def get_load_line_number(line_number):
# The line number of the load must be less than the line number of the instruction. The
# offset is irrelevant, but it must be a machine number with trailing zeroes to avoid silly
# rounding issues.
return line_number - 0.125
@staticmethod
def is_load_line_number(line_number):
return line_number != int(line_number)
@staticmethod
def get_real_line_number(line_number):
return (
int(line_number + 0.125)
if KernelDG.is_load_line_number(line_number)
else line_number
)
def create_DG(self, kernel, flag_dependencies=False): def create_DG(self, kernel, flag_dependencies=False):
""" """
Create directed graph from given kernel Create directed graph from given kernel
@@ -57,10 +83,10 @@ class KernelDG(nx.DiGraph):
:type flag_dependencies: boolean, optional :type flag_dependencies: boolean, optional
:returns: :class:`~nx.DiGraph` -- directed graph object :returns: :class:`~nx.DiGraph` -- directed graph object
""" """
# 1. go through kernel instruction forms and add them as node attribute # Go through kernel instruction forms and add them as nodes of the graph. Create a LOAD
# 2. find edges (to dependend further instruction) # node for instructions that include a memory reference.
# 3. get LT value and set as edge weight
dg = nx.DiGraph() dg = nx.DiGraph()
loads = {}
for i, instruction_form in enumerate(kernel): for i, instruction_form in enumerate(kernel):
dg.add_node(instruction_form.line_number) dg.add_node(instruction_form.line_number)
dg.nodes[instruction_form.line_number]["instruction_form"] = instruction_form dg.nodes[instruction_form.line_number]["instruction_form"] = instruction_form
@@ -70,14 +96,24 @@ class KernelDG(nx.DiGraph):
and INSTR_FLAGS.LD not in instruction_form.flags and INSTR_FLAGS.LD not in instruction_form.flags
): ):
# add new node # add new node
dg.add_node(instruction_form.line_number + 0.1) load_line_number = KernelDG.get_load_line_number(instruction_form.line_number)
dg.nodes[instruction_form.line_number + 0.1]["instruction_form"] = instruction_form loads[instruction_form.line_number] = load_line_number
dg.add_node(load_line_number)
dg.nodes[load_line_number]["instruction_form"] = InstructionForm(
mnemonic="_LOAD_",
line=instruction_form.line,
line_number=load_line_number
)
# and set LD latency as edge weight # and set LD latency as edge weight
dg.add_edge( dg.add_edge(
instruction_form.line_number + 0.1, load_line_number,
instruction_form.line_number, instruction_form.line_number,
latency=instruction_form.latency - instruction_form.latency_wo_load, latency=instruction_form.latency - instruction_form.latency_wo_load,
) )
# 1. find edges (to dependend further instruction)
# 2. get LT value and set as edge weight
for i, instruction_form in enumerate(kernel):
for dep, dep_flags in self.find_depending( for dep, dep_flags in self.find_depending(
instruction_form, kernel[i + 1 :], flag_dependencies instruction_form, kernel[i + 1 :], flag_dependencies
): ):
@@ -91,11 +127,18 @@ class KernelDG(nx.DiGraph):
edge_weight += self.model.get("store_to_load_forward_latency", 0) edge_weight += self.model.get("store_to_load_forward_latency", 0)
if "p_indexed" in dep_flags and self.model is not None: if "p_indexed" in dep_flags and self.model is not None:
edge_weight = self.model.get("p_index_latency", 1) edge_weight = self.model.get("p_index_latency", 1)
dg.add_edge( if "for_load" in dep_flags and self.model is not None and dep.line_number in loads:
instruction_form.line_number, dg.add_edge(
dep.line_number, instruction_form.line_number,
latency=edge_weight, loads[dep.line_number],
) latency=edge_weight,
)
else:
dg.add_edge(
instruction_form.line_number,
dep.line_number,
latency=edge_weight,
)
dg.nodes[dep.line_number]["instruction_form"] = dep dg.nodes[dep.line_number]["instruction_form"] = dep
return dg return dg
@@ -204,23 +247,17 @@ class KernelDG(nx.DiGraph):
for lat_sum, involved_lines in loopcarried_deps: for lat_sum, involved_lines in loopcarried_deps:
dict_key = "-".join([str(il[0]) for il in involved_lines]) dict_key = "-".join([str(il[0]) for il in involved_lines])
loopcarried_deps_dict[dict_key] = { loopcarried_deps_dict[dict_key] = {
"root": self._get_node_by_lineno(involved_lines[0][0]), "root": self._get_node_by_lineno(dg, involved_lines[0][0]),
"dependencies": [ "dependencies": [
(self._get_node_by_lineno(ln), lat) for ln, lat in involved_lines (self._get_node_by_lineno(dg, ln), lat) for ln, lat in involved_lines
], ],
"latency": lat_sum, "latency": lat_sum,
} }
return loopcarried_deps_dict return loopcarried_deps_dict
def _get_node_by_lineno(self, lineno, kernel=None, all=False): def _get_node_by_lineno(self, dg, lineno):
"""Return instruction form with line number ``lineno`` from kernel""" """Return instruction form with line number ``lineno`` from dg"""
if kernel is None: return dg.nodes[lineno]["instruction_form"]
kernel = self.kernel
result = [instr for instr in kernel if instr.line_number == lineno]
if not all:
return result[0]
else:
return result
def get_critical_path(self): def get_critical_path(self):
"""Find and return critical path after the creation of a directed graph.""" """Find and return critical path after the creation of a directed graph."""
@@ -229,21 +266,21 @@ class KernelDG(nx.DiGraph):
longest_path = nx.algorithms.dag.dag_longest_path(self.dg, weight="latency") longest_path = nx.algorithms.dag.dag_longest_path(self.dg, weight="latency")
# TODO verify that we can remove the next two lince due to earlier initialization # TODO verify that we can remove the next two lince due to earlier initialization
for line_number in longest_path: for line_number in longest_path:
self._get_node_by_lineno(int(line_number)).latency_cp = 0 self._get_node_by_lineno(self.dg, line_number).latency_cp = 0
# set cp latency to instruction # set cp latency to instruction
path_latency = 0.0 path_latency = 0.0
for s, d in nx.utils.pairwise(longest_path): for s, d in nx.utils.pairwise(longest_path):
node = self._get_node_by_lineno(int(s)) node = self._get_node_by_lineno(self.dg, s)
node.latency_cp = self.dg.edges[(s, d)]["latency"] node.latency_cp = self.dg.edges[(s, d)]["latency"]
path_latency += node.latency_cp path_latency += node.latency_cp
# add latency for last instruction # add latency for last instruction
node = self._get_node_by_lineno(int(longest_path[-1])) node = self._get_node_by_lineno(self.dg, longest_path[-1])
node.latency_cp = node.latency node.latency_cp = node.latency
if max_latency_instr.latency > path_latency: if max_latency_instr.latency > path_latency:
max_latency_instr.latency_cp = float(max_latency_instr.latency) max_latency_instr.latency_cp = float(max_latency_instr.latency)
return [max_latency_instr] return [max_latency_instr]
else: else:
return [x for x in self.kernel if x.line_number in longest_path] return [self._get_node_by_lineno(self.dg, x) for x in longest_path]
else: else:
# split to DAG # split to DAG
raise NotImplementedError("Kernel is cyclic.") raise NotImplementedError("Kernel is cyclic.")
@@ -284,15 +321,18 @@ class KernelDG(nx.DiGraph):
# print(" TO", instr_form.line, register_changes) # print(" TO", instr_form.line, register_changes)
if isinstance(dst, RegisterOperand): if isinstance(dst, RegisterOperand):
# read of register # read of register
if self.is_read(dst, instr_form): read_kind = self._read_kind(dst, instr_form)
if read_kind != KernelDG.ReadKind.NOT_A_READ:
dep_flags = []
if ( if (
dst.pre_indexed dst.pre_indexed
or dst.post_indexed or dst.post_indexed
or (isinstance(dst.post_indexed, dict)) or (isinstance(dst.post_indexed, dict))
): ):
yield instr_form, ["p_indexed"] dep_flags = ["p_indexed"]
else: if read_kind == KernelDG.ReadKind.READ_FOR_LOAD:
yield instr_form, [] dep_flags += ["for_load"]
yield instr_form, dep_flags
# write to register -> abort # write to register -> abort
if self.is_written(dst, instr_form): if self.is_written(dst, instr_form):
break break
@@ -363,11 +403,12 @@ class KernelDG(nx.DiGraph):
return self.dg.successors(line_number) return self.dg.successors(line_number)
return iter([]) return iter([])
def is_read(self, register, instruction_form): def _read_kind(self, register, instruction_form):
"""Check if instruction form reads from given register""" """Check if instruction form reads from given register. Returns a ReadKind."""
is_read = False is_read = False
for_load = False
if instruction_form.semantic_operands is None: if instruction_form.semantic_operands is None:
return is_read return KernelDG.ReadKind.NOT_A_READ
for src in chain( for src in chain(
instruction_form.semantic_operands["source"], instruction_form.semantic_operands["source"],
instruction_form.semantic_operands["src_dst"], instruction_form.semantic_operands["src_dst"],
@@ -377,10 +418,16 @@ class KernelDG(nx.DiGraph):
if isinstance(src, FlagOperand): if isinstance(src, FlagOperand):
is_read = self.parser.is_flag_dependend_of(register, src) or is_read is_read = self.parser.is_flag_dependend_of(register, src) or is_read
if isinstance(src, MemoryOperand): if isinstance(src, MemoryOperand):
is_memory_read = False
if src.base is not None: if src.base is not None:
is_read = self.parser.is_reg_dependend_of(register, src.base) or is_read is_memory_read = self.parser.is_reg_dependend_of(register, src.base)
if src.index is not None and isinstance(src.index, RegisterOperand): if src.index is not None and isinstance(src.index, RegisterOperand):
is_read = self.parser.is_reg_dependend_of(register, src.index) or is_read is_memory_read = (
self.parser.is_reg_dependend_of(register, src.index)
or is_memory_read
)
for_load = is_memory_read
is_read = is_read or is_memory_read
# Check also if read in destination memory address # Check also if read in destination memory address
for dst in chain( for dst in chain(
instruction_form.semantic_operands["destination"], instruction_form.semantic_operands["destination"],
@@ -391,7 +438,16 @@ class KernelDG(nx.DiGraph):
is_read = self.parser.is_reg_dependend_of(register, dst.base) or is_read is_read = self.parser.is_reg_dependend_of(register, dst.base) or is_read
if dst.index is not None: if dst.index is not None:
is_read = self.parser.is_reg_dependend_of(register, dst.index) or is_read is_read = self.parser.is_reg_dependend_of(register, dst.index) or is_read
return is_read if is_read:
if for_load:
return KernelDG.ReadKind.READ_FOR_LOAD
else:
return KernelDG.ReadKind.OTHER_READ
else:
return KernelDG.ReadKind.NOT_A_READ
def is_read(self, register, instruction_form):
return self._read_kind(register, instruction_form) != KernelDG.ReadKind.NOT_A_READ
def is_memload(self, mem, instruction_form, register_changes={}): def is_memload(self, mem, instruction_form, register_changes={}):
"""Check if instruction form loads from given location, assuming register_changes""" """Check if instruction form loads from given location, assuming register_changes"""
@@ -546,19 +602,9 @@ class KernelDG(nx.DiGraph):
graph.nodes[n]["penwidth"] = 4 graph.nodes[n]["penwidth"] = 4
# Make critical path edges bold. # Make critical path edges bold.
for e in graph.edges: for u, v in zip(cp_line_numbers[:-1], cp_line_numbers[1:]):
if ( graph.edges[u, v]["style"] = "bold"
graph.nodes[e[0]]["instruction_form"].line_number in cp_line_numbers graph.edges[u, v]["penwidth"] = 3
and graph.nodes[e[1]]["instruction_form"].line_number in cp_line_numbers
and e[0] < e[1]
):
bold_edge = True
for i in range(e[0] + 1, e[1]):
if i in cp_line_numbers:
bold_edge = False
if bold_edge:
graph.edges[e]["style"] = "bold"
graph.edges[e]["penwidth"] = 3
# Color the cycles created by loop-carried dependencies, longest first, never recoloring # Color the cycles created by loop-carried dependencies, longest first, never recoloring
# any node or edge, so that the longest LCD and most long chains that are involved in the # any node or edge, so that the longest LCD and most long chains that are involved in the
@@ -600,8 +646,8 @@ class KernelDG(nx.DiGraph):
graph.nodes[n]["style"] += ",filled" graph.nodes[n]["style"] += ",filled"
graph.nodes[n]["fillcolor"] = color graph.nodes[n]["fillcolor"] = color
if ( if (
(max_color >= 4 and color in (1, max_color)) or (max_color >= 4 and color in (1, max_color))
(max_color >= 10 and color in (1, 2, max_color - 1 , max_color)) or (max_color >= 10 and color in (1, 2, max_color - 1 , max_color))
): ):
graph.nodes[n]["fontcolor"] = "white" graph.nodes[n]["fontcolor"] = "white"
for (u, v), color in edge_colors.items(): for (u, v), color in edge_colors.items():
@@ -613,21 +659,17 @@ class KernelDG(nx.DiGraph):
# rename node from [idx] to [idx mnemonic] and add shape # rename node from [idx] to [idx mnemonic] and add shape
mapping = {} mapping = {}
for n in graph.nodes: for n in graph.nodes:
if int(n) != n: node = graph.nodes[n]["instruction_form"]
mapping[n] = "{}: LOAD".format(int(n)) if node.mnemonic is not None:
mapping[n] = "{}: {}".format(KernelDG.get_real_line_number(n), node.mnemonic)
else:
label = "label" if node.label is not None else None
label = "directive" if node.directive is not None else label
label = "comment" if node.comment is not None and label is None else label
mapping[n] = "{}: {}".format(n, label)
graph.nodes[n]["fontname"] = "italic" graph.nodes[n]["fontname"] = "italic"
graph.nodes[n]["fontsize"] = 11.0 graph.nodes[n]["fontsize"] = 11.0
else: if not KernelDG.is_load_line_number(n):
node = graph.nodes[n]["instruction_form"]
if node.mnemonic is not None:
mapping[n] = "{}: {}".format(n, node.mnemonic)
else:
label = "label" if node.label is not None else None
label = "directive" if node.directive is not None else label
label = "comment" if node.comment is not None and label is None else label
mapping[n] = "{}: {}".format(n, label)
graph.nodes[n]["fontname"] = "italic"
graph.nodes[n]["fontsize"] = 11.0
graph.nodes[n]["shape"] = "rectangle" graph.nodes[n]["shape"] = "rectangle"
nx.relabel.relabel_nodes(graph, mapping, copy=False) nx.relabel.relabel_nodes(graph, mapping, copy=False)

View File

@@ -89,6 +89,9 @@ class TestSemanticTools(unittest.TestCase):
cls.machine_model_csx = MachineModel( cls.machine_model_csx = MachineModel(
path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "csx.yml") path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "csx.yml")
) )
cls.machine_model_skx = MachineModel(
path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "skx.yml")
)
cls.machine_model_tx2 = MachineModel( cls.machine_model_tx2 = MachineModel(
path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "tx2.yml") path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "tx2.yml")
) )
@@ -107,6 +110,11 @@ class TestSemanticTools(unittest.TestCase):
cls.machine_model_csx, cls.machine_model_csx,
path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "isa/x86.yml"), path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "isa/x86.yml"),
) )
cls.semantics_skx_intel = ArchSemantics(
cls.parser_x86_intel,
cls.machine_model_skx,
path_to_yaml=os.path.join(cls.MODULE_DATA_DIR, "isa/x86.yml"),
)
cls.semantics_aarch64 = ISASemantics(cls.parser_AArch64) cls.semantics_aarch64 = ISASemantics(cls.parser_AArch64)
cls.semantics_tx2 = ArchSemantics( cls.semantics_tx2 = ArchSemantics(
cls.parser_AArch64, cls.parser_AArch64,
@@ -136,10 +144,10 @@ class TestSemanticTools(unittest.TestCase):
for i in range(len(cls.kernel_x86_intel)): for i in range(len(cls.kernel_x86_intel)):
cls.semantics_csx_intel.assign_src_dst(cls.kernel_x86_intel[i]) cls.semantics_csx_intel.assign_src_dst(cls.kernel_x86_intel[i])
cls.semantics_csx_intel.assign_tp_lt(cls.kernel_x86_intel[i]) cls.semantics_csx_intel.assign_tp_lt(cls.kernel_x86_intel[i])
cls.semantics_csx_intel.normalize_instruction_forms(cls.kernel_x86_intel_memdep) cls.semantics_skx_intel.normalize_instruction_forms(cls.kernel_x86_intel_memdep)
for i in range(len(cls.kernel_x86_intel_memdep)): for i in range(len(cls.kernel_x86_intel_memdep)):
cls.semantics_csx_intel.assign_src_dst(cls.kernel_x86_intel_memdep[i]) cls.semantics_skx_intel.assign_src_dst(cls.kernel_x86_intel_memdep[i])
cls.semantics_csx_intel.assign_tp_lt(cls.kernel_x86_intel_memdep[i]) cls.semantics_skx_intel.assign_tp_lt(cls.kernel_x86_intel_memdep[i])
cls.semantics_tx2.normalize_instruction_forms(cls.kernel_AArch64) cls.semantics_tx2.normalize_instruction_forms(cls.kernel_AArch64)
for i in range(len(cls.kernel_AArch64)): for i in range(len(cls.kernel_AArch64)):
cls.semantics_tx2.assign_src_dst(cls.kernel_AArch64[i]) cls.semantics_tx2.assign_src_dst(cls.kernel_AArch64[i])
@@ -458,7 +466,7 @@ class TestSemanticTools(unittest.TestCase):
# / / # / /
# 4 / # 4 /
# / # /
# 5.1 # 4.875
# #
dg = KernelDG( dg = KernelDG(
self.kernel_x86_intel, self.kernel_x86_intel,
@@ -473,8 +481,8 @@ class TestSemanticTools(unittest.TestCase):
self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=4)), 5) self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=4)), 5)
self.assertEqual(len(list(dg.get_dependent_instruction_forms(line_number=5))), 1) self.assertEqual(len(list(dg.get_dependent_instruction_forms(line_number=5))), 1)
self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=5)), 6) self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=5)), 6)
self.assertEqual(len(list(dg.get_dependent_instruction_forms(line_number=5.1))), 1) self.assertEqual(len(list(dg.get_dependent_instruction_forms(line_number=4.875))), 1)
self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=5.1)), 5) self.assertEqual(next(dg.get_dependent_instruction_forms(line_number=4.875)), 5)
self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=6)), []) self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=6)), [])
self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=7)), []) self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=7)), [])
self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=8)), []) self.assertEqual(list(dg.get_dependent_instruction_forms(line_number=8)), [])
@@ -502,12 +510,15 @@ class TestSemanticTools(unittest.TestCase):
dg = KernelDG( dg = KernelDG(
self.kernel_x86_intel_memdep, self.kernel_x86_intel_memdep,
self.parser_x86_intel, self.parser_x86_intel,
self.machine_model_csx, self.machine_model_skx,
self.semantics_csx_intel, self.semantics_skx_intel,
) )
self.assertTrue(nx.algorithms.dag.is_directed_acyclic_graph(dg.dg)) self.assertTrue(nx.algorithms.dag.is_directed_acyclic_graph(dg.dg))
self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=3)), {6, 8}) self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=3)), {6, 8})
self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=5)), {10, 12}) self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=5)), {10, 12})
self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=18)), {18.875})
self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=18.875)), {19})
self.assertEqual(set(dg.get_dependent_instruction_forms(line_number=19)), set())
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
dg.get_dependent_instruction_forms() dg.get_dependent_instruction_forms()
# test dot creation # test dot creation