diff --git a/README.md b/README.md index 6f21c19..6a60be7 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ ti842py is a TI-BASIC to Python 3 transpiler. A transpiler is a piece of softwar - `Menu()` - `toString()` - `randInt()` - + - **Some drawing functions** + ### Planned Features - `Return` - `eval()`/`expr()` diff --git a/setup.py b/setup.py index 1a80b6a..711991f 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,8 @@ install_requires = [ "basically-ti-basic>=0.1.4", - "keyboard>=0.13.5", - "pythondialog>=3.5.1" + "pythondialog>=3.5.1", + "graphics.py>=5.0.0" ] with open("README.md", "r") as fh: diff --git a/ti842py/__version__.py b/ti842py/__version__.py index 8e40154..a1f13db 100644 --- a/ti842py/__version__.py +++ b/ti842py/__version__.py @@ -1,7 +1,7 @@ __title__ = "ti842py" __description__ = "TI-BASIC to Python 3 Transpiler" __url__ = "https://github.com/TabulateJarl8/ti842py" -__version__ = "0.3.1" +__version__ = "0.4.0" __author__ = "Tabulate" __author_email__ = "tabulatejarl8@gmail.com" __license__ = "GPLv3" diff --git a/ti842py/tiParser.py b/ti842py/tiParser.py index 504840c..29ae630 100644 --- a/ti842py/tiParser.py +++ b/ti842py/tiParser.py @@ -99,6 +99,8 @@ def __init__(self, basic): self.UTILS[os.path.splitext(file)[0]]["imports"] = [line.rstrip() for line in f.readlines() if line.startswith("import ") or line.startswith("from ")] self.UTILS[os.path.splitext(file)[0]]["enabled"] = False + self.drawLock = False + def convertLine(self, index, line): statement = "" # TODO: Make rules for :, dont fully understand it yet @@ -259,13 +261,74 @@ def convertLine(self, index, line): options.extend([tiMenu[i].strip(" \""), tiMenu[i + 1].strip(" \"")]) statement = menu(title, options) self.UTILS["menu"]["enabled"] = True + # Line( + elif line.startswith('Line('): + statement = closeOpen(line.replace('Line(', 'draw.line(')) + self.UTILS['draw']['enabled'] = True + + # BackgroundOff + elif line == 'BackgroundOff': + statement = 'draw.backgroundOff()' + self.UTILS['draw']['enabled'] = True + + # Background On + elif line.startswith('BackgroundOn '): + statement = line.replace('BackgroundOn ', 'draw.backgroundOn(') + statement = statement.split('(')[0] + '("' + statement.split('(')[1] + '")' + self.UTILS['draw']['enabled'] = True + + # ClrDraw + elif line == 'ClrDraw': + statement = 'draw.clrDraw()' + self.UTILS['draw']['enabled'] = True + + # Circle + elif line.startswith('Circle('): + statement = closeOpen(line.replace('Circle(', 'draw.circle(')) + self.UTILS['draw']['enabled'] = True + + # Text + elif line.startswith('Text('): + statement = closeOpen(line.replace('Text(', 'draw.text(')) + self.UTILS['draw']['enabled'] = True + + # Pxl-On + elif line.startswith('Pxl-On('): + statement = closeOpen(line.replace('Pxl-On(', 'draw.pxlOn(')) + self.UTILS['draw']['enabled'] = True + + # Pxl-Off + elif line.startswith('Pxl-Off('): + statement = closeOpen(line.replace('Pxl-Off(', 'draw.pxlOff(')) + self.UTILS['draw']['enabled'] = True + + # pxl-Test + elif line.startswith('pxl-Test('): + statement = closeOpen(line.replace('pxl-Test(', 'draw.pxlTest(')) + self.UTILS['draw']['enabled'] = True + + # Pt-On + elif line.startswith('Pt-On('): + statement = closeOpen(line.replace('Pt-On(', 'draw.ptOn(')) + self.UTILS['draw']['enabled'] = True + + # Pt-Off + elif line.startswith('Pt-Off('): + statement = closeOpen(line.replace('Pt-Off(', 'draw.ptOff(')) + self.UTILS['draw']['enabled'] = True + + # TextColor + elif line.startswith('TextColor('): + statement = closeOpen(line.replace('TextColor(', 'draw.textColor(')) + self.UTILS['draw']['enabled'] = True + else: # Things that can be alone on a line if line.startswith("getKey") or line.startswith("abs") or line.startswith("sqrt") or line.startswith("toString(") or line.startswith('randInt('): statement = line else: statement = "# UNKNOWN INDENTIFIER: {}".format(line) - logger.warning("Unknown indentifier on line %s", index) + logger.warning("Unknown indentifier on line %s", index + 1) # Fix things contained within statement @@ -313,6 +376,11 @@ def convertLine(self, index, line): statement = ' '.join(split_statement) self.UTILS['random']['enabled'] = True + + if self.UTILS['draw']['enabled'] == True and self.drawLock == False: + self.drawLock = True + statement = ['draw = Draw()', 'draw.openWindow()', statement] + return statement def toPython(self): diff --git a/ti842py/utils/draw.py b/ti842py/utils/draw.py new file mode 100644 index 0000000..89b0518 --- /dev/null +++ b/ti842py/utils/draw.py @@ -0,0 +1,160 @@ +from graphics import * +import time + +class Draw: + def __init__(self): + self.win = None + self.pixels = {} + self.points = {} + self.colors = {'BLUE': 'blue', 'RED': 'red', 'BLACK': 'black', 'MAGENTA': 'magenta', 'GREEN': 'green', 'ORANGE': 'orange', 'BROWN': 'brown', 'NAVY': 'navy', 'LTBLUE': 'light sky blue', 'YELLOW': 'yellow', 'WHITE': 'white', 'LTGRAY': 'light gray', 'MEDGRAY': 'dark gray', 'GRAY': 'gray', 'DARKGRAY': 'dark slate gray'} + self.colorNumbers = {'10': 'blue', '11': 'red', '12': 'black', '13': 'magenta', '14': 'green', '15': 'orange', '16': 'brown', '17': 'navy', '18': 'light sky blue', '19': 'yellow', '20': 'white', '21': 'light gray', '22': 'dark gray', '23': 'gray', '24': 'dark slate gray'} + self.currentTextColor = 'blue' + + def _slow(function): + def child_function(*args, **kwargs): + function(*args, **kwargs) + time.sleep(0.01) + return child_function + + def openWindow(self): + # The TI-84 Plus CE has a graph resolution of 250x160 pixels + # Not to be confused with the screen resolution of 320x240 pixels + self.win = GraphWin('ti842py', 250, 160) + self.win.setBackground('white') + + def closeWindow(self): + self.win.close() + + def graphCoordsToPixels(self, x, y, minX=-10, maxX=10, minY=-10, maxY=10): + # Can work for vertical or horizontal coords + xs = (maxX - minX) / 250 + ys = (minY - maxY) / 160 + horiz = (x - minX) / xs + vertical = (y - maxY) / ys + return horiz, vertical + + def tiColorToGraphicsColor(self, color): + color = str(color) + for color1, color2 in self.colors.items(): + color = color.replace(color1, color2) + for color1, color2 in self.colorNumbers.items(): + color = color.replace(color1, color2) + return color + + @_slow + def backgroundOn(self, color): + self.win.setBackground(self.tiColorToGraphicsColor(color)) + + @_slow + def backgroundOff(self): + self.win.setBackground('white') + + def clrDraw(self): + for item in self.win.items[:]: + item.undraw() + self.win.update() + + @_slow + def circle(self, x, y, r, color='blue', linestyle=''): + # TODO: Implement linestyle + c = Circle(Point(self.graphCoordsToPixels(x), self.graphCoordsToPixels(y)), r) + c.setOutline(self.tiColorToGraphicsColor(color)) + c.draw(self.win) + + @_slow + def line(self, x1, y1, x2, y2, erase=1, color='blue', style=1): + # TODO: Erase and style not implemented yet + x1, y1 = self.graphCoordsToPixels(x1, y1) + x2, y2 = self.graphCoordsToPixels(x2, y2) + line = Line(Point(x1, y1), Point(x2, y2)) + line.setOutline(self.tiColorToGraphicsColor(color)) + line.draw(self.win) + + @_slow + def textColor(self, color): + self.currentTextColor = self.tiColorToGraphicsColor(color) + + @_slow + def text(self, row, column, *args): + # TODO: Set font size and use row/column instead of x and y + text = ''.join(args) + message = Text(Point(column, row), text.upper()) + message.setTextColor(self.currentTextColor) + message.draw(self.win) + + @_slow + def pxlOn(self, row, column, color='blue'): + # Row = y; Column = x + pnt = Point(column, row) + pnt.setOutline(self.tiColorToGraphicsColor(color)) + if not column in self.pixels: + self.pixels[str(column)] = {} + self.pixels[str(column)][str(row)] = pnt + + pnt.draw(self.win) + + @_slow + def pxlOff(self, row, column): + if str(column) in self.pixels: + if str(row) in self.pixels[str(column)]: + self.pixels[str(column)][str(row)].undraw() + del self.pixels[str(column)][str(row)] + + @_slow + def pxlTest(self, row, column): + if str(column) in self.pixels and str(row) in self.pixels[str(column)]: + return True + return False + + @_slow + def ptOn(self, x, y, mark=1, color='blue'): + x, y = self.graphCoordsToPixels(x, y) + + if str(x) not in self.points: + self.points[str(x)] = {} + if str(y) not in self.points[str(x)]: + self.points[str(x)][str(y)] = {} + + + # If mark is unknown, it will default to 1, so test for 1 with `else` + + if mark in [2, 6]: + # 3x3 box + p1x = (x - 3 / 2) + p1y = (y + 3 / 2) + p2x = (x + 3 / 2) + p2y = (y - 3 / 2) + rec = Rectangle(Point(p1x, p1y), Point(p2x, p2y)) + rec.setOutline(self.tiColorToGraphicsColor(color)) + rec.draw(self.win) + self.points[str(x)][str(y)][str(mark)] = (rec,) + elif mark in [3, 7]: + # 3x3 cross + line1 = Line(Point(x - 2, y), Point(x + 2, y)) + line1.setOutline(self.tiColorToGraphicsColor(color)) + line1.draw(self.win) + line2 = Line(Point(x, y - 2), Point(x, y + 2)) + line2.setOutline(self.tiColorToGraphicsColor(color)) + line2.draw(self.win) + self.points[str(x)][str(y)][str(mark)] = (line1, line2) + else: + # Dot + p1x = (x - 2 / 2) + p1y = (y + 2 / 2) + p2x = (x + 2 / 2) + p2y = (y - 2 / 2) + rec = Rectangle(Point(p1x, p1y), Point(p2x, p2y)) + rec.setFill(self.tiColorToGraphicsColor(color)) + rec.setOutline(self.tiColorToGraphicsColor(color)) + rec.draw(self.win) + self.points[str(x)][str(y)][str(mark)] = (rec,) + + @_slow + def ptOff(self, x, y, mark=1): + x, y = self.graphCoordsToPixels(x, y) + if str(x) in self.points: + if str(y) in self.points[str(x)]: + if str(mark) in self.points[str(x)][str(y)]: + for item in self.points[str(x)][str(y)][str(mark)]: + item.undraw() + del self.points[str(x)][str(y)][str(mark)] diff --git a/ti842py/utils/getKey.py b/ti842py/utils/getKey.py index 046f423..aab940e 100644 --- a/ti842py/utils/getKey.py +++ b/ti842py/utils/getKey.py @@ -1,5 +1,98 @@ -import keyboard -import subprocess +import os + +# Windows +if os.name == 'nt': + import msvcrt + +# Posix (Linux, OS X) +else: + import sys + import termios + import atexit + from select import select + + +class KBHit: + + def __init__(self): + '''Creates a KBHit object that you can call to do various keyboard things. + ''' + + if os.name == 'nt': + pass + + else: + + # Save the terminal settings + self.fd = sys.stdin.fileno() + self.new_term = termios.tcgetattr(self.fd) + self.old_term = termios.tcgetattr(self.fd) + + # New terminal setting unbuffered + self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO) + termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term) + + # Support normal-terminal reset at exit + atexit.register(self.set_normal_term) + + + def set_normal_term(self): + ''' Resets to normal terminal. On Windows this is a no-op. + ''' + + if os.name == 'nt': + pass + + else: + termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) + + + def getch(self): + ''' Returns a keyboard character after kbhit() has been called. + Should not be called in the same program as getarrow(). + ''' + + s = '' + + if os.name == 'nt': + return msvcrt.getch().decode('utf-8') + + else: + return sys.stdin.read(1) + + + def getarrow(self): + ''' Returns an arrow-key code after kbhit() has been called. Codes are + 0 : up + 1 : right + 2 : down + 3 : left + Should not be called in the same program as getch(). + ''' + + if os.name == 'nt': + msvcrt.getch() # skip 0xE0 + c = msvcrt.getch() + vals = [72, 77, 80, 75] + + else: + c = sys.stdin.read(3)[2] + vals = [65, 67, 66, 68] + + return vals.index(ord(c.decode('utf-8'))) + + + def kbhit(self): + ''' Returns True if keyboard character was hit, False otherwise. + ''' + if os.name == 'nt': + return msvcrt.kbhit() + + else: + dr,dw,de = select([sys.stdin], [], [], 0) + return dr != [] + +kb = KBHit() keyMap = { "f1": 11, @@ -78,11 +171,10 @@ } def getKey(): - subprocess.check_call(["stty", "-echo"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - currentKey = keyboard.read_key().lower() - if currentKey in keyMap: - subprocess.check_call(["stty", "echo"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - return keyMap[currentKey] + if kb.kbhit(): + c = kb.getch().lower() + if c in keyMap: + return keyMap[c] + return 0 else: - subprocess.check_call(["stty", "echo"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - return 0 \ No newline at end of file + return 0 diff --git a/ti842py/utils/goto.py b/ti842py/utils/goto.py index f079bec..09002ef 100644 --- a/ti842py/utils/goto.py +++ b/ti842py/utils/goto.py @@ -3,7 +3,8 @@ import array import types import functools - +import weakref +import warnings try: _array_to_bytes = array.array.tobytes @@ -13,83 +14,98 @@ class _Bytecode: def __init__(self): - code = (lambda: x if x else y).__code__.co_code - opcode, oparg = struct.unpack_from('BB', code, 2) - - # Starting with Python 3.6, the bytecode format has changed, using - # 16-bit words (8-bit opcode + 8-bit argument) for each instruction, - # as opposed to previously 24 bit (8-bit opcode + 16-bit argument) - # for instructions that expect an argument and otherwise 8 bit. - # https://bugs.python.org/issue26647 - if dis.opname[opcode] == 'POP_JUMP_IF_FALSE': - self.argument = struct.Struct('B') - self.have_argument = 0 - # As of Python 3.6, jump targets are still addressed by their - # byte unit. This is matter to change, so that jump targets, - # in the future might refer to code units (address in bytes / 2). - # https://bugs.python.org/issue26647 - self.jump_unit = 8 // oparg - else: - self.argument = struct.Struct('= _BYTECODE.have_argument: - oparg = extended_arg | _BYTECODE.argument.unpack_from(code, pos)[0] - pos += _BYTECODE.argument.size + oparg = None + if opcode >= _BYTECODE.have_argument: + oparg = extended_arg | _BYTECODE.argument.unpack_from(code, pos)[0] + pos += _BYTECODE.argument.size - if opcode == dis.EXTENDED_ARG: - extended_arg = oparg << _BYTECODE.argument_bits - extended_arg_offset = offset - continue + if opcode == dis.EXTENDED_ARG: + extended_arg = oparg << _BYTECODE.argument_bits + extended_arg_offset = offset + continue - extended_arg = 0 - extended_arg_offset = None - yield (dis.opname[opcode], oparg, offset) + extended_arg = 0 + extended_arg_offset = None + yield (dis.opname[opcode], oparg, offset) + + for _ in range(yield_nones_at_end): + yield (None, None, None) def _get_instruction_size(opname, oparg=0): @@ -97,12 +113,12 @@ def _get_instruction_size(opname, oparg=0): extended_arg = oparg >> _BYTECODE.argument_bits if extended_arg != 0: - size += _get_instruction_size('EXTENDED_ARG', extended_arg) - oparg &= (1 << _BYTECODE.argument_bits) - 1 + size += _get_instruction_size('EXTENDED_ARG', extended_arg) + oparg &= (1 << _BYTECODE.argument_bits) - 1 opcode = dis.opmap[opname] if opcode >= _BYTECODE.have_argument: - size += _BYTECODE.argument.size + size += _BYTECODE.argument.size return size @@ -110,144 +126,265 @@ def _get_instruction_size(opname, oparg=0): def _get_instructions_size(ops): size = 0 for op in ops: - if isinstance(op, str): - size += _get_instruction_size(op) - else: - size += _get_instruction_size(*op) + if isinstance(op, str): + size += _get_instruction_size(op) + else: + size += _get_instruction_size(*op) return size def _write_instruction(buf, pos, opname, oparg=0): extended_arg = oparg >> _BYTECODE.argument_bits if extended_arg != 0: - pos = _write_instruction(buf, pos, 'EXTENDED_ARG', extended_arg) - oparg &= (1 << _BYTECODE.argument_bits) - 1 + pos = _write_instruction(buf, pos, 'EXTENDED_ARG', extended_arg) + oparg &= (1 << _BYTECODE.argument_bits) - 1 opcode = dis.opmap[opname] buf[pos] = opcode pos += 1 if opcode >= _BYTECODE.have_argument: - _BYTECODE.argument.pack_into(buf, pos, oparg) - pos += _BYTECODE.argument.size + _BYTECODE.argument.pack_into(buf, pos, oparg) + pos += _BYTECODE.argument.size return pos def _write_instructions(buf, pos, ops): for op in ops: - if isinstance(op, str): - pos = _write_instruction(buf, pos, op) - else: - pos = _write_instruction(buf, pos, *op) + if isinstance(op, str): + pos = _write_instruction(buf, pos, op) + else: + pos = _write_instruction(buf, pos, *op) return pos +def _warn_bug(msg): + warnings.warn("Internal error detected" + + " - result of with_goto may be incorrect. (%s)" % msg) + + +class _BlockStack(object): + def __init__(self, labels, gotos): + self.stack = [] + self.block_counter = 0 + self.last_block = None + self.labels = labels + self.gotos = gotos + + def _replace_in_stack(self, stack, old_block, new_block): + for i, block in enumerate(stack): + if block == old_block: + stack[i] = new_block + + def replace(self, old_block, new_block): + self._replace_in_stack(self.stack, old_block, new_block) + + for label in self.labels: + _, _, label_blocks = self.labels[label] + self._replace_in_stack(label_blocks, old_block, new_block) + + for goto in self.gotos: + _, _, _, goto_blocks = goto + self._replace_in_stack(goto_blocks, old_block, new_block) + + def push(self, opname, target_offset=None, previous=None): + self.block_counter += 1 + self.stack.append((opname, target_offset, + previous, self.block_counter)) + + def pop(self): + if self.stack: + self.last_block = self.stack.pop() + return self.last_block + else: + _warn_bug("can't pop block") + + def pop_of_type(self, type): + if self.stack and self.top()[0] != type: + _warn_bug("mismatched block type") + else: + return self.pop() + + def copy_to_list(self): + return list(self.stack) + + def top(self): + return self.stack[-1] if self.stack else None + + def __len__(self): + return len(self.stack) + + def _find_labels_and_gotos(code): labels = {} gotos = [] - block_stack = [] - block_counter = 0 + block_stack = _BlockStack(labels, gotos) opname1 = oparg1 = offset1 = None opname2 = oparg2 = offset2 = None opname3 = oparg3 = offset3 = None - for opname4, oparg4, offset4 in _parse_instructions(code.co_code): - if opname1 in ('LOAD_GLOBAL', 'LOAD_NAME'): - if opname2 == 'LOAD_ATTR' and opname3 == 'POP_TOP': - name = code.co_names[oparg1] - if name == 'label': - if oparg2 in labels: - raise SyntaxError('Ambiguous label {0!r}'.format( - code.co_names[oparg2] - )) - labels[oparg2] = (offset1, - offset4, - tuple(block_stack)) - elif name == 'goto': - gotos.append((offset1, - offset4, - oparg2, - tuple(block_stack))) - elif opname1 in ('SETUP_LOOP', - 'SETUP_EXCEPT', 'SETUP_FINALLY', - 'SETUP_WITH', 'SETUP_ASYNC_WITH'): - block_counter += 1 - block_stack.append(block_counter) - elif opname1 == 'POP_BLOCK' and block_stack: - block_stack.pop() - - opname1, oparg1, offset1 = opname2, oparg2, offset2 - opname2, oparg2, offset2 = opname3, oparg3, offset3 - opname3, oparg3, offset3 = opname4, oparg4, offset4 + for opname4, oparg4, offset4 in _parse_instructions(code.co_code, 3): + endoffset1 = offset2 + + # check for block exits + while block_stack and offset1 == block_stack.top()[1]: + exit_block = block_stack.pop() + exit_name = exit_block[0] + + if exit_name == 'SETUP_EXCEPT' and _BYTECODE.has_pop_except: + block_stack.push('', previous=exit_block) + elif exit_name == 'SETUP_FINALLY': + block_stack.push('', previous=exit_block) + + # check for special opcodes + if opname1 in ('LOAD_GLOBAL', 'LOAD_NAME'): + if opname2 == 'LOAD_ATTR' and opname3 == 'POP_TOP': + name = code.co_names[oparg1] + if name == 'label': + if oparg2 in labels: + raise SyntaxError('Ambiguous label {0!r}'.format( + code.co_names[oparg2] + )) + labels[oparg2] = (offset1, + offset4, + block_stack.copy_to_list()) + elif name == 'goto': + gotos.append((offset1, + offset4, + oparg2, + block_stack.copy_to_list())) + elif (opname1 in ('SETUP_LOOP', + 'SETUP_EXCEPT', 'SETUP_FINALLY', + 'SETUP_WITH', 'SETUP_ASYNC_WITH')) or \ + (not _BYTECODE.has_loop_blocks and opname1 == 'FOR_ITER'): + block_stack.push(opname1, endoffset1 + oparg1) + elif opname1 == 'POP_EXCEPT': + top_block = block_stack.top() + if not _BYTECODE.has_setup_except and \ + top_block and top_block[0] == '': + # in 3.8, only finally blocks are supported, so we must + # determine whether it's except/finally ourselves + block_stack.replace(top_block, + ('',) + top_block[1:]) + _, _, setup_block, _ = top_block + block_stack.replace(setup_block, + ('SETUP_EXCEPT',) + setup_block[1:]) + block_stack.pop_of_type('') + elif opname1 == 'END_FINALLY': + # Python puts END_FINALLY at the very end of except + # clauses, so we must ignore it in the wrong place. + if block_stack and block_stack.top()[0] == '': + block_stack.pop_of_type('') + elif opname1 in ('WITH_CLEANUP', 'WITH_CLEANUP_START'): + if _BYTECODE.has_setup_with: + # temporary block to match END_FINALLY + block_stack.push('') + else: + # python 2.6 - finally was actually with + last_block = block_stack.last_block + block_stack.replace(last_block, + ('SETUP_WITH',) + last_block[1:]) + + opname1, oparg1, offset1 = opname2, oparg2, offset2 + opname2, oparg2, offset2 = opname3, oparg3, offset3 + opname3, oparg3, offset3 = opname4, oparg4, offset4 + + if block_stack: + _warn_bug("block stack not empty") return labels, gotos def _inject_nop_sled(buf, pos, end): while pos < end: - pos = _write_instruction(buf, pos, 'NOP') + pos = _write_instruction(buf, pos, 'NOP') def _patch_code(code): + new_code = _patched_code_cache.get(code) + if new_code is not None: + return new_code + labels, gotos = _find_labels_and_gotos(code) buf = array.array('B', code.co_code) for pos, end, _ in labels.values(): - _inject_nop_sled(buf, pos, end) + _inject_nop_sled(buf, pos, end) for pos, end, label, origin_stack in gotos: - try: - _, target, target_stack = labels[label] - except KeyError: - raise SyntaxError('Unknown label {0!r}'.format( - code.co_names[label] - )) - - target_depth = len(target_stack) - if origin_stack[:target_depth] != target_stack: - raise SyntaxError('Jump into different block') - - ops = [] - for i in range(len(origin_stack) - target_depth): - ops.append('POP_BLOCK') - ops.append(('JUMP_ABSOLUTE', target // _BYTECODE.jump_unit)) - - if pos + _get_instructions_size(ops) > end: - # not enough space, add code at buffer end and jump there - buf_end = len(buf) - - go_to_end_ops = [('JUMP_ABSOLUTE', buf_end // _BYTECODE.jump_unit)] - - if pos + _get_instructions_size(go_to_end_ops) > end: - # not sure if reachable - raise SyntaxError('Goto in an incredibly huge function') - - pos = _write_instructions(buf, pos, go_to_end_ops) - _inject_nop_sled(buf, pos, end) - - buf.extend([0] * _get_instructions_size(ops)) - _write_instructions(buf, buf_end, ops) - else: - pos = _write_instructions(buf, pos, ops) - _inject_nop_sled(buf, pos, end) - - return _make_code(code, _array_to_bytes(buf)) + try: + _, target, target_stack = labels[label] + except KeyError: + raise SyntaxError('Unknown label {0!r}'.format( + code.co_names[label] + )) + + target_depth = len(target_stack) + if origin_stack[:target_depth] != target_stack: + raise SyntaxError('Jump into different block') + + ops = [] + for block, _, _, _ in reversed(origin_stack[target_depth:]): + if block == 'FOR_ITER': + ops.append('POP_TOP') + elif block == '': + ops.append('POP_EXCEPT') + elif block == '': + ops.append('END_FINALLY') + else: + ops.append('POP_BLOCK') + if block in ('SETUP_WITH', 'SETUP_ASYNC_WITH'): + ops.append('POP_TOP') + # END_FINALLY is needed only in pypy, + # but seems logical everywhere + if block in ('SETUP_FINALLY', 'SETUP_WITH', + 'SETUP_ASYNC_WITH'): + ops.append('BEGIN_FINALLY' if + _BYTECODE.has_begin_finally else + ('LOAD_CONST', code.co_consts.index(None))) + ops.append('END_FINALLY') + + ops.append(('JUMP_ABSOLUTE', target // _BYTECODE.jump_unit)) + + if pos + _get_instructions_size(ops) > end: + # not enough space, add code at buffer end and jump there + buf_end = len(buf) + + go_to_end_ops = [('JUMP_ABSOLUTE', buf_end // _BYTECODE.jump_unit)] + + if pos + _get_instructions_size(go_to_end_ops) > end: + # not sure if reachable + raise SyntaxError('Goto in an incredibly huge function') + + pos = _write_instructions(buf, pos, go_to_end_ops) + _inject_nop_sled(buf, pos, end) + + buf.extend([0] * _get_instructions_size(ops)) + _write_instructions(buf, buf_end, ops) + else: + pos = _write_instructions(buf, pos, ops) + _inject_nop_sled(buf, pos, end) + + new_code = _make_code(code, _array_to_bytes(buf)) + + _patched_code_cache[code] = new_code + return new_code def with_goto(func_or_code): if isinstance(func_or_code, types.CodeType): - return _patch_code(func_or_code) + return _patch_code(func_or_code) return functools.update_wrapper( - types.FunctionType( - _patch_code(func_or_code.__code__), - func_or_code.__globals__, - func_or_code.__name__, - func_or_code.__defaults__, - func_or_code.__closure__, - ), - func_or_code + types.FunctionType( + _patch_code(func_or_code.__code__), + func_or_code.__globals__, + func_or_code.__name__, + func_or_code.__defaults__, + func_or_code.__closure__, + ), + func_or_code )