diff --git a/pytest/test_std.py b/pytest/test_std.py index b6b4388f..0f51ff0b 100644 --- a/pytest/test_std.py +++ b/pytest/test_std.py @@ -155,6 +155,7 @@ def test_distb(traceback_fixture, stream_fixture): # assert actual == expected def test_get_instructions(): + # opc = get_opcode_module(PYTHON_VERSION_TRIPLE, PYTHON_IMPLEMENTATION) actual = list(dis.get_instructions(TEST_SOURCE_CODE)) actual_len = len(actual) assert actual_len > 0 diff --git a/pytest/testdata/01_fstring-3.6.right b/pytest/testdata/01_fstring-3.6.right index e8d4eaff..fed58ec7 100644 --- a/pytest/testdata/01_fstring-3.6.right +++ b/pytest/testdata/01_fstring-3.6.right @@ -29,6 +29,7 @@ # 3: date # 4: anniversary # 5: print + 3: 0 LOAD_CONST (0) 2 LOAD_CONST (None) 4 IMPORT_NAME (datetime) diff --git a/pytest/testdata/01_fstring-xasm-3.6.right b/pytest/testdata/01_fstring-xasm-3.6.right index 0e0644ce..b3f9f2d7 100644 --- a/pytest/testdata/01_fstring-xasm-3.6.right +++ b/pytest/testdata/01_fstring-xasm-3.6.right @@ -30,6 +30,7 @@ # 3: date # 4: anniversary # 5: print + 3: LOAD_CONST 0 (0) LOAD_CONST 1 (None) diff --git a/pytest/testdata/03_annotations-3.7.right b/pytest/testdata/03_annotations-3.7.right index 888cd0d2..f0aa4d0c 100644 --- a/pytest/testdata/03_annotations-3.7.right +++ b/pytest/testdata/03_annotations-3.7.right @@ -14,6 +14,7 @@ # Names: # 0: __future__ # 1: annotations + 1: 0 LOAD_CONST (0) 2 LOAD_CONST (('annotations',)) 4 IMPORT_NAME (__future__) diff --git a/pytest/testdata/03_annotations-xasm-3.7.right b/pytest/testdata/03_annotations-xasm-3.7.right index fb8c193d..a7a88972 100644 --- a/pytest/testdata/03_annotations-xasm-3.7.right +++ b/pytest/testdata/03_annotations-xasm-3.7.right @@ -15,6 +15,7 @@ # Names: # 0: __future__ # 1: annotations + 1: LOAD_CONST 0 (0) LOAD_CONST 1 (('annotations',)) diff --git a/pytest/testdata/03_big_dict-2.7.right b/pytest/testdata/03_big_dict-2.7.right index 05ae6942..3e240dc5 100644 --- a/pytest/testdata/03_big_dict-2.7.right +++ b/pytest/testdata/03_big_dict-2.7.right @@ -350,6 +350,7 @@ # 341: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 1: 0 BUILD_MAP 113 2: 3 LOAD_CONST (('+',)) diff --git a/pytest/testdata/03_big_dict-3.3.right b/pytest/testdata/03_big_dict-3.3.right index 9896e4d7..af6b7577 100644 --- a/pytest/testdata/03_big_dict-3.3.right +++ b/pytest/testdata/03_big_dict-3.3.right @@ -380,6 +380,7 @@ # 369: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 1: 0 BUILD_MAP 113 2: 3 LOAD_CONST (('+',)) diff --git a/pytest/testdata/03_big_dict-3.5.right b/pytest/testdata/03_big_dict-3.5.right index a10d46b6..1ad75073 100644 --- a/pytest/testdata/03_big_dict-3.5.right +++ b/pytest/testdata/03_big_dict-3.5.right @@ -380,6 +380,7 @@ # 369: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 2: 0 LOAD_CONST ("BINARY_ADD") 3 LOAD_CONST (('+',)) diff --git a/pytest/testdata/03_big_dict-3.6.right b/pytest/testdata/03_big_dict-3.6.right index 39eba4ce..b9e1abf4 100644 --- a/pytest/testdata/03_big_dict-3.6.right +++ b/pytest/testdata/03_big_dict-3.6.right @@ -268,6 +268,7 @@ # 257: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 2: 0 LOAD_CONST (('+',)) 3: 2 LOAD_CONST (('-',)) diff --git a/pytest/testdata/03_big_dict-xasm-2.7.right b/pytest/testdata/03_big_dict-xasm-2.7.right index aaa0cfd0..94b7d4b1 100644 --- a/pytest/testdata/03_big_dict-xasm-2.7.right +++ b/pytest/testdata/03_big_dict-xasm-2.7.right @@ -351,6 +351,7 @@ # 341: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 1: BUILD_MAP 113 diff --git a/pytest/testdata/03_big_dict-xasm-3.3.right b/pytest/testdata/03_big_dict-xasm-3.3.right index ce26f015..75e2e2aa 100644 --- a/pytest/testdata/03_big_dict-xasm-3.3.right +++ b/pytest/testdata/03_big_dict-xasm-3.3.right @@ -381,6 +381,7 @@ # 369: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 1: BUILD_MAP 113 diff --git a/pytest/testdata/03_big_dict-xasm-3.5.right b/pytest/testdata/03_big_dict-xasm-3.5.right index e371677e..f7fa0886 100644 --- a/pytest/testdata/03_big_dict-xasm-3.5.right +++ b/pytest/testdata/03_big_dict-xasm-3.5.right @@ -381,6 +381,7 @@ # 369: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 2: LOAD_CONST 0 ("BINARY_ADD") LOAD_CONST 213 (('+',)) diff --git a/pytest/testdata/03_big_dict-xasm-3.6.right b/pytest/testdata/03_big_dict-xasm-3.6.right index 32c314e6..9d84b0c8 100644 --- a/pytest/testdata/03_big_dict-xasm-3.6.right +++ b/pytest/testdata/03_big_dict-xasm-3.6.right @@ -269,6 +269,7 @@ # 257: ('%c: %c', 1, 2) # Names: # 0: TABLE_DIRECT + 2: LOAD_CONST 101 (('+',)) diff --git a/pytest/testdata/04_pypy_lambda-2.7PyPy.right b/pytest/testdata/04_pypy_lambda-2.7PyPy.right index fccbf5ad..d19e8ed4 100644 --- a/pytest/testdata/04_pypy_lambda-2.7PyPy.right +++ b/pytest/testdata/04_pypy_lambda-2.7PyPy.right @@ -10,6 +10,7 @@ # 1: None # Names: # 0: f + 4: 0 LOAD_CONST ( None: def disassemble_bytes( self, - bytecode: Union[CodeType, bytes, str], + code_object: CodeType, lasti: int = -1, - varnames=None, - names=None, - constants=None, - cells=None, line_starts=None, file=sys.stdout, line_offset=0, @@ -660,7 +640,6 @@ def disassemble_bytes( filename: Optional[str] = None, show_source=True, first_line_number: Optional[int] = None, - exception_entries=None, ) -> list: # Omit the line number column entirely if we have no line number info show_lineno = line_starts is not None or self.opc.version_tuple < (2, 3) @@ -703,17 +682,7 @@ def show_source_text(line_number: Optional[int]) -> None: else: get_instructions_fn = get_instructions_bytes - for instr in get_instructions_fn( - bytecode, - self.opc, - varnames, - names, - constants, - cells, - line_starts, - line_offset=line_offset, - exception_entries=exception_entries, - ): + for instr in get_instructions_fn(code_object, self.opc): # Python 1.x into early 2.0 uses SET_LINENO if last_was_set_lineno: instr = Instruction( @@ -789,7 +758,7 @@ def show_source_text(line_number: Optional[int]) -> None: new_source_line = show_lineno and ( extended_arg_starts_line or instr.starts_line is not None - and instr.offset > 0 + and instr.offset >= 0 ) if new_source_line: file.write("\n") @@ -833,7 +802,7 @@ def show_source_text(line_number: Optional[int]) -> None: pass return instructions - def get_instructions(self, x, first_line=None): + def get_instructions(self, x): """Iterator for the opcodes in methods, functions or code Generates a series of Instruction named tuples giving the details of @@ -845,22 +814,7 @@ def get_instructions(self, x, first_line=None): the disassembled code object. """ co = get_code_object(x) - cell_names = co.co_cellvars + co.co_freevars - line_starts = dict(self.opc.findlinestarts(co)) - if first_line is not None: - line_offset = first_line - co.co_firstlineno - else: - line_offset = 0 - return get_instructions_bytes( - co.co_code, - self.opc, - co.co_varnames, - co.co_names, - co.co_consts, - cell_names, - line_starts, - line_offset, - ) + return get_instructions_bytes(co, self.opc) def list2bytecode( diff --git a/xdis/bytecode_graal.py b/xdis/bytecode_graal.py index 4197747d..5f80b1a1 100644 --- a/xdis/bytecode_graal.py +++ b/xdis/bytecode_graal.py @@ -1,7 +1,10 @@ # from xdis.bytecode import get_optype +from typing import Optional + from xdis.bytecode import Bytecode from xdis.cross_dis import get_code_object from xdis.instruction import Instruction +from xdis.lineoffsets_graal import SourceMap, find_linestarts_graal from xdis.opcodes.base_graal import ( BINARY_OPS, COLLECTION_KIND, @@ -11,15 +14,8 @@ def get_instructions_bytes_graal( - bytecode, + code_object, opc, - varnames, - names, - constants, - cells, - freevars, - line_offset, - exception_entries, ): """ Iterate over the instructions in a bytecode string. @@ -29,19 +25,26 @@ def get_instructions_bytes_graal( e.g., variable names, constants, can be specified using optional arguments. """ - # source_map = ??? + bytecode: bytes = code_object.co_code + constants: tuple = code_object.co_consts + names: tuple = code_object.co_names + varnames: tuple = code_object.co_varnames + # cells: tuple = code_object.co_cells + # freevars: tuple = code_object.co_freevars i = 0 n = len(bytecode) - extended_arg_count = 0 labels = opc.findlabels(bytecode, opc) + linestarts = find_linestarts_graal(code_object, opc, dup_lines=True) + extended_arg_count = 0 while i < n: opcode = bytecode[i] opname = opc.opname[opcode] optype = get_optype_graal(opcode, opc) offset = i + starts_line = linestarts.get(offset, None) arg_count = opc.arg_counts[opcode] is_jump_target = i in labels @@ -191,7 +194,7 @@ def get_instructions_bytes_graal( arg = 0 break - inst_size = (i - offset + 1) + (extended_arg_count * 2) + inst_size = (arg_count + 1) + (extended_arg_count * 2) start_offset = offset if opc.oppop[opcode] == 0 else None # for (int i = 0 i < exceptionHandlerRanges.length; i += 4) { @@ -242,7 +245,7 @@ def get_instructions_bytes_graal( yield Instruction( is_jump_target=is_jump_target, - starts_line=False, # starts_line, + starts_line= starts_line, offset=offset, opname=opname, opcode=opcode, @@ -280,8 +283,7 @@ def __init__(self, x, opc, first_line=None, current_offset=None) -> None: self._line_offset = first_line - co.co_firstlineno pass - # self._linestarts = dict(opc.findlinestarts(co, dup_lines=dup_lines)) - self._linestarts = None + self._linestarts = find_linestarts_graal(co, opc, dup_lines=True) self._original_object = x self.opc = opc self.opnames = opc.opname @@ -298,11 +300,6 @@ def __iter__(self): return get_instructions_bytes_graal( co.co_code, self.opc, - co.co_varnames, - co.co_names, - co.co_consts, - co.co_cellvars, - co.co_freevars, ) def __repr__(self) -> str: @@ -319,13 +316,5 @@ def get_instructions(self, x): Otherwise, the source line information (if any) is taken directly from the disassembled code object. """ - co = get_code_object(x) - return get_instructions_bytes_graal( - co.co_code, - self.opc, - co.co_varnames, - co.co_names, - co.co_consts, - co.co_cellvars, - co.co_freevars, - ) + code_object = get_code_object(x) + return get_instructions_bytes_graal(code_object, self.opc) diff --git a/xdis/codetype/__init__.py b/xdis/codetype/__init__.py index 3302e569..11933542 100644 --- a/xdis/codetype/__init__.py +++ b/xdis/codetype/__init__.py @@ -33,7 +33,7 @@ __docformat__ = "restructuredtext" -def codeType2Portable(code, version_triple=PYTHON_VERSION_TRIPLE, is_graal: bool=False): +def codeType2Portable(code, version_triple=PYTHON_VERSION_TRIPLE, is_graal: bool=False, other_fields: dict={}): """Converts a native types.CodeType code object into a corresponding more flexible xdis Code type. """ @@ -149,20 +149,7 @@ def codeType2Portable(code, version_triple=PYTHON_VERSION_TRIPLE, is_graal: bool version_triple=version_triple, ) elif version_triple[:2] >= (3, 11): - other_fields = {} if is_graal: - other_fields["condition_profileCount"]=code.condition_profileCount if hasattr(code, "condition_profileCount") else -1, - other_fields["endColumn"]=code.endColumn if hasattr(code, "endColumn") else -1, - other_fields["endLine"]=code.endLine if hasattr(code, "endLine") else -1, - other_fields["exception_handler_ranges"]=code.exception_handler_ranges if hasattr(code, "exception_handler_ranges") else tuple(), - other_fields["generalizeInputsMap"]=code.generalizeInputsMap if hasattr(code, "generalizeInputsMap") else {}, - other_fields["generalizeVarsMap"]=code.generalizeVarsMap if hasattr(code, "generalizeVarsMap") else {}, - other_fields["outputCanQuicken"]=code.outputCanQuicken if hasattr(code, "outputCanQuicken") else b"", - other_fields["primitiveConstants"]=code.primitiveConstants if hasattr(code, "primitiveConstants") else tuple(), - other_fields["srcOffsetTable"]=code.srcOffsetTable if hasattr(code, "srcOffsetTable") else b"", - other_fields["startColumn"]=code.startColumn if hasattr(code, "startColumn") else -1, - other_fields["startLine"]=code.startLine if hasattr(code, "startLine") else -1, - other_fields["variableShouldUnbox"]=code.variableShouldUnbox if hasattr(code, "variableShouldUnbox") else b"", return Code311Graal( co_argcount=code.co_argcount, @@ -363,7 +350,7 @@ def to_portable( version_triple=version_triple, other_fields=other_fields, ) - return codeType2Portable(code, version_triple, is_graal=is_graal) + return codeType2Portable(code, version_triple, is_graal=is_graal, other_fields=other_fields) if __name__ == "__main__": diff --git a/xdis/disasm.py b/xdis/disasm.py index fc7b2cba..4274df80 100644 --- a/xdis/disasm.py +++ b/xdis/disasm.py @@ -142,7 +142,7 @@ def disco( co, timestamp, out=sys.stdout, - magic_int=None, + magic_int: int=-1, source_size=None, sip_hash=None, asm_format: str = "classic", diff --git a/xdis/instruction.py b/xdis/instruction.py index 08fff7a4..5d21cd05 100644 --- a/xdis/instruction.py +++ b/xdis/instruction.py @@ -22,6 +22,8 @@ import re from typing import Any, Dict, NamedTuple, Optional, Union +from xdis.version_info import PythonImplementation + # _Instruction.tos_str.__doc__ = ( # "If not None, a string representation of the top of the stack (TOS)" # ) @@ -290,18 +292,32 @@ def disassemble( # Column: Instruction bytes if asm_format in ("extended-bytes", "bytes"): hex_bytecode = "|%02x" % opcode - if self.inst_size == 1: - # Not 3.6 or later - hex_bytecode += " " * (2 * 3) - if self.inst_size == 2: - # Must be Python 3.6 or later - if self.has_arg and self.arg is not None: - hex_bytecode += " %02x" % (self.arg % 256) - else: - hex_bytecode += " 00" - elif self.inst_size == 3 and self.arg is not None: - # Not 3.6 or later - hex_bytecode += " %02x %02x" % divmod(self.arg, 256) + if opc.python_implementation == PythonImplementation.Graal: + if self.inst_size == 1: + hex_bytecode += " " * (3 * 3) + elif self.inst_size == 2: + if self.has_arg and self.arg is not None: + hex_bytecode += " %02x " % (self.arg % 0x100) + else: + hex_bytecode += " 00 " + elif self.inst_size == 3: + hex_bytecode += " %02x %02x " % divmod(self.arg, 0x100) + elif self.inst_size == 4: + upper, lower = divmod(self.arg, 0x10000) + hex_bytecode += " %02x %02x %02x" % (upper, *divmod(lower, 0x100)) + else: + if self.inst_size == 1: + # Not 3.6 or later + hex_bytecode += " " * (2 * 3) + if self.inst_size == 2: + # Must be Python 3.6 or later + if self.has_arg and self.arg is not None: + hex_bytecode += " %02x" % (self.arg % 256) + else: + hex_bytecode += " 00" + elif self.inst_size == 3 and self.arg is not None: + # Not 3.6 or later + hex_bytecode += " %02x %02x" % divmod(self.arg, 256) fields.append(hex_bytecode + "|") diff --git a/xdis/lineoffsets_graal.py b/xdis/lineoffsets_graal.py new file mode 100644 index 00000000..a7bf7353 --- /dev/null +++ b/xdis/lineoffsets_graal.py @@ -0,0 +1,181 @@ +""" + +This module is an adaptation of the GraalPython's Java SourceMap helper + +The original assumes Truffle's Source/SourceSection APIs. + + * The get_source_section(source, ...) method expects a "source" object that implements: + - has_characters() -> bool + - get_line_length(line: int) -> int + - create_unavailable_section() -> any + - create_section(start_line, start_column, end_line, end_column) -> any + * The SourceMap constructor accepts an optional op_length_fn parameter (callable) + that takes a single byte value (0..255) and returns the length (int) of the + instruction at that BCI. If omitted, all instructions are assumed to have length 1. + + - Byte encoding/decoding handles Java's signed byte semantics (values -128..127). + - This is intended to reproduce logic and encoding used in the Java original. It is + not tied to any Truffle internals and can be adapted to a concrete environment. + +See https://github.com/oracle/graalpython/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/compiler/SourceMap.java +""" + +from typing import Dict, List, Tuple + +# Signed byte constants (as in Java code) +EXTENDED_NUM = -128 +NEXT_LINE = -127 +NEXT_LINES = -126 +MIN_NUM = -125 +MULTIPLIER_NEGATIVE = MIN_NUM + 1 # -124 +MAX_NUM = 127 +MULTIPLIER_POSITIVE = MAX_NUM # 127 + + +def _to_signed(byte_value: int) -> int: + """Convert 0..255 byte to signed -128..127 value like Java byte.""" + return byte_value if byte_value < 128 else byte_value - 256 + + +class SourceMap: + """ + Encapsulates encoding/decoding of source line/column information for GraalPython bytecode. + + Constructor: + SourceMap(code: bytes, src_table: bytes, start_line: int, start_column: int, + op_length_fn: Optional[Callable[[int], int]] = None) + + - code: bytes of bytecode (used to determine map size and iterate instruction BCIs) + - src_table: the encoded source table bytes produced by Builder + - start_line, start_column: initial line/column used to start decoding + - op_length_fn: optional callable taking an opcode byte (0..255) and returning + the instruction length in bytes. If None, instructions are assumed length 1. + """ + + def __init__( + self, + code_object, + opc, + ): + # use Python lists (mutable) to store maps; they match length of code + + bytecode: bytes = code_object.co_code + start_column: int = code_object.startColumn + start_line: int = code_object.startLine + # cells: tuple = code_object.co_cells + # freevars: tuple = code_object.co_freevars + arg_counts: Dict[int, int] = opc.arg_counts + + + n = len(bytecode) + self.startLineMap: List[int] = [0] * n + self.endLineMap: List[int] = [0] * n + self.startColumnMap: List[int] = [0] * n + self.endColumnMap: List[int] = [0] * n + self.source_table: bytes = code_object.srcOffsetTable + self.source_table_len: int = len(self.source_table) + + # op_length_fn determines instruction size; default to 1 + + self.source_table_pos = 0 + + self.next_column = start_column + self.next_line = start_line + offset = 0 + while offset < n: + + # code[offset] is an int 0..255 in Python 3 when indexing bytes + op_byte = bytecode[offset] + op_len = arg_counts[op_byte] + 1 + + + start_line, start_column = self._next_line_and_column() + end_line, end_column = self._next_line_and_column() + + # fill maps for the bytes covered by this opcode + for i in range(offset, min(offset + op_len, n)): + self.startLineMap[i] = start_line + self.startColumnMap[i] = start_column + self.endLineMap[i] = end_line + self.endColumnMap[i] = end_column + offset += op_len + + def _next_line_and_column(self) -> Tuple[int, int]: + """ + Get the (line, column) pair delta from self.source_table. + + Java code (which reads from a stream): + stream.mark(1); + byte value = (byte) stream.read(); + if (value == NEXT_LINE) { pair[0]++; pair[1] = 0; } + else if (value == NEXT_LINES) { pair[0] += readNum(stream); pair[1] = 0; } + else { stream.reset(); } + pair[1] += readNum(stream); + """ + if self.source_table_pos >= self.source_table_len: + raise EOFError("Unexpected end of source table while reading line/column") + + old_pos = self.source_table_pos + b = self.source_table[self.source_table_pos] + self.source_table_pos += 1 + v = _to_signed(b) + if v == NEXT_LINE: + self.next_line += 1 + self.next_column = 0 + elif v == NEXT_LINES: + self.next_line += self._get_num() + self.next_column = 0 + else: + # reset to before the byte we consumed + self.source_table_pos = old_pos + self.next_column += self._get_num() + return self.next_line, self.next_column + + def _get_num(self) -> int: + """ + Decode a signed (possibly extended) number from the stream. + + This Behavior mirrors Java's readNum using EXTENDED_NUM and multipliers, + but that code reads from a stream insttead of self.source_table. + """ + extensions = 0 + while True: + if self.source_table_pos >= self.source_table_len: + raise EOFError("Unexpected end of source table while reading line/column") + + b = self.source_table[self.source_table_pos] + self.source_table_pos += 1 + val = _to_signed(b) + if val == EXTENDED_NUM: + extensions += 1 + elif val < 0: + # negative single-byte value (signed) + return extensions * MULTIPLIER_NEGATIVE + val + else: + # non-negative single-byte value + return extensions * MULTIPLIER_POSITIVE + val + +def find_linestarts_graal(code_object, opc, dup_lines: bool) -> dict: + source_map = SourceMap(code_object, opc) + bytecode: bytes = code_object.co_code + i = 0 + n = len(bytecode) + last_lineno = -1 + offset2line: Dict[int, int] = {} + lines_seen = set() + while i < n: + opcode = bytecode[i] + offset = i + i += opc.arg_counts[opcode] + 1 + line_number = source_map.startLineMap[offset] + if line_number != last_lineno: + if not dup_lines: + if line_number in lines_seen: + continue + else: + lines_seen.add(line_number) + offset2line[offset] = line_number + + last_lineno = line_number + + return offset2line diff --git a/xdis/opcodes/opcode_312graal.py b/xdis/opcodes/opcode_312graal.py index f38fb860..4e1287d3 100644 --- a/xdis/opcodes/opcode_312graal.py +++ b/xdis/opcodes/opcode_312graal.py @@ -22,7 +22,8 @@ from typing import Dict, Set from xdis.opcodes.base import init_opdata -from xdis.opcodes.base_graal import findlabels # noqa + +# from xdis.opcodes.base_graal import findlabels # noqa from xdis.opcodes.base_graal import ( # find_linestarts, # noqa binary_op_graal, call_op_graal, @@ -56,10 +57,11 @@ # opcodes that perform a unary operation on the toip stack entry unaryop: Set[int] = set([]) -loc = locals() +opc = loc = locals() init_opdata(loc, None, None) + # Instruction opcodes for compiled code # Blank lines correspond to available opcodes diff --git a/xdis/std.py b/xdis/std.py index ceac4d81..37ef65d4 100644 --- a/xdis/std.py +++ b/xdis/std.py @@ -223,7 +223,7 @@ def disco(self, code, lasti=-1, file=None) -> None: python_implementation=self.python_version_tuple, ) - def get_instructions(self, x, first_line=None): + def get_instructions(self, x): """Iterator for the opcodes in methods, functions or code Generates a series of Instruction named tuples giving the details of @@ -234,7 +234,11 @@ def get_instructions(self, x, first_line=None): Otherwise, the source line information (if any) is taken directly from the disassembled code object. """ - return self.Bytecode(x).get_instructions(x, first_line) + if isinstance(x, str): + code_obj = compile(x, f"", "exec") + else: + code_obj = x + return self.Bytecode(code_obj).get_instructions(code_obj) def findlinestarts(self, code): """Find the offsets in a byte code which are start of lines in the source.