From 924488493eb123025e03b257cab408f6252154af Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Sun, 10 May 2026 19:39:50 +0100 Subject: [PATCH 1/3] perf: move BasicBlock type check from __iter__ to insertion methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isinstance check on every yielded item in BasicBlock.__iter__ was purely defensive — it detected invalid types that had already entered the list. Moving it to append/extend/insert/__setitem__ catches invalid types at the point of insertion instead, eliminating the check from the hot iteration path entirely. The structural checks (jump must be last, jump/TryBegin target must be a BasicBlock) remain in __iter__ as they depend on the full block context and cannot be verified at insertion time. Profiling data | Hotspot | Before | After | |---|---|---| | `BasicBlock.__iter__` own | 4.91% | 2.82% | | `ControlFlowGraph.from_bytecode` own | 4.61% | 4.04% | Throughput (Bytecode.from_code().to_code() on dis module's code object, 1 second timed window, 5 runs): | | r/s range | |---|---| | Before | 133–144 | | After | 142–144 | --- src/bytecode/cfg.py | 44 ++++++++++++++++++++++++++++++++++++++------ tests/test_cfg.py | 9 +++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/bytecode/cfg.py b/src/bytecode/cfg.py index 6bbdd632..507acbbf 100644 --- a/src/bytecode/cfg.py +++ b/src/bytecode/cfg.py @@ -42,18 +42,50 @@ def __init__( if instructions: super().__init__(instructions) + _VALID_TYPES = (SetLineno, Instr, TryBegin, TryEnd) + + @staticmethod + def _check_instr(instr: Any) -> None: + if not isinstance(instr, (SetLineno, Instr, TryBegin, TryEnd)): + raise ValueError( + "BasicBlock must only contain SetLineno and Instr objects, " + "but %s was found" % instr.__class__.__name__ + ) + + def append(self, instr: Union[Instr, SetLineno, TryBegin, TryEnd]) -> None: + self._check_instr(instr) + super().append(instr) + + def insert( + self, index: SupportsIndex, instr: Union[Instr, SetLineno, TryBegin, TryEnd] + ) -> None: + self._check_instr(instr) + super().insert(index, instr) + + def extend( + self, instrs: Iterable[Union[Instr, SetLineno, TryBegin, TryEnd]] + ) -> None: + instrs = list(instrs) + for instr in instrs: + self._check_instr(instr) + super().extend(instrs) + + def __setitem__(self, index, value): + if isinstance(index, slice): + values = list(value) + for instr in values: + self._check_instr(instr) + super().__setitem__(index, values) + else: + self._check_instr(value) + super().__setitem__(index, value) + def __iter__(self) -> Iterator[Union[Instr, SetLineno, TryBegin, TryEnd]]: index = 0 while index < len(self): instr = self[index] index += 1 - if not isinstance(instr, (SetLineno, Instr, TryBegin, TryEnd)): - raise ValueError( - "BasicBlock must only contain SetLineno and Instr objects, " - "but %s was found" % instr.__class__.__name__ - ) - if isinstance(instr, Instr) and instr.has_jump(): if index < len(self) and any( isinstance(self[i], Instr) for i in range(index, len(self)) diff --git a/tests/test_cfg.py b/tests/test_cfg.py index 1d29e7eb..41995a45 100644 --- a/tests/test_cfg.py +++ b/tests/test_cfg.py @@ -55,13 +55,14 @@ def disassemble( class BlockTests(unittest.TestCase): def test_iter_invalid_types(self): - # Labels are not allowed in basic blocks + # Labels are not allowed in basic blocks — caught at insertion time block = BasicBlock() - block.append(Label()) with self.assertRaises(ValueError): - list(block) + block.append(Label()) with self.assertRaises(ValueError): - block.legalize(1) + block.extend([Label()]) + with self.assertRaises(ValueError): + block.insert(0, Label()) # Only one jump allowed and only at the end block = BasicBlock() From 9f9f61e4e0654b8b479c35d1d540d7666449b7e1 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Mon, 11 May 2026 17:58:24 +0100 Subject: [PATCH 2/3] add setitem test --- tests/test_cfg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_cfg.py b/tests/test_cfg.py index 41995a45..71e09975 100644 --- a/tests/test_cfg.py +++ b/tests/test_cfg.py @@ -63,6 +63,11 @@ def test_iter_invalid_types(self): block.extend([Label()]) with self.assertRaises(ValueError): block.insert(0, Label()) + block.append(Instr("NOP")) + with self.assertRaises(ValueError): + block[0] = Label() + with self.assertRaises(ValueError): + block[:] = [Label()] # Only one jump allowed and only at the end block = BasicBlock() From 108f3ff79c1c1be5175b6ae36dd9ca8a926241c3 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 12 May 2026 10:24:44 +0100 Subject: [PATCH 3/3] add setitem happy paht tests --- tests/test_cfg.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cfg.py b/tests/test_cfg.py index 71e09975..7cb9be3f 100644 --- a/tests/test_cfg.py +++ b/tests/test_cfg.py @@ -69,6 +69,19 @@ def test_iter_invalid_types(self): with self.assertRaises(ValueError): block[:] = [Label()] + # Valid types are accepted via all insertion methods + nop = Instr("NOP") + block = BasicBlock() + block.append(nop) + self.assertEqual(block[0], nop) + block.insert(0, nop) + self.assertEqual(len(block), 2) + block.extend([nop]) + self.assertEqual(len(block), 3) + block[0] = nop + block[:] = [nop] + self.assertEqual(len(block), 1) + # Only one jump allowed and only at the end block = BasicBlock() block2 = BasicBlock()