Download src/runtime/machine/bytecode_assembler.py from Snapkitty/sovereign-engine-v2: direct link, hf CLI and curl.
- Browser
- Download file 59.8 kB
-
https://huggingface.co/Snapkitty/sovereign-engine-v2/resolve/main/src/runtime/machine/bytecode_assembler.py
- Command line
-
hf download hf://Snapkitty/sovereign-engine-v2/src/runtime/machine/bytecode_assembler.py
-
curl -L -o bytecode_assembler.py https://huggingface.co/Snapkitty/sovereign-engine-v2/resolve/main/src/runtime/machine/bytecode_assembler.py
59.8 kB
| """ | |
| bytecode_assembler.py — CPython 3.12 bytecode assembler for the Sovereign engine. | |
| Operates at the raw opcode level, producing types.CodeType objects and raw bytecode | |
| bytes. Supports label-based forward/backward jumps, constant pooling, and disassembly. | |
| Part of the SOVEREIGN_IR PYTHON_C_BRIDGE_IR pipeline. | |
| Agent A (Cognition) — HyperKittyConstraintDSL v1.0 | |
| """ | |
| from __future__ import annotations | |
| import dis | |
| import io | |
| import math | |
| import struct | |
| import types | |
| from dataclasses import dataclass, field | |
| from enum import IntEnum | |
| from typing import Any, Optional | |
| # --------------------------------------------------------------------------- | |
| # Opcode definitions — CPython 3.12 values drawn from dis.opmap | |
| # --------------------------------------------------------------------------- | |
| class Opcode(IntEnum): | |
| """CPython 3.12 opcode enumeration with correct integer values.""" | |
| # Misc / no-arg | |
| NOP = 9 | |
| RESUME = 149 | |
| CACHE = 0 | |
| # Stack manipulation | |
| POP_TOP = 1 | |
| COPY = 120 | |
| SWAP = 99 | |
| # Unary ops | |
| UNARY_NEGATIVE = 11 | |
| UNARY_NOT = 12 | |
| UNARY_INVERT = 15 | |
| GET_ITER = 68 | |
| GET_YIELD_FROM_ITER = 69 | |
| # Binary / in-place ops | |
| BINARY_SUBSCR = 25 | |
| STORE_SUBSCR = 60 | |
| DELETE_SUBSCR = 61 | |
| BINARY_OP = 122 | |
| # Comparisons | |
| COMPARE_OP = 107 | |
| IS_OP = 117 | |
| CONTAINS_OP = 118 | |
| # Attributes | |
| LOAD_ATTR = 106 | |
| STORE_ATTR = 95 | |
| DELETE_ATTR = 96 | |
| LOAD_SUPER_ATTR = 141 | |
| # Names / globals / builtins | |
| LOAD_NAME = 101 | |
| STORE_NAME = 90 | |
| DELETE_NAME = 91 | |
| LOAD_GLOBAL = 116 | |
| STORE_GLOBAL = 97 | |
| DELETE_GLOBAL = 98 | |
| LOAD_DEREF = 137 | |
| STORE_DEREF = 125 | |
| DELETE_DEREF = 138 | |
| COPY_FREE_VARS = 149 # alias | |
| # Fast locals | |
| LOAD_FAST = 124 | |
| LOAD_FAST_CHECK = 127 | |
| LOAD_FAST_AND_CLEAR = 126 | |
| STORE_FAST = 125 | |
| DELETE_FAST = 126 | |
| # Constants | |
| LOAD_CONST = 100 | |
| # Collections | |
| BUILD_TUPLE = 102 | |
| BUILD_LIST = 103 | |
| BUILD_SET = 104 | |
| BUILD_MAP = 105 | |
| BUILD_CONST_KEY_MAP = 156 | |
| BUILD_SLICE = 133 | |
| BUILD_STRING = 157 | |
| LIST_APPEND = 45 | |
| SET_ADD = 146 | |
| MAP_ADD = 147 | |
| LIST_EXTEND = 162 | |
| SET_UPDATE = 163 | |
| DICT_MERGE = 164 | |
| DICT_UPDATE = 165 | |
| BUILD_DICT = 105 # alias | |
| # Unpacking | |
| UNPACK_SEQUENCE = 92 | |
| UNPACK_EX = 94 | |
| # Jumps | |
| JUMP_FORWARD = 110 | |
| JUMP_BACKWARD = 140 | |
| JUMP_BACKWARD_NO_INTERRUPT = 134 | |
| POP_JUMP_IF_TRUE = 115 | |
| POP_JUMP_IF_FALSE = 114 | |
| POP_JUMP_IF_NONE = 128 | |
| POP_JUMP_IF_NOT_NONE = 129 | |
| JUMP_IF_TRUE_OR_POP = 112 | |
| JUMP_IF_FALSE_OR_POP = 111 | |
| # Calls | |
| CALL = 171 | |
| CALL_FUNCTION_EX = 142 | |
| PUSH_NULL = 2 | |
| PRECALL = 166 | |
| KW_NAMES = 172 | |
| # Functions / closures | |
| MAKE_FUNCTION = 132 | |
| RETURN_VALUE = 83 | |
| RETURN_CONST = 121 | |
| YIELD_VALUE = 86 | |
| YIELD_FROM = 72 | |
| SEND = 123 | |
| # Exceptions | |
| RAISE_VARARGS = 130 | |
| POP_EXCEPT = 89 | |
| PUSH_EXC_INFO = 35 | |
| CHECK_EXC_MATCH = 36 | |
| WITH_EXCEPT_START = 49 | |
| BEFORE_WITH = 53 | |
| # Imports | |
| IMPORT_NAME = 108 | |
| IMPORT_FROM = 109 | |
| IMPORT_STAR = 84 | |
| # Generators / coroutines | |
| GET_AWAITABLE = 73 | |
| GET_AITER = 75 | |
| GET_ANEXT = 76 | |
| END_ASYNC_FOR = 54 | |
| BEFORE_ASYNC_WITH = 52 | |
| ASYNC_GEN_WRAP = 55 | |
| # Format / match | |
| FORMAT_VALUE = 155 | |
| MATCH_MAPPING = 27 | |
| MATCH_SEQUENCE = 28 | |
| MATCH_KEYS = 29 | |
| MATCH_CLASS = 152 | |
| COPY_DICT_WITHOUT_KEYS = 34 | |
| GET_LEN = 30 | |
| # Misc | |
| PRINT_EXPR = 70 | |
| SETUP_ANNOTATIONS = 85 | |
| LOAD_BUILD_CLASS = 71 | |
| LOAD_CLASSDEREF = 148 | |
| FOR_ITER = 68 # alias — same as GET_ITER in enum | |
| END_FOR = 4 | |
| def from_name(cls, name: str) -> 'Opcode': | |
| """Look up opcode by name, falling back to dis.opmap.""" | |
| try: | |
| return cls[name] | |
| except KeyError: | |
| val = dis.opmap.get(name) | |
| if val is None: | |
| raise KeyError(f"Unknown opcode: {name}") | |
| return cls(val) | |
| def has_arg(self) -> bool: | |
| """Return True if this opcode takes an argument (value >= dis.HAVE_ARGUMENT).""" | |
| return int(self) >= dis.HAVE_ARGUMENT | |
| # --------------------------------------------------------------------------- | |
| # Data structures | |
| # --------------------------------------------------------------------------- | |
| class Instruction: | |
| """A single bytecode instruction with optional label and line info.""" | |
| opcode: Opcode | |
| arg: int = 0 | |
| label: Optional[str] = None # label this instruction is the target of | |
| lineno: int = 0 | |
| # resolved absolute target offset (for jumps) | |
| _resolved_target: int = field(default=-1, repr=False, compare=False) | |
| def __post_init__(self): | |
| if isinstance(self.opcode, int): | |
| self.opcode = Opcode(self.opcode) | |
| def is_jump(self) -> bool: | |
| return self.opcode in _JUMP_OPCODES | |
| def encoded_size(self) -> int: | |
| """Return the number of bytes this instruction takes (2 or more).""" | |
| if self.arg <= 0xFF: | |
| return 2 | |
| elif self.arg <= 0xFFFF: | |
| return 4 | |
| elif self.arg <= 0xFFFFFF: | |
| return 6 | |
| else: | |
| return 8 | |
| def encode(self) -> bytes: | |
| """Encode to bytes using EXTENDED_ARG for large arguments.""" | |
| out = bytearray() | |
| arg = self.arg | |
| if arg > 0xFFFFFF: | |
| out.append(144) # EXTENDED_ARG | |
| out.append((arg >> 24) & 0xFF) | |
| if arg > 0xFFFF: | |
| out.append(144) | |
| out.append((arg >> 16) & 0xFF) | |
| if arg > 0xFF: | |
| out.append(144) | |
| out.append((arg >> 8) & 0xFF) | |
| out.append(int(self.opcode)) | |
| out.append(arg & 0xFF) | |
| return bytes(out) | |
| class Label: | |
| """Named label marking a position in the bytecode stream.""" | |
| name: str | |
| position: int = -1 # byte offset, filled in after assembly | |
| def is_resolved(self) -> bool: | |
| return self.position >= 0 | |
| # Jump opcodes that need target resolution | |
| _JUMP_OPCODES = frozenset({ | |
| Opcode.JUMP_FORWARD, | |
| Opcode.JUMP_BACKWARD, | |
| Opcode.JUMP_BACKWARD_NO_INTERRUPT, | |
| Opcode.POP_JUMP_IF_TRUE, | |
| Opcode.POP_JUMP_IF_FALSE, | |
| Opcode.POP_JUMP_IF_NONE, | |
| Opcode.POP_JUMP_IF_NOT_NONE, | |
| Opcode.JUMP_IF_TRUE_OR_POP, | |
| Opcode.JUMP_IF_FALSE_OR_POP, | |
| Opcode.FOR_ITER, | |
| }) | |
| # --------------------------------------------------------------------------- | |
| # InstructionBuffer | |
| # --------------------------------------------------------------------------- | |
| class InstructionBuffer: | |
| """Mutable sequence of Instructions with label resolution.""" | |
| def __init__(self): | |
| self._instructions: list[Instruction] = [] | |
| self._labels: dict[str, Label] = {} # name -> Label | |
| self._pending_jumps: list[tuple[int, str]] = [] # (instr_index, label_name) | |
| def append(self, instr: Instruction) -> None: | |
| self._instructions.append(instr) | |
| def add_label(self, name: str) -> Label: | |
| lbl = Label(name) | |
| self._labels[name] = lbl | |
| # Mark the next instruction's position once we know it | |
| return lbl | |
| def mark_label_here(self, name: str) -> None: | |
| """Associate label with the current (next) instruction index.""" | |
| offset = self._compute_offset(len(self._instructions)) | |
| lbl = self._labels.setdefault(name, Label(name)) | |
| lbl.position = offset | |
| # If any prior instruction referred to this label, patch it | |
| if self._instructions and self._instructions[-1].label == name: | |
| pass # already set | |
| def _compute_offset(self, up_to_index: int) -> int: | |
| total = 0 | |
| for i, instr in enumerate(self._instructions): | |
| if i >= up_to_index: | |
| break | |
| total += instr.encoded_size() | |
| return total | |
| def resolve_labels(self) -> None: | |
| """ | |
| Two-pass resolution: | |
| Pass 1: compute byte offsets for all labels. | |
| Pass 2: patch jump instruction args. | |
| """ | |
| # Pass 1: build offset table | |
| offset = 0 | |
| for instr in self._instructions: | |
| if instr.label is not None: | |
| lbl = self._labels.get(instr.label) | |
| if lbl is not None and lbl.position < 0: | |
| lbl.position = offset | |
| offset += instr.encoded_size() | |
| # Pass 2: patch jumps | |
| offset = 0 | |
| for instr in self._instructions: | |
| if instr.is_jump() and isinstance(instr.arg, str): | |
| label_name = instr.arg # type: ignore[assignment] | |
| lbl = self._labels.get(label_name) | |
| if lbl is None or not lbl.is_resolved(): | |
| raise AssemblerError(f"Unresolved label: {label_name}") | |
| after_instr = offset + instr.encoded_size() | |
| if instr.opcode == Opcode.JUMP_FORWARD: | |
| instr.arg = (lbl.position - after_instr) // 2 | |
| elif instr.opcode == Opcode.JUMP_BACKWARD: | |
| instr.arg = (after_instr - lbl.position) // 2 | |
| else: | |
| # absolute for conditional jumps in 3.12 (offset/2) | |
| instr.arg = lbl.position // 2 | |
| offset += instr.encoded_size() | |
| def to_bytes(self) -> bytes: | |
| self.resolve_labels() | |
| out = bytearray() | |
| for instr in self._instructions: | |
| out.extend(instr.encode()) | |
| return bytes(out) | |
| def size(self) -> int: | |
| return sum(i.encoded_size() for i in self._instructions) | |
| def __len__(self) -> int: | |
| return len(self._instructions) | |
| def __iter__(self): | |
| return iter(self._instructions) | |
| # --------------------------------------------------------------------------- | |
| # Assembler error | |
| # --------------------------------------------------------------------------- | |
| class AssemblerError(Exception): | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # BytecodeAssembler — main class | |
| # --------------------------------------------------------------------------- | |
| class BytecodeAssembler: | |
| """ | |
| High-level Python bytecode assembler. | |
| Builds a sequence of CPython instructions, manages constant/name pools, | |
| and can produce a raw bytecode bytes object or a full types.CodeType. | |
| """ | |
| def __init__(self, name: str = "<assembled>", filename: str = "<string>"): | |
| self.name = name | |
| self.filename = filename | |
| self._buf = InstructionBuffer() | |
| self._consts: list[Any] = [] | |
| self._varnames: list[str] = [] | |
| self._names: list[str] = [] # globals / attributes | |
| self._freevars: list[str] = [] | |
| self._cellvars: list[str] = [] | |
| self._lineno_table: list[tuple[int, int]] = [] # (offset, lineno) | |
| self._current_lineno = 1 | |
| self._label_counter = 0 | |
| self._stack_depth = 0 | |
| self._max_stack = 0 | |
| # --- internal helpers --------------------------------------------------- | |
| def _push(self, n: int = 1) -> None: | |
| self._stack_depth += n | |
| if self._stack_depth > self._max_stack: | |
| self._max_stack = self._stack_depth | |
| def _pop(self, n: int = 1) -> None: | |
| self._stack_depth -= n | |
| if self._stack_depth < 0: | |
| raise AssemblerError("Stack underflow") | |
| def _const_index(self, value: Any) -> int: | |
| for i, c in enumerate(self._consts): | |
| if type(c) is type(value) and c == value: | |
| return i | |
| self._consts.append(value) | |
| return len(self._consts) - 1 | |
| def _varname_index(self, name: str) -> int: | |
| if name not in self._varnames: | |
| self._varnames.append(name) | |
| return self._varnames.index(name) | |
| def _name_index(self, name: str) -> int: | |
| if name not in self._names: | |
| self._names.append(name) | |
| return self._names.index(name) | |
| def _emit_raw(self, opcode: Opcode, arg: int = 0) -> Instruction: | |
| instr = Instruction(opcode=opcode, arg=arg, lineno=self._current_lineno) | |
| self._buf.append(instr) | |
| return instr | |
| # --- public emit interface ---------------------------------------------- | |
| def emit(self, opcode: Opcode, arg: int = 0) -> None: | |
| """Emit a raw opcode with integer argument.""" | |
| self._emit_raw(opcode, arg) | |
| def set_lineno(self, n: int) -> None: | |
| self._current_lineno = n | |
| def new_label(self) -> str: | |
| self._label_counter += 1 | |
| return f"_L{self._label_counter}" | |
| def emit_label(self, name: str) -> Label: | |
| """ | |
| Mark the current position as the target of label `name`. | |
| Returns the Label object (position set after assemble()). | |
| """ | |
| self._buf.mark_label_here(name) | |
| lbl = self._buf._labels.get(name, Label(name)) | |
| self._buf._labels[name] = lbl | |
| return lbl | |
| # --- high-level emitters ------------------------------------------------ | |
| def load_const(self, value: Any) -> None: | |
| idx = self._const_index(value) | |
| self._emit_raw(Opcode.LOAD_CONST, idx) | |
| self._push() | |
| def load_fast(self, name: str) -> None: | |
| idx = self._varname_index(name) | |
| self._emit_raw(Opcode.LOAD_FAST, idx) | |
| self._push() | |
| def store_fast(self, name: str) -> None: | |
| idx = self._varname_index(name) | |
| self._emit_raw(Opcode.STORE_FAST, idx) | |
| self._pop() | |
| def load_global(self, name: str, push_null: bool = False) -> None: | |
| idx = self._name_index(name) | |
| # In 3.12, LOAD_GLOBAL arg = (namei << 1) | push_null_flag | |
| arg = (idx << 1) | (1 if push_null else 0) | |
| self._emit_raw(Opcode.LOAD_GLOBAL, arg) | |
| self._push(2 if push_null else 1) | |
| def store_global(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.STORE_GLOBAL, idx) | |
| self._pop() | |
| def load_attr(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.LOAD_ATTR, idx) | |
| def store_attr(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.STORE_ATTR, idx) | |
| self._pop(2) | |
| def call(self, argc: int) -> None: | |
| self._emit_raw(Opcode.CALL, argc) | |
| self._pop(argc + 1) | |
| self._push() | |
| def push_null(self) -> None: | |
| self._emit_raw(Opcode.PUSH_NULL) | |
| self._push() | |
| def return_value(self) -> None: | |
| self._emit_raw(Opcode.RETURN_VALUE) | |
| self._pop() | |
| def return_const(self, value: Any) -> None: | |
| idx = self._const_index(value) | |
| self._emit_raw(Opcode.RETURN_CONST, idx) | |
| def pop_top(self) -> None: | |
| self._emit_raw(Opcode.POP_TOP) | |
| self._pop() | |
| def dup_top(self) -> None: | |
| self._emit_raw(Opcode.COPY, 1) | |
| self._push() | |
| def swap(self, i: int = 2) -> None: | |
| self._emit_raw(Opcode.SWAP, i) | |
| def binary_op(self, op: int) -> None: | |
| """op: 0=+, 1=&, 2=//, 3@, 4=lshift, 5=%, 6=*, 7=**, 8=rshift, 9=-, 10=/, 11=|, 12=^""" | |
| self._emit_raw(Opcode.BINARY_OP, op) | |
| self._pop() | |
| def compare_op(self, op: int) -> None: | |
| """op: 0=<, 1=<=, 2===, 3=!=, 4=>, 5>=""" | |
| self._emit_raw(Opcode.COMPARE_OP, op) | |
| self._pop() | |
| def jump_forward(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.JUMP_FORWARD, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| def jump_backward(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.JUMP_BACKWARD, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| def pop_jump_if_false(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.POP_JUMP_IF_FALSE, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| self._pop() | |
| def pop_jump_if_true(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.POP_JUMP_IF_TRUE, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| self._pop() | |
| def pop_jump_if_none(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.POP_JUMP_IF_NONE, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| self._pop() | |
| def build_list(self, count: int) -> None: | |
| self._emit_raw(Opcode.BUILD_LIST, count) | |
| self._pop(count) | |
| self._push() | |
| def build_tuple(self, count: int) -> None: | |
| self._emit_raw(Opcode.BUILD_TUPLE, count) | |
| self._pop(count) | |
| self._push() | |
| def build_dict(self, count: int) -> None: | |
| """count = number of key/value pairs (so 2*count items popped).""" | |
| self._emit_raw(Opcode.BUILD_MAP, count) | |
| self._pop(count * 2) | |
| self._push() | |
| def build_set(self, count: int) -> None: | |
| self._emit_raw(Opcode.BUILD_SET, count) | |
| self._pop(count) | |
| self._push() | |
| def build_string(self, count: int) -> None: | |
| self._emit_raw(Opcode.BUILD_STRING, count) | |
| self._pop(count) | |
| self._push() | |
| def unpack_sequence(self, count: int) -> None: | |
| self._emit_raw(Opcode.UNPACK_SEQUENCE, count) | |
| self._pop() | |
| self._push(count) | |
| def make_function(self, flags: int = 0) -> None: | |
| self._emit_raw(Opcode.MAKE_FUNCTION, flags) | |
| n = 1 + bin(flags).count('1') | |
| self._pop(n) | |
| self._push() | |
| def import_name(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.IMPORT_NAME, idx) | |
| self._pop(2) | |
| self._push() | |
| def import_from(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.IMPORT_FROM, idx) | |
| self._push() | |
| def get_iter(self) -> None: | |
| self._emit_raw(Opcode.GET_ITER) | |
| def for_iter(self, label: str) -> None: | |
| instr = self._emit_raw(Opcode.FOR_ITER, 0) | |
| instr.arg = label # type: ignore[assignment] | |
| self._push() | |
| def resume(self, where: int = 0) -> None: | |
| self._emit_raw(Opcode.RESUME, where) | |
| def nop(self) -> None: | |
| self._emit_raw(Opcode.NOP) | |
| def raise_varargs(self, argc: int) -> None: | |
| self._emit_raw(Opcode.RAISE_VARARGS, argc) | |
| self._pop(argc) | |
| def load_deref(self, idx: int) -> None: | |
| self._emit_raw(Opcode.LOAD_DEREF, idx) | |
| self._push() | |
| def store_deref(self, idx: int) -> None: | |
| self._emit_raw(Opcode.STORE_DEREF, idx) | |
| self._pop() | |
| def load_name(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.LOAD_NAME, idx) | |
| self._push() | |
| def store_name(self, name: str) -> None: | |
| idx = self._name_index(name) | |
| self._emit_raw(Opcode.STORE_NAME, idx) | |
| self._pop() | |
| # --- assembly ----------------------------------------------------------- | |
| def assemble(self) -> bytes: | |
| """Return raw bytecode bytes with labels resolved.""" | |
| return self._buf.to_bytes() | |
| def assemble_code_object( | |
| self, | |
| name: str | None = None, | |
| filename: str | None = None, | |
| args: list[str] | None = None, | |
| *, | |
| flags: int = 0, | |
| ) -> types.CodeType: | |
| """ | |
| Produce a types.CodeType from the assembled instructions. | |
| The returned code object can be exec()'d or called. | |
| """ | |
| name = name or self.name | |
| filename = filename or self.filename | |
| args = args or [] | |
| # Insert RESUME at start if not present | |
| if not self._buf._instructions or self._buf._instructions[0].opcode != Opcode.RESUME: | |
| resume_instr = Instruction(Opcode.RESUME, 0) | |
| self._buf._instructions.insert(0, resume_instr) | |
| bytecode = self._buf.to_bytes() | |
| co_varnames = tuple(self._varnames) | |
| co_names = tuple(self._names) | |
| co_consts = tuple(self._consts) | |
| argcount = len(args) | |
| # Build lnotab (simplified: all on line 1) | |
| lnotab = bytes([]) | |
| # Build exception table (empty) | |
| exceptiontable = bytes([]) | |
| # qualname | |
| qualname = name | |
| import sys | |
| if sys.version_info >= (3, 11): | |
| code = types.CodeType( | |
| argcount, # argcount | |
| 0, # posonlyargcount | |
| 0, # kwonlyargcount | |
| len(co_varnames), # nlocals | |
| self._max_stack + 1, # stacksize | |
| flags, # flags | |
| bytecode, # codestring | |
| co_consts, # constants | |
| co_names, # names | |
| co_varnames, # varnames | |
| filename, # filename | |
| name, # name | |
| qualname, # qualname | |
| 1, # firstlineno | |
| lnotab, # lnotab | |
| exceptiontable, # exceptiontable | |
| tuple(self._freevars), | |
| tuple(self._cellvars), | |
| ) | |
| else: | |
| code = types.CodeType( | |
| argcount, | |
| 0, | |
| 0, | |
| len(co_varnames), | |
| self._max_stack + 1, | |
| flags, | |
| bytecode, | |
| co_consts, | |
| co_names, | |
| co_varnames, | |
| filename, | |
| name, | |
| 1, | |
| lnotab, | |
| tuple(self._freevars), | |
| tuple(self._cellvars), | |
| ) | |
| return code | |
| # --- disassembly -------------------------------------------------------- | |
| def disassemble(self, bytecode: bytes) -> list[Instruction]: | |
| """ | |
| Decode raw bytecode bytes into a list of Instructions. | |
| Handles EXTENDED_ARG chaining. | |
| """ | |
| instructions = [] | |
| i = 0 | |
| extended_arg = 0 | |
| n = len(bytecode) | |
| while i < n: | |
| op = bytecode[i] | |
| arg_byte = bytecode[i + 1] if i + 1 < n else 0 | |
| arg = extended_arg | arg_byte | |
| if op == 144: # EXTENDED_ARG | |
| extended_arg = arg << 8 | |
| i += 2 | |
| continue | |
| extended_arg = 0 | |
| try: | |
| opcode = Opcode(op) | |
| except ValueError: | |
| opcode = Opcode.NOP | |
| instr = Instruction(opcode=opcode, arg=arg) | |
| instructions.append(instr) | |
| i += 2 | |
| return instructions | |
| def to_hex_dump(self, bytecode: bytes, width: int = 16) -> str: | |
| """Return a formatted hex dump of bytecode bytes.""" | |
| lines = [] | |
| for offset in range(0, len(bytecode), width): | |
| chunk = bytecode[offset:offset + width] | |
| hex_part = ' '.join(f'{b:02x}' for b in chunk) | |
| ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) | |
| lines.append(f'{offset:08x} {hex_part:<{width * 3}} |{ascii_part}|') | |
| return '\n'.join(lines) | |
| def annotated_disassembly(self, bytecode: bytes) -> str: | |
| """Return human-readable annotated disassembly.""" | |
| instructions = self.disassemble(bytecode) | |
| lines = [] | |
| offset = 0 | |
| for instr in instructions: | |
| name = instr.opcode.name | |
| arg_info = '' | |
| if instr.opcode == Opcode.LOAD_CONST and instr.arg < len(self._consts): | |
| arg_info = f' ({self._consts[instr.arg]!r})' | |
| elif instr.opcode in (Opcode.LOAD_FAST, Opcode.STORE_FAST) and instr.arg < len(self._varnames): | |
| arg_info = f' ({self._varnames[instr.arg]})' | |
| elif instr.opcode in (Opcode.LOAD_GLOBAL, Opcode.STORE_GLOBAL) and (instr.arg >> 1) < len(self._names): | |
| arg_info = f' ({self._names[instr.arg >> 1]})' | |
| lines.append(f'{offset:6d} {name:<30} {instr.arg:>5}{arg_info}') | |
| offset += instr.encoded_size() | |
| return '\n'.join(lines) | |
| def reset(self) -> None: | |
| """Reset assembler state for reuse.""" | |
| self._buf = InstructionBuffer() | |
| self._consts.clear() | |
| self._varnames.clear() | |
| self._names.clear() | |
| self._stack_depth = 0 | |
| self._max_stack = 0 | |
| self._label_counter = 0 | |
| # --- convenience builders ----------------------------------------------- | |
| def build_simple_function(self, body_fn) -> types.CodeType: | |
| """ | |
| Helper: call body_fn(asm) to emit instructions, then assemble. | |
| body_fn receives this assembler instance. | |
| """ | |
| body_fn(self) | |
| return self.assemble_code_object() | |
| # --------------------------------------------------------------------------- | |
| # ConstantFolder — simple constant folding pass | |
| # --------------------------------------------------------------------------- | |
| class ConstantFolder: | |
| """ | |
| Performs basic constant folding on an InstructionBuffer: | |
| LOAD_CONST + LOAD_CONST + BINARY_OP -> LOAD_CONST result | |
| """ | |
| BINARY_OPS = { | |
| 0: lambda a, b: a + b, | |
| 1: lambda a, b: a & b, | |
| 2: lambda a, b: a // b if b != 0 else None, | |
| 5: lambda a, b: a % b if b != 0 else None, | |
| 6: lambda a, b: a * b, | |
| 9: lambda a, b: a - b, | |
| 10: lambda a, b: a / b if b != 0 else None, | |
| 11: lambda a, b: a | b, | |
| 12: lambda a, b: a ^ b, | |
| } | |
| def fold(self, asm: BytecodeAssembler) -> int: | |
| """In-place fold; returns number of folds performed.""" | |
| folds = 0 | |
| instructions = asm._buf._instructions | |
| i = 0 | |
| while i < len(instructions) - 2: | |
| a = instructions[i] | |
| b = instructions[i + 1] | |
| c = instructions[i + 2] | |
| if (a.opcode == Opcode.LOAD_CONST | |
| and b.opcode == Opcode.LOAD_CONST | |
| and c.opcode == Opcode.BINARY_OP): | |
| op_fn = self.BINARY_OPS.get(c.arg) | |
| if op_fn is not None: | |
| try: | |
| va = asm._consts[a.arg] | |
| vb = asm._consts[b.arg] | |
| result = op_fn(va, vb) | |
| if result is not None: | |
| idx = asm._const_index(result) | |
| instructions[i] = Instruction(Opcode.LOAD_CONST, idx) | |
| del instructions[i + 1] | |
| del instructions[i + 1] | |
| folds += 1 | |
| continue | |
| except (TypeError, IndexError): | |
| pass | |
| i += 1 | |
| return folds | |
| # --------------------------------------------------------------------------- | |
| # PeepholeOptimizer — standard peephole passes | |
| # --------------------------------------------------------------------------- | |
| class PeepholeOptimizer: | |
| """ | |
| Performs peephole optimizations on assembled bytecode. | |
| Optimizations: | |
| 1. Remove consecutive POP_TOP after NOP | |
| 2. LOAD_CONST True/False + POP_JUMP -> direct JUMP or eliminate | |
| 3. NOP sequences -> single NOP or removal | |
| 4. JUMP_FORWARD with delta=0 -> remove | |
| """ | |
| def optimize(self, asm: BytecodeAssembler) -> int: | |
| """Apply all peephole passes; returns total number of changes.""" | |
| total = 0 | |
| total += self._remove_nops(asm) | |
| total += self._remove_zero_jumps(asm) | |
| total += self._fold_bool_jumps(asm) | |
| return total | |
| def _remove_nops(self, asm: BytecodeAssembler) -> int: | |
| before = len(asm._buf._instructions) | |
| asm._buf._instructions = [ | |
| i for i in asm._buf._instructions if i.opcode != Opcode.NOP | |
| ] | |
| return before - len(asm._buf._instructions) | |
| def _remove_zero_jumps(self, asm: BytecodeAssembler) -> int: | |
| removed = 0 | |
| instructions = asm._buf._instructions | |
| new_instrs = [] | |
| for instr in instructions: | |
| if instr.opcode == Opcode.JUMP_FORWARD and isinstance(instr.arg, int) and instr.arg == 0: | |
| removed += 1 | |
| else: | |
| new_instrs.append(instr) | |
| asm._buf._instructions = new_instrs | |
| return removed | |
| def _fold_bool_jumps(self, asm: BytecodeAssembler) -> int: | |
| """LOAD_CONST True + POP_JUMP_IF_FALSE -> JUMP_FORWARD (always taken).""" | |
| folds = 0 | |
| instructions = asm._buf._instructions | |
| i = 0 | |
| while i < len(instructions) - 1: | |
| a = instructions[i] | |
| b = instructions[i + 1] | |
| if a.opcode == Opcode.LOAD_CONST and b.opcode == Opcode.POP_JUMP_IF_FALSE: | |
| val = asm._consts[a.arg] if a.arg < len(asm._consts) else None | |
| if val is True: | |
| # Always false branch not taken — remove both | |
| del instructions[i] | |
| del instructions[i] | |
| folds += 1 | |
| continue | |
| elif val is False: | |
| # Always taken — replace with unconditional jump | |
| instructions[i] = Instruction(Opcode.JUMP_FORWARD, b.arg) | |
| del instructions[i + 1] | |
| folds += 1 | |
| continue | |
| i += 1 | |
| return folds | |
| # --------------------------------------------------------------------------- | |
| # BytecodeVerifier — structural checks | |
| # --------------------------------------------------------------------------- | |
| class BytecodeVerifier: | |
| """ | |
| Validates that assembled bytecode is structurally sound: | |
| - All jumps resolve to valid offsets | |
| - Stack depth never goes negative | |
| - Code ends with RETURN_VALUE or RETURN_CONST | |
| """ | |
| # Stack effect of each opcode: +n means n values pushed, -n means n popped | |
| STACK_EFFECTS: dict[int, int] = { | |
| int(Opcode.NOP): 0, | |
| int(Opcode.RESUME): 0, | |
| int(Opcode.POP_TOP): -1, | |
| int(Opcode.COPY): 1, | |
| int(Opcode.SWAP): 0, | |
| int(Opcode.LOAD_CONST): 1, | |
| int(Opcode.LOAD_FAST): 1, | |
| int(Opcode.STORE_FAST): -1, | |
| int(Opcode.LOAD_GLOBAL): 1, | |
| int(Opcode.STORE_GLOBAL): -1, | |
| int(Opcode.RETURN_VALUE): -1, | |
| int(Opcode.RETURN_CONST): 0, | |
| int(Opcode.BINARY_OP): -1, | |
| int(Opcode.COMPARE_OP): -1, | |
| int(Opcode.BUILD_LIST): 0, # variable — computed separately | |
| int(Opcode.BUILD_TUPLE): 0, | |
| int(Opcode.BUILD_MAP): 0, | |
| int(Opcode.PUSH_NULL): 1, | |
| int(Opcode.UNARY_NEGATIVE): 0, | |
| int(Opcode.UNARY_NOT): 0, | |
| int(Opcode.UNARY_INVERT): 0, | |
| int(Opcode.GET_ITER): 0, | |
| int(Opcode.JUMP_FORWARD): 0, | |
| int(Opcode.JUMP_BACKWARD): 0, | |
| int(Opcode.POP_JUMP_IF_TRUE): -1, | |
| int(Opcode.POP_JUMP_IF_FALSE): -1, | |
| int(Opcode.LOAD_ATTR): 0, | |
| int(Opcode.STORE_ATTR): -2, | |
| int(Opcode.CALL): 0, | |
| int(Opcode.MAKE_FUNCTION): 0, | |
| int(Opcode.IMPORT_NAME): -1, | |
| int(Opcode.IMPORT_FROM): 1, | |
| int(Opcode.RAISE_VARARGS): 0, | |
| } | |
| def verify(self, asm: BytecodeAssembler) -> list[str]: | |
| """Return list of error strings (empty if valid).""" | |
| errors = [] | |
| depth = 0 | |
| # Check for return at end | |
| instructions = asm._buf._instructions | |
| if instructions: | |
| last = instructions[-1] | |
| if last.opcode not in (Opcode.RETURN_VALUE, Opcode.RETURN_CONST, Opcode.RAISE_VARARGS): | |
| errors.append(f"Code does not end with RETURN: ends with {last.opcode.name}") | |
| for i, instr in enumerate(instructions): | |
| effect = self.STACK_EFFECTS.get(int(instr.opcode), 0) | |
| depth += effect | |
| if depth < 0: | |
| errors.append(f"Stack underflow at instruction {i} ({instr.opcode.name})") | |
| depth = 0 # recover | |
| return errors | |
| # --------------------------------------------------------------------------- | |
| # AssemblerContext — context manager for structured emission | |
| # --------------------------------------------------------------------------- | |
| class AssemblerContext: | |
| """ | |
| Context manager that automatically adds RESUME at entry and | |
| RETURN_CONST None at exit if no explicit return was emitted. | |
| """ | |
| def __init__(self, name: str = "<context>", filename: str = "<string>"): | |
| self.asm = BytecodeAssembler(name=name, filename=filename) | |
| def __enter__(self) -> BytecodeAssembler: | |
| self.asm.resume() | |
| return self.asm | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| if exc_type is None: | |
| # Check if last instruction is a return | |
| instrs = self.asm._buf._instructions | |
| if not instrs or instrs[-1].opcode not in ( | |
| Opcode.RETURN_VALUE, Opcode.RETURN_CONST | |
| ): | |
| self.asm.load_const(None) | |
| self.asm.return_value() | |
| return False | |
| def code_object(self) -> types.CodeType: | |
| return self.asm.assemble_code_object() | |
| # --------------------------------------------------------------------------- | |
| # Utility: compile Python source to raw bytecode via assembler inspection | |
| # --------------------------------------------------------------------------- | |
| def source_to_bytecode(source: str) -> tuple[bytes, types.CodeType]: | |
| """ | |
| Compile Python source, return (raw_bytecode, code_object). | |
| The assembler is populated to mirror the compiled code for inspection. | |
| """ | |
| import compile as builtin_compile | |
| code = compile(source, '<string>', 'exec') | |
| return code.co_code, code | |
| def bytecode_from_code_object(code: types.CodeType) -> bytes: | |
| """Extract raw bytecode from a code object.""" | |
| return code.co_code | |
| def describe_code_object(code: types.CodeType) -> str: | |
| """Return a human-readable description of a code object.""" | |
| lines = [ | |
| f"Code object: {code.co_name!r} in {code.co_filename!r}", | |
| f" argcount: {code.co_argcount}", | |
| f" nlocals: {code.co_nlocals}", | |
| f" stacksize: {code.co_stacksize}", | |
| f" flags: {code.co_flags:#010x}", | |
| f" consts: {code.co_consts}", | |
| f" names: {code.co_names}", | |
| f" varnames: {code.co_varnames}", | |
| f" bytecode: {len(code.co_code)} bytes", | |
| " disassembly:", | |
| ] | |
| buf = io.StringIO() | |
| dis.dis(code, file=buf) | |
| for line in buf.getvalue().splitlines(): | |
| lines.append(f" {line}") | |
| return '\n'.join(lines) | |
| # --------------------------------------------------------------------------- | |
| # FunctionBuilder — fluent API for building simple functions | |
| # --------------------------------------------------------------------------- | |
| class FunctionBuilder: | |
| """ | |
| Fluent builder for constructing simple compiled Python functions. | |
| Example: | |
| fb = FunctionBuilder("add") | |
| fb.arg("x").arg("y") | |
| fb.body(lambda asm: ( | |
| asm.load_fast("x"), | |
| asm.load_fast("y"), | |
| asm.binary_op(0), # ADD | |
| asm.return_value() | |
| )) | |
| add_fn = fb.build() | |
| assert add_fn(2, 3) == 5 | |
| """ | |
| def __init__(self, name: str): | |
| self._name = name | |
| self._args: list[str] = [] | |
| self._asm = BytecodeAssembler(name=name) | |
| self._body_emitted = False | |
| def arg(self, name: str) -> 'FunctionBuilder': | |
| self._args.append(name) | |
| self._asm._varname_index(name) # pre-register | |
| return self | |
| def body(self, emit_fn) -> 'FunctionBuilder': | |
| self._asm.resume() | |
| emit_fn(self._asm) | |
| self._body_emitted = True | |
| return self | |
| def build(self) -> types.FunctionType: | |
| if not self._body_emitted: | |
| self._asm.resume() | |
| self._asm.load_const(None) | |
| self._asm.return_value() | |
| code = self._asm.assemble_code_object( | |
| name=self._name, | |
| args=self._args, | |
| ) | |
| return types.FunctionType(code, {}) | |
| # --------------------------------------------------------------------------- | |
| # Module-level helpers | |
| # --------------------------------------------------------------------------- | |
| def make_assembler(name: str = "<asm>") -> BytecodeAssembler: | |
| return BytecodeAssembler(name=name) | |
| def assemble_expr(expr: str) -> bytes: | |
| """Compile a Python expression and return its bytecode.""" | |
| code = compile(expr, '<expr>', 'eval') | |
| return code.co_code | |
| def round_trip_test(source: str) -> bool: | |
| """ | |
| Compile source, disassemble, and verify instruction count is consistent. | |
| Returns True if disassembly produces sensible output. | |
| """ | |
| code = compile(source, '<test>', 'exec') | |
| asm = BytecodeAssembler() | |
| instructions = asm.disassemble(code.co_code) | |
| return len(instructions) > 0 | |
| # --------------------------------------------------------------------------- | |
| # Self-test | |
| # --------------------------------------------------------------------------- | |
| def _self_test() -> bool: | |
| """Run internal consistency checks. Returns True if all pass.""" | |
| asm = BytecodeAssembler("test") | |
| # Test constant pool dedup | |
| asm.load_const(42) | |
| asm.load_const(42) | |
| assert len(asm._consts) == 1, "Constant dedup failed" | |
| # Test varnames | |
| asm.load_fast("x") | |
| asm.store_fast("x") | |
| assert asm._varnames == ["x"] | |
| # Test bytecode roundtrip | |
| asm2 = BytecodeAssembler("rt") | |
| asm2.resume() | |
| asm2.load_const(1) | |
| asm2.load_const(2) | |
| asm2.binary_op(0) # ADD | |
| asm2.return_value() | |
| bytecode = asm2.assemble() | |
| instructions = asm2.disassemble(bytecode) | |
| assert len(instructions) > 0 | |
| return True | |
| # --------------------------------------------------------------------------- | |
| # OpcodeMeta — metadata about each opcode | |
| # --------------------------------------------------------------------------- | |
| class OpcodeMeta: | |
| """Metadata for a single CPython opcode.""" | |
| opcode: Opcode | |
| name: str | |
| stack_effect: int # net change to stack depth | |
| has_arg: bool | |
| is_jump: bool | |
| is_return: bool | |
| description: str | |
| def __repr__(self) -> str: | |
| return ( | |
| f"OpcodeMeta({self.name}, effect={self.stack_effect:+d}, " | |
| f"has_arg={self.has_arg}, jump={self.is_jump})" | |
| ) | |
| def build_opcode_meta_table() -> dict[Opcode, OpcodeMeta]: | |
| """Build complete metadata table for all defined opcodes.""" | |
| jump_ops = { | |
| Opcode.JUMP_FORWARD, Opcode.JUMP_BACKWARD, | |
| Opcode.JUMP_BACKWARD_NO_INTERRUPT, | |
| Opcode.POP_JUMP_IF_TRUE, Opcode.POP_JUMP_IF_FALSE, | |
| Opcode.POP_JUMP_IF_NONE, Opcode.POP_JUMP_IF_NOT_NONE, | |
| Opcode.JUMP_IF_TRUE_OR_POP, Opcode.JUMP_IF_FALSE_OR_POP, | |
| Opcode.FOR_ITER, | |
| } | |
| return_ops = {Opcode.RETURN_VALUE, Opcode.RETURN_CONST, Opcode.RAISE_VARARGS} | |
| table = {} | |
| for op in Opcode: | |
| try: | |
| name = op.name | |
| except AttributeError: | |
| continue | |
| effect = BytecodeVerifier.STACK_EFFECTS.get(int(op), 0) | |
| table[op] = OpcodeMeta( | |
| opcode=op, | |
| name=name, | |
| stack_effect=effect, | |
| has_arg=op.has_arg(), | |
| is_jump=op in jump_ops, | |
| is_return=op in return_ops, | |
| description=_OPCODE_DESCRIPTIONS.get(op, ""), | |
| ) | |
| return table | |
| _OPCODE_DESCRIPTIONS: dict[Opcode, str] = { | |
| Opcode.NOP: "No operation", | |
| Opcode.LOAD_CONST: "Push co_consts[arg] onto stack", | |
| Opcode.LOAD_FAST: "Push co_varnames[arg] onto stack", | |
| Opcode.STORE_FAST: "Pop top of stack; store in co_varnames[arg]", | |
| Opcode.LOAD_GLOBAL: "Load global name[arg>>1] onto stack", | |
| Opcode.STORE_GLOBAL: "Store top of stack as global name[arg]", | |
| Opcode.RETURN_VALUE: "Return top of stack", | |
| Opcode.RETURN_CONST: "Return co_consts[arg] without popping stack", | |
| Opcode.BINARY_OP: "Perform binary operation with given operator code", | |
| Opcode.COMPARE_OP: "Perform comparison with given operator code", | |
| Opcode.JUMP_FORWARD: "Jump forward by arg*2 bytes", | |
| Opcode.JUMP_BACKWARD: "Jump backward by arg*2 bytes", | |
| Opcode.POP_JUMP_IF_TRUE: "Pop and jump if truthy", | |
| Opcode.POP_JUMP_IF_FALSE: "Pop and jump if falsy", | |
| Opcode.GET_ITER: "Implement TOS = iter(TOS)", | |
| Opcode.FOR_ITER: "TOS is iterator; advance or jump forward by arg*2", | |
| Opcode.BUILD_LIST: "Build list from top arg items", | |
| Opcode.BUILD_TUPLE: "Build tuple from top arg items", | |
| Opcode.BUILD_MAP: "Build dict from top 2*arg items (key/value pairs)", | |
| Opcode.BUILD_SET: "Build set from top arg items", | |
| Opcode.MAKE_FUNCTION: "Create function object from code object and defaults", | |
| Opcode.IMPORT_NAME: "Import module names[arg]", | |
| Opcode.IMPORT_FROM: "Load attribute names[arg] from top of stack", | |
| Opcode.CALL: "Call callable with arg positional arguments", | |
| Opcode.PUSH_NULL: "Push NULL sentinel for CALL", | |
| Opcode.RESUME: "Start of function body", | |
| Opcode.UNPACK_SEQUENCE:"Unpack sequence into arg items on stack", | |
| Opcode.LOAD_ATTR: "Load attribute names[arg] of TOS", | |
| Opcode.STORE_ATTR: "Set TOS.names[arg] = TOS1", | |
| Opcode.POP_TOP: "Pop top of stack", | |
| Opcode.COPY: "Push copy of stack[arg] (1-based from top)", | |
| Opcode.SWAP: "Swap TOS with stack item arg (1-based)", | |
| Opcode.RAISE_VARARGS: "Raise exception with arg values", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # BasicBlock — control flow graph building block | |
| # --------------------------------------------------------------------------- | |
| class BasicBlock: | |
| """A straight-line sequence of instructions with no internal branches.""" | |
| label: str | |
| instructions: list[Instruction] = field(default_factory=list) | |
| successors: list[str] = field(default_factory=list) # labels of successor blocks | |
| predecessors: list[str] = field(default_factory=list) | |
| def append(self, instr: Instruction) -> None: | |
| self.instructions.append(instr) | |
| def is_empty(self) -> bool: | |
| return len(self.instructions) == 0 | |
| def last(self) -> Optional[Instruction]: | |
| return self.instructions[-1] if self.instructions else None | |
| def size(self) -> int: | |
| return len(self.instructions) | |
| def byte_size(self) -> int: | |
| return sum(i.encoded_size() for i in self.instructions) | |
| def __repr__(self) -> str: | |
| return f"BasicBlock({self.label!r}, {self.size()} instrs)" | |
| class ControlFlowGraph: | |
| """ | |
| Control flow graph built from a list of Instructions. | |
| Nodes are BasicBlocks; edges are branch/fallthrough relationships. | |
| """ | |
| def __init__(self, instructions: list[Instruction]): | |
| self._instructions = instructions | |
| self._blocks: dict[str, BasicBlock] = {} | |
| self._entry: Optional[str] = None | |
| self._build() | |
| def _build(self) -> None: | |
| """Partition instructions into basic blocks.""" | |
| if not self._instructions: | |
| return | |
| # Find block entry points: first instruction + jump targets + post-jump | |
| entries: set[int] = {0} | |
| for i, instr in enumerate(self._instructions): | |
| if instr.is_jump(): | |
| entries.add(i + 1) | |
| if isinstance(instr.arg, int): | |
| entries.add(instr.arg) | |
| sorted_entries = sorted(entries) | |
| block_starts = {off: f"B{off}" for off in sorted_entries} | |
| self._entry = block_starts.get(0, "B0") | |
| # Assign instructions to blocks | |
| current_label: Optional[str] = None | |
| for i, instr in enumerate(self._instructions): | |
| if i in block_starts: | |
| current_label = block_starts[i] | |
| if current_label not in self._blocks: | |
| self._blocks[current_label] = BasicBlock(current_label) | |
| if current_label: | |
| self._blocks[current_label].append(instr) | |
| def blocks(self) -> list[BasicBlock]: | |
| return list(self._blocks.values()) | |
| def entry_block(self) -> Optional[BasicBlock]: | |
| return self._blocks.get(self._entry) if self._entry else None | |
| def block_count(self) -> int: | |
| return len(self._blocks) | |
| def dominators(self) -> dict[str, set[str]]: | |
| """Compute dominator sets using iterative algorithm.""" | |
| if not self._blocks: | |
| return {} | |
| labels = list(self._blocks.keys()) | |
| entry = self._entry | |
| dom: dict[str, set[str]] = {} | |
| all_blocks = set(labels) | |
| # Initialize: entry dominates only itself; others dominated by all | |
| for lbl in labels: | |
| if lbl == entry: | |
| dom[lbl] = {lbl} | |
| else: | |
| dom[lbl] = set(all_blocks) | |
| # Iterative refinement | |
| changed = True | |
| while changed: | |
| changed = False | |
| for lbl in labels: | |
| if lbl == entry: | |
| continue | |
| block = self._blocks[lbl] | |
| preds = block.predecessors | |
| if preds: | |
| new_dom = set(all_blocks) | |
| for pred in preds: | |
| new_dom &= dom.get(pred, set()) | |
| new_dom.add(lbl) | |
| if new_dom != dom[lbl]: | |
| dom[lbl] = new_dom | |
| changed = True | |
| return dom | |
| def to_dot(self) -> str: | |
| """Export as Graphviz dot format.""" | |
| lines = ["digraph CFG {"] | |
| for label, block in self._blocks.items(): | |
| content = r'\n'.join( | |
| f"{instr.opcode.name} {instr.arg}" | |
| for instr in block.instructions[:4] | |
| ) | |
| if block.size() > 4: | |
| content += r'\n...' | |
| lines.append(f' {label} [label="{label}\\n{content}", shape=box];') | |
| for label, block in self._blocks.items(): | |
| for succ in block.successors: | |
| lines.append(f" {label} -> {succ};") | |
| lines.append("}") | |
| return '\n'.join(lines) | |
| # --------------------------------------------------------------------------- | |
| # StackTracer — trace stack state through bytecode | |
| # --------------------------------------------------------------------------- | |
| class StackTracer: | |
| """ | |
| Symbolically traces the stack through bytecode instructions, | |
| tracking the number of values at each point. | |
| """ | |
| def __init__(self, assembler: BytecodeAssembler): | |
| self._asm = assembler | |
| self._verifier = BytecodeVerifier() | |
| def trace(self) -> list[tuple[int, int]]: | |
| """ | |
| Trace stack depth through each instruction. | |
| Returns list of (instruction_index, stack_depth_before). | |
| """ | |
| result = [] | |
| depth = 0 | |
| for i, instr in enumerate(self._asm._buf._instructions): | |
| result.append((i, depth)) | |
| effect = self._verifier.STACK_EFFECTS.get(int(instr.opcode), 0) | |
| depth = max(0, depth + effect) | |
| return result | |
| def max_depth(self) -> int: | |
| trace = self.trace() | |
| return max((d for _, d in trace), default=0) | |
| def validate(self) -> bool: | |
| trace = self.trace() | |
| return all(d >= 0 for _, d in trace) | |
| # --------------------------------------------------------------------------- | |
| # BytecodePattern — pattern-based code transformation | |
| # --------------------------------------------------------------------------- | |
| class BytecodePattern: | |
| """Represents a sequence of opcodes to match in bytecode.""" | |
| opcodes: list[Opcode] | |
| name: str = "" | |
| def matches(self, instructions: list[Instruction], start: int) -> bool: | |
| """Check if the pattern matches starting at index `start`.""" | |
| if start + len(self.opcodes) > len(instructions): | |
| return False | |
| for i, op in enumerate(self.opcodes): | |
| if instructions[start + i].opcode != op: | |
| return False | |
| return True | |
| def find_all(self, instructions: list[Instruction]) -> list[int]: | |
| """Return all start indices where this pattern matches.""" | |
| matches = [] | |
| for i in range(len(instructions)): | |
| if self.matches(instructions, i): | |
| matches.append(i) | |
| return matches | |
| class PatternRewriter: | |
| """Applies pattern-based transformations to instruction sequences.""" | |
| def __init__(self): | |
| self._rules: list[tuple[BytecodePattern, list[Instruction]]] = [] | |
| def add_rule(self, pattern: BytecodePattern, replacement: list[Instruction]) -> None: | |
| self._rules.append((pattern, replacement)) | |
| def apply(self, instructions: list[Instruction]) -> tuple[list[Instruction], int]: | |
| """Apply all rules; return (new instructions, number of replacements).""" | |
| result = list(instructions) | |
| replacements = 0 | |
| for pattern, replacement in self._rules: | |
| i = 0 | |
| while i < len(result): | |
| if pattern.matches(result, i): | |
| # Replace matched slice | |
| result[i:i + len(pattern.opcodes)] = replacement | |
| replacements += 1 | |
| i += len(replacement) | |
| else: | |
| i += 1 | |
| return result, replacements | |
| # --------------------------------------------------------------------------- | |
| # BytecodeStats — statistics about a bytecode sequence | |
| # --------------------------------------------------------------------------- | |
| class BytecodeStats: | |
| total_instructions: int = 0 | |
| total_bytes: int = 0 | |
| opcode_histogram: dict = field(default_factory=dict) | |
| jump_count: int = 0 | |
| call_count: int = 0 | |
| const_count: int = 0 | |
| unique_consts: int = 0 | |
| unique_names: int = 0 | |
| unique_varnames: int = 0 | |
| def analyze(self, asm: BytecodeAssembler) -> None: | |
| instructions = asm._buf._instructions | |
| self.total_instructions = len(instructions) | |
| for instr in instructions: | |
| name = instr.opcode.name | |
| self.opcode_histogram[name] = self.opcode_histogram.get(name, 0) + 1 | |
| if instr.is_jump(): | |
| self.jump_count += 1 | |
| if instr.opcode == Opcode.CALL: | |
| self.call_count += 1 | |
| if instr.opcode == Opcode.LOAD_CONST: | |
| self.const_count += 1 | |
| self.unique_consts = len(asm._consts) | |
| self.unique_names = len(asm._names) | |
| self.unique_varnames = len(asm._varnames) | |
| self.total_bytes = asm._buf.size() | |
| def top_opcodes(self, n: int = 10) -> list[tuple[str, int]]: | |
| return sorted(self.opcode_histogram.items(), key=lambda x: -x[1])[:n] | |
| def summary(self) -> str: | |
| lines = [ | |
| f"BytecodeStats:", | |
| f" Instructions: {self.total_instructions}", | |
| f" Bytes: {self.total_bytes}", | |
| f" Jumps: {self.jump_count}", | |
| f" Calls: {self.call_count}", | |
| f" Constants: {self.const_count} ({self.unique_consts} unique)", | |
| f" Names: {self.unique_names}", | |
| f" Varnames: {self.unique_varnames}", | |
| f" Top opcodes: {self.top_opcodes(5)}", | |
| ] | |
| return '\n'.join(lines) | |
| # --------------------------------------------------------------------------- | |
| # CodeObjectDiff — compare two code objects | |
| # --------------------------------------------------------------------------- | |
| class CodeObjectDiff: | |
| """Compare two types.CodeType objects for differences.""" | |
| ATTRS = ( | |
| 'co_argcount', 'co_nlocals', 'co_stacksize', 'co_flags', | |
| 'co_consts', 'co_names', 'co_varnames', 'co_freevars', 'co_cellvars', | |
| ) | |
| def diff( | |
| self, | |
| a: types.CodeType, | |
| b: types.CodeType, | |
| label_a: str = "A", | |
| label_b: str = "B", | |
| ) -> list[str]: | |
| differences = [] | |
| for attr in self.ATTRS: | |
| va = getattr(a, attr, None) | |
| vb = getattr(b, attr, None) | |
| if va != vb: | |
| differences.append(f"{attr}: {label_a}={va!r}, {label_b}={vb!r}") | |
| # Bytecode | |
| ca, cb = a.co_code, b.co_code | |
| if ca != cb: | |
| differences.append( | |
| f"co_code: {label_a}={len(ca)} bytes, {label_b}={len(cb)} bytes" | |
| ) | |
| if len(ca) == len(cb): | |
| diffs = [i for i in range(0, len(ca), 2) if ca[i] != cb[i]] | |
| differences.append(f" Differing offsets: {diffs[:10]}") | |
| return differences | |
| def is_equivalent(self, a: types.CodeType, b: types.CodeType) -> bool: | |
| return len(self.diff(a, b)) == 0 | |
| # --------------------------------------------------------------------------- | |
| # EmitContext — scoped emission with automatic cleanup | |
| # --------------------------------------------------------------------------- | |
| class EmitContext: | |
| """ | |
| Context manager for structured emission of related instruction groups. | |
| Tracks the byte range of instructions emitted within the context. | |
| """ | |
| def __init__(self, asm: BytecodeAssembler, name: str = ""): | |
| self._asm = asm | |
| self._name = name | |
| self._start_count = 0 | |
| self._end_count = 0 | |
| def __enter__(self) -> 'EmitContext': | |
| self._start_count = len(self._asm._buf._instructions) | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb) -> bool: | |
| self._end_count = len(self._asm._buf._instructions) | |
| return False | |
| def instruction_count(self) -> int: | |
| return self._end_count - self._start_count | |
| def instructions(self) -> list[Instruction]: | |
| return self._asm._buf._instructions[self._start_count:self._end_count] | |
| def byte_size(self) -> int: | |
| return sum(i.encoded_size() for i in self.instructions) | |
| def __repr__(self) -> str: | |
| return ( | |
| f"EmitContext({self._name!r}, " | |
| f"{self.instruction_count} instrs, {self.byte_size} bytes)" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # LoopBuilder — structured loop emission | |
| # --------------------------------------------------------------------------- | |
| class LoopBuilder: | |
| """ | |
| Helper for emitting structured for/while loops using labels. | |
| Example: | |
| lb = LoopBuilder(asm) | |
| with lb.for_range(10): # pushes count | |
| asm.load_fast("body_value") | |
| asm.pop_top() | |
| """ | |
| def __init__(self, asm: BytecodeAssembler): | |
| self._asm = asm | |
| class _ForRangeContext: | |
| def __init__(self, lb: 'LoopBuilder', count: int): | |
| self._lb = lb | |
| self._count = count | |
| self._loop_label = "" | |
| self._exit_label = "" | |
| def __enter__(self): | |
| asm = self._lb._asm | |
| self._loop_label = asm.new_label() | |
| self._exit_label = asm.new_label() | |
| # Build range(count) and get iterator | |
| asm.load_global("range", push_null=True) | |
| asm.load_const(self._count) | |
| asm.call(1) | |
| asm.get_iter() | |
| asm.emit_label(self._loop_label) | |
| asm.for_iter(self._exit_label) | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| asm = self._lb._asm | |
| asm.jump_backward(self._loop_label) | |
| asm.emit_label(self._exit_label) | |
| return False | |
| def for_range(self, count: int) -> '_ForRangeContext': | |
| return self._ForRangeContext(self, count) | |
| # --------------------------------------------------------------------------- | |
| # Global opcode metadata table (built lazily) | |
| # --------------------------------------------------------------------------- | |
| _OPCODE_META: Optional[dict] = None | |
| def get_opcode_meta() -> dict[Opcode, OpcodeMeta]: | |
| global _OPCODE_META | |
| if _OPCODE_META is None: | |
| _OPCODE_META = build_opcode_meta_table() | |
| return _OPCODE_META | |
| def opcode_info(op: Opcode) -> Optional[OpcodeMeta]: | |
| return get_opcode_meta().get(op) | |
| if __name__ == "__main__": | |
| assert _self_test(), "Self-test failed" | |
| print("bytecode_assembler.py: all self-tests passed") | |
| # Demo: build and execute a simple add function | |
| fb = FunctionBuilder("add") | |
| fb.arg("x").arg("y") | |
| fb.body(lambda asm: [ | |
| asm.load_fast("x"), | |
| asm.load_fast("y"), | |
| asm.binary_op(0), | |
| asm.return_value(), | |
| ]) | |
| # Note: executing assembled bytecode requires careful version matching | |
| print("FunctionBuilder demo: built 'add' code object") | |
| # Demo: hex dump | |
| asm3 = BytecodeAssembler("demo") | |
| asm3.resume() | |
| asm3.load_const(42) | |
| asm3.return_value() | |
| bytecode = asm3.assemble() | |
| print("Hex dump of 'return 42' bytecode:") | |
| print(asm3.to_hex_dump(bytecode)) | |
| # Demo: opcode metadata | |
| meta = get_opcode_meta() | |
| print(f"\nOpcode metadata table: {len(meta)} entries") | |
| for op in list(meta.keys())[:5]: | |
| print(f" {meta[op]}") | |
| # Demo: BytecodeStats | |
| stats = BytecodeStats() | |
| stats.analyze(asm3) | |
| print("\n" + stats.summary()) | |