Skip to content

Various minor crash/etc fixes #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c5a1271
Properly fix `Jump out of too many nested blocks` error
insignification Dec 10, 2019
8b431a2
Rename new SyntaxError to be more descriptive in case ever encountered.
Dec 10, 2019
c1613f5
Style & test updates (other updates to commit in following commits)
Dec 13, 2019
df40382
Refactor the ops sizing/writing to avoid code duplication
Dec 13, 2019
fedd2cb
Added python 3.8 support
Dec 12, 2019
5133d8c
Updated style & added py38 to tox/travis/readme
Dec 14, 2019
2d9047c
fix line endings snafu (you'll squash anyway, so I'm keeping history)
Dec 14, 2019
281cc1c
Remove the commented out tests (They'll come back in the next PR).
Dec 14, 2019
1e28f0f
Merge branch 'master' into py38
insignification Dec 14, 2019
e26a2d0
Refactor previous changes to avoid duplication & fix safety issues
Dec 12, 2019
5a8064d
Fix test_jump_out_of_try_block_and_survive.
Dec 13, 2019
3055aa7
Avoid patching code again and again in nested functions
Dec 13, 2019
16ad693
Fix previous change for python2.6 (doesn't support Code weakrefs)
Dec 13, 2019
5b7eb5a
Fix with blocks (add tests)
Dec 13, 2019
26efae6
Support jumping out of except & finally blocks!
Dec 13, 2019
2674255
Add combined try/catch/finally test and remove unneccessary op
Dec 13, 2019
28a3d87
Fixed two dumb bugs that cancelled each other out in most cases
Dec 13, 2019
0419965
Fix jumping out of with blocks in py26 (and added test)
Dec 13, 2019
ac926dc
Update merge
Dec 14, 2019
599edb9
Added while / while true tests & fix code to make them pass
Dec 14, 2019
46a1d6b
Merge branch 'master' into fix2
insignification Dec 15, 2019
bbb4b21
Merge branch 'master' into fix2
insignification Dec 15, 2019
cf0bccf
Style fixes
Dec 15, 2019
e3c23ce
Added tests that show remaining issues, first part of fixing them.
Dec 15, 2019
0835958
Backport change from 'iter' needed for the newly added tests
Dec 15, 2019
7d48292
Refactor _find_labels_and_gotos to use a class instead of nested funcs
Dec 15, 2019
c9fe97f
Avoid __pypy__ import and instead always call END_FINALLY when possible
Dec 15, 2019
27a27ba
Go a step further and always call END_FINALLY - make it possible in 3.8
Dec 15, 2019
bf3076f
flakate
Dec 15, 2019
e9480ff
Update comment
Dec 15, 2019
be15c5f
Minor code change
Dec 15, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "pypy"
- "pypy3"

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Pypi Entry](https://badge.fury.io/py/goto-statement.svg)](https://pypi.python.org/pypi/goto-statement)

A function decorator to use `goto` in Python.
Tested on Python 2.6 through 3.7 and PyPy.
Tested on Python 2.6 through 3.8 and PyPy.

[![](https://imgs.xkcd.com/comics/goto.png)](https://xkcd.com/292/)

Expand Down
198 changes: 170 additions & 28 deletions goto.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import array
import types
import functools

import weakref
import warnings

try:
_array_to_bytes = array.array.tobytes
Expand Down Expand Up @@ -34,6 +35,12 @@ def __init__(self):
self.have_argument = dis.HAVE_ARGUMENT
self.jump_unit = 1

self.has_loop_blocks = 'SETUP_LOOP' in dis.opmap
self.has_pop_except = 'POP_EXCEPT' in dis.opmap
self.has_setup_with = 'SETUP_WITH' in dis.opmap
self.has_setup_except = 'SETUP_EXCEPT' in dis.opmap
self.has_begin_finally = 'BEGIN_FINALLY' in dis.opmap

@property
def argument_bits(self):
return self.argument.size * 8
Expand All @@ -42,24 +49,35 @@ def argument_bits(self):
_BYTECODE = _Bytecode()


def _make_code(code, codestring):
args = [
code.co_argcount, code.co_nlocals, code.co_stacksize,
code.co_flags, codestring, code.co_consts,
code.co_names, code.co_varnames, code.co_filename,
code.co_name, code.co_firstlineno, code.co_lnotab,
code.co_freevars, code.co_cellvars
]
# use a weak dictionary in case code objects can be garbage-collected
_patched_code_cache = weakref.WeakKeyDictionary()
try:
_patched_code_cache[_Bytecode.__init__.__code__] = None
except TypeError:
_patched_code_cache = {} # ...unless not supported


def _make_code(code, codestring):
try:
args.insert(1, code.co_kwonlyargcount) # PY3
return code.replace(co_code=codestring) # new in 3.8+
except AttributeError:
pass
args = [
code.co_argcount, code.co_nlocals, code.co_stacksize,
code.co_flags, codestring, code.co_consts,
code.co_names, code.co_varnames, code.co_filename,
code.co_name, code.co_firstlineno, code.co_lnotab,
code.co_freevars, code.co_cellvars
]

try:
args.insert(1, code.co_kwonlyargcount) # PY3
except AttributeError:
pass

return types.CodeType(*args)
return types.CodeType(*args)


def _parse_instructions(code):
def _parse_instructions(code, yield_nones_at_end=0):
extended_arg = 0
extended_arg_offset = None
pos = 0
Expand All @@ -86,6 +104,9 @@ def _parse_instructions(code):
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):
size = 1
Expand Down Expand Up @@ -138,18 +159,87 @@ def _write_instructions(buf, pos, ops):
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):
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('<EXCEPT>', previous=exit_block)
elif exit_name == 'SETUP_FINALLY':
block_stack.push('<FINALLY>', 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]
Expand All @@ -160,24 +250,51 @@ def _find_labels_and_gotos(code):
))
labels[oparg2] = (offset1,
offset4,
tuple(block_stack))
block_stack.copy_to_list())
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()
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] == '<FINALLY>':
# in 3.8, only finally blocks are supported, so we must
# determine whether it's except/finally ourselves
block_stack.replace(top_block,
('<EXCEPT>',) + top_block[1:])
_, _, setup_block, _ = top_block
block_stack.replace(setup_block,
('SETUP_EXCEPT',) + setup_block[1:])
block_stack.pop_of_type('<EXCEPT>')
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] == '<FINALLY>':
block_stack.pop_of_type('<FINALLY>')
elif opname1 in ('WITH_CLEANUP', 'WITH_CLEANUP_START'):
if _BYTECODE.has_setup_with:
# temporary block to match END_FINALLY
block_stack.push('<FINALLY>')
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


Expand All @@ -187,6 +304,10 @@ def _inject_nop_sled(buf, pos, end):


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)

Expand All @@ -206,8 +327,26 @@ def _patch_code(code):
raise SyntaxError('Jump into different block')

ops = []
for i in range(len(origin_stack) - target_depth):
ops.append('POP_BLOCK')
for block, _, _, _ in reversed(origin_stack[target_depth:]):
if block == 'FOR_ITER':
ops.append('POP_TOP')
elif block == '<EXCEPT>':
ops.append('POP_EXCEPT')
elif block == '<FINALLY>':
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:
Expand All @@ -229,7 +368,10 @@ def _patch_code(code):
pos = _write_instructions(buf, pos, ops)
_inject_nop_sled(buf, pos, end)

return _make_code(code, _array_to_bytes(buf))
new_code = _make_code(code, _array_to_bytes(buf))

_patched_code_cache[code] = new_code
return new_code


def with_goto(func_or_code):
Expand Down
Loading