diff --git a/Lib/test/_code_definitions.py b/Lib/test/_code_definitions.py new file mode 100644 index 0000000000..70c44da2ec --- /dev/null +++ b/Lib/test/_code_definitions.py @@ -0,0 +1,323 @@ + +def simple_script(): + assert True + + +def complex_script(): + obj = 'a string' + pickle = __import__('pickle') + def spam_minimal(): + pass + spam_minimal() + data = pickle.dumps(obj) + res = pickle.loads(data) + assert res == obj, (res, obj) + + +def script_with_globals(): + obj1, obj2 = spam(42) + assert obj1 == 42 + assert obj2 is None + + +def script_with_explicit_empty_return(): + return None + + +def script_with_return(): + return True + + +def spam_minimal(): + # no arg defaults or kwarg defaults + # no annotations + # no local vars + # no free vars + # no globals + # no builtins + # no attr access (names) + # no code + return + + +def spam_with_builtins(): + x = 42 + values = (42,) + checks = tuple(callable(v) for v in values) + res = callable(values), tuple(values), list(values), checks + print(res) + + +def spam_with_globals_and_builtins(): + func1 = spam + func2 = spam_minimal + funcs = (func1, func2) + checks = tuple(callable(f) for f in funcs) + res = callable(funcs), tuple(funcs), list(funcs), checks + print(res) + + +def spam_with_global_and_attr_same_name(): + try: + spam_minimal.spam_minimal + except AttributeError: + pass + + +def spam_full_args(a, b, /, c, d, *args, e, f, **kwargs): + return (a, b, c, d, e, f, args, kwargs) + + +def spam_full_args_with_defaults(a=-1, b=-2, /, c=-3, d=-4, *args, + e=-5, f=-6, **kwargs): + return (a, b, c, d, e, f, args, kwargs) + + +def spam_args_attrs_and_builtins(a, b, /, c, d, *args, e, f, **kwargs): + if args.__len__() > 2: + return None + return a, b, c, d, e, f, args, kwargs + + +def spam_returns_arg(x): + return x + + +def spam_raises(): + raise Exception('spam!') + + +def spam_with_inner_not_closure(): + def eggs(): + pass + eggs() + + +def spam_with_inner_closure(): + x = 42 + def eggs(): + print(x) + eggs() + + +def spam_annotated(a: int, b: str, c: object) -> tuple: + return a, b, c + + +def spam_full(a, b, /, c, d:int=1, *args, e, f:object=None, **kwargs) -> tuple: + # arg defaults, kwarg defaults + # annotations + # all kinds of local vars, except cells + # no free vars + # some globals + # some builtins + # some attr access (names) + x = args + y = kwargs + z = (a, b, c, d) + kwargs['e'] = e + kwargs['f'] = f + extras = list((x, y, z, spam, spam.__name__)) + return tuple(a, b, c, d, e, f, args, kwargs), extras + + +def spam(x): + return x, None + + +def spam_N(x): + def eggs_nested(y): + return None, y + return eggs_nested, x + + +def spam_C(x): + a = 1 + def eggs_closure(y): + return None, y, a, x + return eggs_closure, a, x + + +def spam_NN(x): + def eggs_nested_N(y): + def ham_nested(z): + return None, z + return ham_nested, y + return eggs_nested_N, x + + +def spam_NC(x): + a = 1 + def eggs_nested_C(y): + def ham_closure(z): + return None, z, y, a, x + return ham_closure, y + return eggs_nested_C, a, x + + +def spam_CN(x): + a = 1 + def eggs_closure_N(y): + def ham_C_nested(z): + return None, z + return ham_C_nested, y, a, x + return eggs_closure_N, a, x + + +def spam_CC(x): + a = 1 + def eggs_closure_C(y): + b = 2 + def ham_C_closure(z): + return None, z, b, y, a, x + return ham_C_closure, b, y, a, x + return eggs_closure_C, a, x + + +eggs_nested, *_ = spam_N(1) +eggs_closure, *_ = spam_C(1) +eggs_nested_N, *_ = spam_NN(1) +eggs_nested_C, *_ = spam_NC(1) +eggs_closure_N, *_ = spam_CN(1) +eggs_closure_C, *_ = spam_CC(1) + +ham_nested, *_ = eggs_nested_N(2) +ham_closure, *_ = eggs_nested_C(2) +ham_C_nested, *_ = eggs_closure_N(2) +ham_C_closure, *_ = eggs_closure_C(2) + + +TOP_FUNCTIONS = [ + # shallow + simple_script, + complex_script, + script_with_globals, + script_with_explicit_empty_return, + script_with_return, + spam_minimal, + spam_with_builtins, + spam_with_globals_and_builtins, + spam_with_global_and_attr_same_name, + spam_full_args, + spam_full_args_with_defaults, + spam_args_attrs_and_builtins, + spam_returns_arg, + spam_raises, + spam_with_inner_not_closure, + spam_with_inner_closure, + spam_annotated, + spam_full, + spam, + # outer func + spam_N, + spam_C, + spam_NN, + spam_NC, + spam_CN, + spam_CC, +] +NESTED_FUNCTIONS = [ + # inner func + eggs_nested, + eggs_closure, + eggs_nested_N, + eggs_nested_C, + eggs_closure_N, + eggs_closure_C, + # inner inner func + ham_nested, + ham_closure, + ham_C_nested, + ham_C_closure, +] +FUNCTIONS = [ + *TOP_FUNCTIONS, + *NESTED_FUNCTIONS, +] + +STATELESS_FUNCTIONS = [ + simple_script, + complex_script, + script_with_explicit_empty_return, + script_with_return, + spam, + spam_minimal, + spam_with_builtins, + spam_full_args, + spam_args_attrs_and_builtins, + spam_returns_arg, + spam_raises, + spam_annotated, + spam_with_inner_not_closure, + spam_with_inner_closure, + spam_N, + spam_C, + spam_NN, + spam_NC, + spam_CN, + spam_CC, + eggs_nested, + eggs_nested_N, + ham_nested, + ham_C_nested +] +STATELESS_CODE = [ + *STATELESS_FUNCTIONS, + script_with_globals, + spam_full_args_with_defaults, + spam_with_globals_and_builtins, + spam_with_global_and_attr_same_name, + spam_full, +] + +PURE_SCRIPT_FUNCTIONS = [ + simple_script, + complex_script, + script_with_explicit_empty_return, + spam_minimal, + spam_with_builtins, + spam_raises, + spam_with_inner_not_closure, + spam_with_inner_closure, +] +SCRIPT_FUNCTIONS = [ + *PURE_SCRIPT_FUNCTIONS, + script_with_globals, + spam_with_globals_and_builtins, + spam_with_global_and_attr_same_name, +] + + +# generators + +def gen_spam_1(*args): + for arg in args: + yield arg + + +def gen_spam_2(*args): + yield from args + + +async def async_spam(): + pass +coro_spam = async_spam() +coro_spam.close() + + +async def asyncgen_spam(*args): + for arg in args: + yield arg +asynccoro_spam = asyncgen_spam(1, 2, 3) + + +FUNCTION_LIKE = [ + gen_spam_1, + gen_spam_2, + async_spam, + asyncgen_spam, +] +FUNCTION_LIKE_APPLIED = [ + coro_spam, # actually FunctionType? + asynccoro_spam, # actually FunctionType? +] diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index f2ef233a59..b4b15e29f2 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -6,8 +6,7 @@ ... return g ... -# TODO: RUSTPYTHON ->>> # dump(f.__code__) +>>> dump(f.__code__) name: f argcount: 1 posonlyargcount: 0 @@ -18,10 +17,9 @@ freevars: () nlocals: 2 flags: 3 -consts: ('None', '') +consts: ('',) -# TODO: RUSTPYTHON ->>> # dump(f(4).__code__) +>>> dump(f(4).__code__) name: g argcount: 1 posonlyargcount: 0 @@ -41,8 +39,7 @@ ... return c ... -# TODO: RUSTPYTHON ->>> # dump(h.__code__) +>>> dump(h.__code__) name: h argcount: 2 posonlyargcount: 0 @@ -60,8 +57,7 @@ ... print(obj.attr2) ... print(obj.attr3) -# TODO: RUSTPYTHON ->>> # dump(attrs.__code__) +>>> dump(attrs.__code__) name: attrs argcount: 1 posonlyargcount: 0 @@ -80,8 +76,7 @@ ... 53 ... 0x53 -# TODO: RUSTPYTHON ->>> # dump(optimize_away.__code__) +>>> dump(optimize_away.__code__) name: optimize_away argcount: 0 posonlyargcount: 0 @@ -91,15 +86,14 @@ cellvars: () freevars: () nlocals: 0 -flags: 3 +flags: 67108867 consts: ("'doc string'", 'None') >>> def keywordonly_args(a,b,*,k1): ... return a,b,k1 ... -# TODO: RUSTPYTHON ->>> # dump(keywordonly_args.__code__) +>>> dump(keywordonly_args.__code__) name: keywordonly_args argcount: 2 posonlyargcount: 0 @@ -116,8 +110,7 @@ ... return a,b,c ... -# TODO: RUSTPYTHON ->>> # dump(posonly_args.__code__) +>>> dump(posonly_args.__code__) name: posonly_args argcount: 3 posonlyargcount: 2 @@ -130,8 +123,78 @@ flags: 3 consts: ('None',) +>>> def has_docstring(x: str): +... 'This is a one-line doc string' +... x += x +... x += "hello world" +... # co_flags should be 0x4000003 = 67108867 +... return x + +>>> dump(has_docstring.__code__) +name: has_docstring +argcount: 1 +posonlyargcount: 0 +kwonlyargcount: 0 +names: () +varnames: ('x',) +cellvars: () +freevars: () +nlocals: 1 +flags: 67108867 +consts: ("'This is a one-line doc string'", "'hello world'") + +>>> async def async_func_docstring(x: str, y: str): +... "This is a docstring from async function" +... import asyncio +... await asyncio.sleep(1) +... # co_flags should be 0x4000083 = 67108995 +... return x + y + +>>> dump(async_func_docstring.__code__) +name: async_func_docstring +argcount: 2 +posonlyargcount: 0 +kwonlyargcount: 0 +names: ('asyncio', 'sleep') +varnames: ('x', 'y', 'asyncio') +cellvars: () +freevars: () +nlocals: 3 +flags: 67108995 +consts: ("'This is a docstring from async function'", 'None') + +>>> def no_docstring(x, y, z): +... return x + "hello" + y + z + "world" + +>>> dump(no_docstring.__code__) +name: no_docstring +argcount: 3 +posonlyargcount: 0 +kwonlyargcount: 0 +names: () +varnames: ('x', 'y', 'z') +cellvars: () +freevars: () +nlocals: 3 +flags: 3 +consts: ("'hello'", "'world'") + +>>> class class_with_docstring: +... '''This is a docstring for class''' +... '''This line is not docstring''' +... pass + +>>> print(class_with_docstring.__doc__) +This is a docstring for class + +>>> class class_without_docstring: +... pass + +>>> print(class_without_docstring.__doc__) +None """ +import copy import inspect import sys import threading @@ -147,10 +210,21 @@ ctypes = None from test.support import (cpython_only, check_impl_detail, requires_debug_ranges, - gc_collect) + gc_collect, Py_GIL_DISABLED, late_deletion) from test.support.script_helper import assert_python_ok -from test.support import threading_helper +from test.support import threading_helper, import_helper +from test.support.bytecode_helper import instructions_with_positions from opcode import opmap, opname +try: + from _testcapi import code_offset_to_line +except ModuleNotFoundError: + code_offset_to_line = None +try: + import _testinternalcapi +except ModuleNotFoundError: + _testinternalcapi = None +import test._code_definitions as defs + COPY_FREE_VARS = opmap['COPY_FREE_VARS'] @@ -176,11 +250,12 @@ def dump(co): def external_getitem(self, i): return f"Foreign getitem: {super().__getitem__(i)}" + class CodeTest(unittest.TestCase): @cpython_only def test_newempty(self): - import _testcapi + _testcapi = import_helper.import_module("_testcapi") co = _testcapi.code_newempty("filename", "funcname", 15) self.assertEqual(co.co_filename, "filename") self.assertEqual(co.co_name, "funcname") @@ -259,10 +334,11 @@ def func(): return x code = func.__code__ - # different co_name, co_varnames, co_consts + # Different co_name, co_varnames, co_consts. + # Must have the same number of constants and + # variables or we get crashes. def func2(): y = 2 - z = 3 return y code2 = func2.__code__ @@ -271,7 +347,7 @@ def func2(): ("co_posonlyargcount", 0), ("co_kwonlyargcount", 0), ("co_nlocals", 1), - ("co_stacksize", 0), + ("co_stacksize", 1), ("co_flags", code.co_flags | inspect.CO_COROUTINE), ("co_firstlineno", 100), ("co_code", code2.co_code), @@ -287,11 +363,17 @@ def func2(): with self.subTest(attr=attr, value=value): new_code = code.replace(**{attr: value}) self.assertEqual(getattr(new_code, attr), value) + new_code = copy.replace(code, **{attr: value}) + self.assertEqual(getattr(new_code, attr), value) new_code = code.replace(co_varnames=code2.co_varnames, co_nlocals=code2.co_nlocals) self.assertEqual(new_code.co_varnames, code2.co_varnames) self.assertEqual(new_code.co_nlocals, code2.co_nlocals) + new_code = copy.replace(code, co_varnames=code2.co_varnames, + co_nlocals=code2.co_nlocals) + self.assertEqual(new_code.co_varnames, code2.co_varnames) + self.assertEqual(new_code.co_nlocals, code2.co_nlocals) def test_nlocals_mismatch(self): def func(): @@ -330,8 +412,6 @@ def func(): with self.assertRaises(ValueError): co.replace(co_nlocals=co.co_nlocals + 1) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_shrinking_localsplus(self): # Check that PyCode_NewWithPosOnlyArgs resizes both # localsplusnames and localspluskinds, if an argument is a cell. @@ -347,7 +427,7 @@ def func(): new_code = code = func.__code__.replace(co_linetable=b'') self.assertEqual(list(new_code.co_lines()), []) - # TODO: RUSTPYTHON + # TODO: RUSTPYTHON; co_lnotab intentionally not implemented (deprecated since 3.12) @unittest.expectedFailure def test_co_lnotab_is_deprecated(self): # TODO: remove in 3.14 def func(): @@ -356,20 +436,75 @@ def func(): with self.assertWarns(DeprecationWarning): func.__code__.co_lnotab - # TODO: RUSTPYTHON + @unittest.skipIf(_testinternalcapi is None, '_testinternalcapi is missing') + def test_returns_only_none(self): + value = True + + def spam1(): + pass + def spam2(): + return + def spam3(): + return None + def spam4(): + if not value: + return + ... + def spam5(): + if not value: + return None + ... + lambda1 = (lambda: None) + for func in [ + spam1, + spam2, + spam3, + spam4, + spam5, + lambda1, + ]: + with self.subTest(func): + res = _testinternalcapi.code_returns_only_none(func.__code__) + self.assertTrue(res) + + def spam6(): + return True + def spam7(): + return value + def spam8(): + if value: + return None + return True + def spam9(): + if value: + return True + return None + lambda2 = (lambda: True) + for func in [ + spam6, + spam7, + spam8, + spam9, + lambda2, + ]: + with self.subTest(func): + res = _testinternalcapi.code_returns_only_none(func.__code__) + self.assertFalse(res) + + # TODO: RUSTPYTHON; replace() rejects invalid bytecodes for safety @unittest.expectedFailure def test_invalid_bytecode(self): def foo(): pass - # assert that opcode 229 is invalid - self.assertEqual(opname[229], '<229>') + # assert that opcode 127 is invalid + self.assertEqual(opname[127], '<127>') - # change first opcode to 0xeb (=229) + # change first opcode to 0x7f (=127) foo.__code__ = foo.__code__.replace( - co_code=b'\xe5' + foo.__code__.co_code[1:]) + co_code=b'\x7f' + foo.__code__.co_code[1:]) - msg = "unknown opcode 229" + msg = "unknown opcode 127" with self.assertRaisesRegex(SystemError, msg): foo() @@ -392,10 +527,8 @@ def test_co_positions_artificial_instructions(self): code = traceback.tb_frame.f_code artificial_instructions = [] - for instr, positions in zip( - dis.get_instructions(code, show_caches=True), - code.co_positions(), - strict=True + for instr, positions in instructions_with_positions( + dis.get_instructions(code), code.co_positions() ): # If any of the positions is None, then all have to # be None as well for the case above. There are still @@ -456,7 +589,7 @@ def f(): # co_positions behavior when info is missing. - # @requires_debug_ranges() + @requires_debug_ranges() def test_co_positions_empty_linetable(self): def func(): x = 1 @@ -513,6 +646,533 @@ def test_code_hash_uses_bytecode(self): self.assertNotEqual(c, c1) self.assertNotEqual(hash(c), hash(c1)) + @cpython_only + def test_code_equal_with_instrumentation(self): + """ GH-109052 + + Make sure the instrumentation doesn't affect the code equality + The validity of this test relies on the fact that "x is x" and + "x in x" have only one different instruction and the instructions + have the same argument. + + """ + code1 = compile("x is x", "example.py", "eval") + code2 = compile("x in x", "example.py", "eval") + sys._getframe().f_trace_opcodes = True + sys.settrace(lambda *args: None) + exec(code1, {'x': []}) + exec(code2, {'x': []}) + self.assertNotEqual(code1, code2) + sys.settrace(None) + + @unittest.skipIf(_testinternalcapi is None, "missing _testinternalcapi") + def test_local_kinds(self): + CO_FAST_ARG_POS = 0x02 + CO_FAST_ARG_KW = 0x04 + CO_FAST_ARG_VAR = 0x08 + CO_FAST_HIDDEN = 0x10 + CO_FAST_LOCAL = 0x20 + CO_FAST_CELL = 0x40 + CO_FAST_FREE = 0x80 + + POSONLY = CO_FAST_LOCAL | CO_FAST_ARG_POS + POSORKW = CO_FAST_LOCAL | CO_FAST_ARG_POS | CO_FAST_ARG_KW + KWONLY = CO_FAST_LOCAL | CO_FAST_ARG_KW + VARARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_POS + VARKWARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_KW + + funcs = { + defs.simple_script: {}, + defs.complex_script: { + 'obj': CO_FAST_LOCAL, + 'pickle': CO_FAST_LOCAL, + 'spam_minimal': CO_FAST_LOCAL, + 'data': CO_FAST_LOCAL, + 'res': CO_FAST_LOCAL, + }, + defs.script_with_globals: { + 'obj1': CO_FAST_LOCAL, + 'obj2': CO_FAST_LOCAL, + }, + defs.script_with_explicit_empty_return: {}, + defs.script_with_return: {}, + defs.spam_minimal: {}, + defs.spam_with_builtins: { + 'x': CO_FAST_LOCAL, + 'values': CO_FAST_LOCAL, + 'checks': CO_FAST_LOCAL, + 'res': CO_FAST_LOCAL, + }, + defs.spam_with_globals_and_builtins: { + 'func1': CO_FAST_LOCAL, + 'func2': CO_FAST_LOCAL, + 'funcs': CO_FAST_LOCAL, + 'checks': CO_FAST_LOCAL, + 'res': CO_FAST_LOCAL, + }, + defs.spam_with_global_and_attr_same_name: {}, + defs.spam_full_args: { + 'a': POSONLY, + 'b': POSONLY, + 'c': POSORKW, + 'd': POSORKW, + 'e': KWONLY, + 'f': KWONLY, + 'args': VARARGS, + 'kwargs': VARKWARGS, + }, + defs.spam_full_args_with_defaults: { + 'a': POSONLY, + 'b': POSONLY, + 'c': POSORKW, + 'd': POSORKW, + 'e': KWONLY, + 'f': KWONLY, + 'args': VARARGS, + 'kwargs': VARKWARGS, + }, + defs.spam_args_attrs_and_builtins: { + 'a': POSONLY, + 'b': POSONLY, + 'c': POSORKW, + 'd': POSORKW, + 'e': KWONLY, + 'f': KWONLY, + 'args': VARARGS, + 'kwargs': VARKWARGS, + }, + defs.spam_returns_arg: { + 'x': POSORKW, + }, + defs.spam_raises: {}, + defs.spam_with_inner_not_closure: { + 'eggs': CO_FAST_LOCAL, + }, + defs.spam_with_inner_closure: { + 'x': CO_FAST_CELL, + 'eggs': CO_FAST_LOCAL, + }, + defs.spam_annotated: { + 'a': POSORKW, + 'b': POSORKW, + 'c': POSORKW, + }, + defs.spam_full: { + 'a': POSONLY, + 'b': POSONLY, + 'c': POSORKW, + 'd': POSORKW, + 'e': KWONLY, + 'f': KWONLY, + 'args': VARARGS, + 'kwargs': VARKWARGS, + 'x': CO_FAST_LOCAL, + 'y': CO_FAST_LOCAL, + 'z': CO_FAST_LOCAL, + 'extras': CO_FAST_LOCAL, + }, + defs.spam: { + 'x': POSORKW, + }, + defs.spam_N: { + 'x': POSORKW, + 'eggs_nested': CO_FAST_LOCAL, + }, + defs.spam_C: { + 'x': POSORKW | CO_FAST_CELL, + 'a': CO_FAST_CELL, + 'eggs_closure': CO_FAST_LOCAL, + }, + defs.spam_NN: { + 'x': POSORKW, + 'eggs_nested_N': CO_FAST_LOCAL, + }, + defs.spam_NC: { + 'x': POSORKW | CO_FAST_CELL, + 'a': CO_FAST_CELL, + 'eggs_nested_C': CO_FAST_LOCAL, + }, + defs.spam_CN: { + 'x': POSORKW | CO_FAST_CELL, + 'a': CO_FAST_CELL, + 'eggs_closure_N': CO_FAST_LOCAL, + }, + defs.spam_CC: { + 'x': POSORKW | CO_FAST_CELL, + 'a': CO_FAST_CELL, + 'eggs_closure_C': CO_FAST_LOCAL, + }, + defs.eggs_nested: { + 'y': POSORKW, + }, + defs.eggs_closure: { + 'y': POSORKW, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + }, + defs.eggs_nested_N: { + 'y': POSORKW, + 'ham_nested': CO_FAST_LOCAL, + }, + defs.eggs_nested_C: { + 'y': POSORKW | CO_FAST_CELL, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + 'ham_closure': CO_FAST_LOCAL, + }, + defs.eggs_closure_N: { + 'y': POSORKW, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + 'ham_C_nested': CO_FAST_LOCAL, + }, + defs.eggs_closure_C: { + 'y': POSORKW | CO_FAST_CELL, + 'b': CO_FAST_CELL, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + 'ham_C_closure': CO_FAST_LOCAL, + }, + defs.ham_nested: { + 'z': POSORKW, + }, + defs.ham_closure: { + 'z': POSORKW, + 'y': CO_FAST_FREE, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + }, + defs.ham_C_nested: { + 'z': POSORKW, + }, + defs.ham_C_closure: { + 'z': POSORKW, + 'y': CO_FAST_FREE, + 'b': CO_FAST_FREE, + 'x': CO_FAST_FREE, + 'a': CO_FAST_FREE, + }, + } + assert len(funcs) == len(defs.FUNCTIONS) + for func in defs.FUNCTIONS: + with self.subTest(func): + expected = funcs[func] + kinds = _testinternalcapi.get_co_localskinds(func.__code__) + self.assertEqual(kinds, expected) + + @unittest.skipIf(_testinternalcapi is None, "missing _testinternalcapi") + def test_var_counts(self): + self.maxDiff = None + def new_var_counts(*, + posonly=0, + posorkw=0, + kwonly=0, + varargs=0, + varkwargs=0, + purelocals=0, + argcells=0, + othercells=0, + freevars=0, + globalvars=0, + attrs=0, + unknown=0, + ): + nargvars = posonly + posorkw + kwonly + varargs + varkwargs + nlocals = nargvars + purelocals + othercells + if isinstance(globalvars, int): + globalvars = { + 'total': globalvars, + 'numglobal': 0, + 'numbuiltin': 0, + 'numunknown': globalvars, + } + else: + g_numunknown = 0 + if isinstance(globalvars, dict): + numglobal = globalvars['numglobal'] + numbuiltin = globalvars['numbuiltin'] + size = 2 + if 'numunknown' in globalvars: + g_numunknown = globalvars['numunknown'] + size += 1 + assert len(globalvars) == size, globalvars + else: + assert not isinstance(globalvars, str), repr(globalvars) + try: + numglobal, numbuiltin = globalvars + except ValueError: + numglobal, numbuiltin, g_numunknown = globalvars + globalvars = { + 'total': numglobal + numbuiltin + g_numunknown, + 'numglobal': numglobal, + 'numbuiltin': numbuiltin, + 'numunknown': g_numunknown, + } + unbound = globalvars['total'] + attrs + unknown + return { + 'total': nlocals + freevars + unbound, + 'locals': { + 'total': nlocals, + 'args': { + 'total': nargvars, + 'numposonly': posonly, + 'numposorkw': posorkw, + 'numkwonly': kwonly, + 'varargs': varargs, + 'varkwargs': varkwargs, + }, + 'numpure': purelocals, + 'cells': { + 'total': argcells + othercells, + 'numargs': argcells, + 'numothers': othercells, + }, + 'hidden': { + 'total': 0, + 'numpure': 0, + 'numcells': 0, + }, + }, + 'numfree': freevars, + 'unbound': { + 'total': unbound, + 'globals': globalvars, + 'numattrs': attrs, + 'numunknown': unknown, + }, + } + + funcs = { + defs.simple_script: new_var_counts(), + defs.complex_script: new_var_counts( + purelocals=5, + globalvars=1, + attrs=2, + ), + defs.script_with_globals: new_var_counts( + purelocals=2, + globalvars=1, + ), + defs.script_with_explicit_empty_return: new_var_counts(), + defs.script_with_return: new_var_counts(), + defs.spam_minimal: new_var_counts(), + defs.spam_minimal: new_var_counts(), + defs.spam_with_builtins: new_var_counts( + purelocals=4, + globalvars=4, + ), + defs.spam_with_globals_and_builtins: new_var_counts( + purelocals=5, + globalvars=6, + ), + defs.spam_with_global_and_attr_same_name: new_var_counts( + globalvars=2, + attrs=1, + ), + defs.spam_full_args: new_var_counts( + posonly=2, + posorkw=2, + kwonly=2, + varargs=1, + varkwargs=1, + ), + defs.spam_full_args_with_defaults: new_var_counts( + posonly=2, + posorkw=2, + kwonly=2, + varargs=1, + varkwargs=1, + ), + defs.spam_args_attrs_and_builtins: new_var_counts( + posonly=2, + posorkw=2, + kwonly=2, + varargs=1, + varkwargs=1, + attrs=1, + ), + defs.spam_returns_arg: new_var_counts( + posorkw=1, + ), + defs.spam_raises: new_var_counts( + globalvars=1, + ), + defs.spam_with_inner_not_closure: new_var_counts( + purelocals=1, + ), + defs.spam_with_inner_closure: new_var_counts( + othercells=1, + purelocals=1, + ), + defs.spam_annotated: new_var_counts( + posorkw=3, + ), + defs.spam_full: new_var_counts( + posonly=2, + posorkw=2, + kwonly=2, + varargs=1, + varkwargs=1, + purelocals=4, + globalvars=3, + attrs=1, + ), + defs.spam: new_var_counts( + posorkw=1, + ), + defs.spam_N: new_var_counts( + posorkw=1, + purelocals=1, + ), + defs.spam_C: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + othercells=1, + ), + defs.spam_NN: new_var_counts( + posorkw=1, + purelocals=1, + ), + defs.spam_NC: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + othercells=1, + ), + defs.spam_CN: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + othercells=1, + ), + defs.spam_CC: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + othercells=1, + ), + defs.eggs_nested: new_var_counts( + posorkw=1, + ), + defs.eggs_closure: new_var_counts( + posorkw=1, + freevars=2, + ), + defs.eggs_nested_N: new_var_counts( + posorkw=1, + purelocals=1, + ), + defs.eggs_nested_C: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + freevars=2, + ), + defs.eggs_closure_N: new_var_counts( + posorkw=1, + purelocals=1, + freevars=2, + ), + defs.eggs_closure_C: new_var_counts( + posorkw=1, + purelocals=1, + argcells=1, + othercells=1, + freevars=2, + ), + defs.ham_nested: new_var_counts( + posorkw=1, + ), + defs.ham_closure: new_var_counts( + posorkw=1, + freevars=3, + ), + defs.ham_C_nested: new_var_counts( + posorkw=1, + ), + defs.ham_C_closure: new_var_counts( + posorkw=1, + freevars=4, + ), + } + assert len(funcs) == len(defs.FUNCTIONS), (len(funcs), len(defs.FUNCTIONS)) + for func in defs.FUNCTIONS: + with self.subTest(func): + expected = funcs[func] + counts = _testinternalcapi.get_code_var_counts(func.__code__) + self.assertEqual(counts, expected) + + func = defs.spam_with_globals_and_builtins + with self.subTest(f'{func} code'): + expected = new_var_counts( + purelocals=5, + globalvars=6, + ) + counts = _testinternalcapi.get_code_var_counts(func.__code__) + self.assertEqual(counts, expected) + + with self.subTest(f'{func} with own globals and builtins'): + expected = new_var_counts( + purelocals=5, + globalvars=(2, 4), + ) + counts = _testinternalcapi.get_code_var_counts(func) + self.assertEqual(counts, expected) + + with self.subTest(f'{func} without globals'): + expected = new_var_counts( + purelocals=5, + globalvars=(0, 4, 2), + ) + counts = _testinternalcapi.get_code_var_counts(func, globalsns={}) + self.assertEqual(counts, expected) + + with self.subTest(f'{func} without both'): + expected = new_var_counts( + purelocals=5, + globalvars=6, + ) + counts = _testinternalcapi.get_code_var_counts(func, globalsns={}, + builtinsns={}) + self.assertEqual(counts, expected) + + with self.subTest(f'{func} without builtins'): + expected = new_var_counts( + purelocals=5, + globalvars=(2, 0, 4), + ) + counts = _testinternalcapi.get_code_var_counts(func, builtinsns={}) + self.assertEqual(counts, expected) + + @unittest.skipIf(_testinternalcapi is None, "missing _testinternalcapi") + def test_stateless(self): + self.maxDiff = None + + STATELESS_FUNCTIONS = [ + *defs.STATELESS_FUNCTIONS, + # stateless with defaults + defs.spam_full_args_with_defaults, + ] + + for func in defs.STATELESS_CODE: + with self.subTest((func, '(code)')): + _testinternalcapi.verify_stateless_code(func.__code__) + for func in STATELESS_FUNCTIONS: + with self.subTest((func, '(func)')): + _testinternalcapi.verify_stateless_code(func) + + for func in defs.FUNCTIONS: + if func not in defs.STATELESS_CODE: + with self.subTest((func, '(code)')): + with self.assertRaises(Exception): + _testinternalcapi.verify_stateless_code(func.__code__) + + if func not in STATELESS_FUNCTIONS: + with self.subTest((func, '(func)')): + with self.assertRaises(Exception): + _testinternalcapi.verify_stateless_code(func) + def isinterned(s): return s is sys.intern(('_' + s + '_')[1:-1]) @@ -559,11 +1219,47 @@ def f(a='str_value'): self.assertIsInterned(f()) @cpython_only + @unittest.skipIf(Py_GIL_DISABLED, "free-threaded build interns all string constants") def test_interned_string_with_null(self): co = compile(r'res = "str\0value!"', '?', 'exec') v = self.find_const(co.co_consts, 'str\0value!') self.assertIsNotInterned(v) + @cpython_only + @unittest.skipUnless(Py_GIL_DISABLED, "does not intern all constants") + def test_interned_constants(self): + # compile separately to avoid compile time de-duping + + globals = {} + exec(textwrap.dedent(""" + def func1(): + return (0.0, (1, 2, "hello")) + """), globals) + + exec(textwrap.dedent(""" + def func2(): + return (0.0, (1, 2, "hello")) + """), globals) + + self.assertTrue(globals["func1"]() is globals["func2"]()) + + @cpython_only + def test_unusual_constants(self): + # gh-130851: Code objects constructed with constants that are not + # types generated by the bytecode compiler should not crash the + # interpreter. + class Unhashable: + def __hash__(self): + raise TypeError("unhashable type") + + class MyInt(int): + pass + + code = compile("a = 1", "", "exec") + code = code.replace(co_consts=(1, Unhashable(), MyInt(1), MyInt(1))) + self.assertIsInstance(code.co_consts[1], Unhashable) + self.assertEqual(code.co_consts[2], code.co_consts[3]) + class CodeWeakRefTest(unittest.TestCase): @@ -750,7 +1446,7 @@ def f(): co_code=bytes( [ dis.opmap["RESUME"], 0, - dis.opmap["LOAD_ASSERTION_ERROR"], 0, + dis.opmap["LOAD_COMMON_CONSTANT"], 0, dis.opmap["RAISE_VARARGS"], 1, ] ), @@ -769,6 +1465,81 @@ def f(): 3 * [(42, 42, None, None)], ) + @cpython_only + def test_docstring_under_o2(self): + code = textwrap.dedent(''' + def has_docstring(x, y): + """This is a first-line doc string""" + """This is a second-line doc string""" + a = x + y + b = x - y + return a, b + + + def no_docstring(x): + def g(y): + return x + y + return g + + + async def async_func(): + """asynf function doc string""" + pass + + + for func in [has_docstring, no_docstring(4), async_func]: + assert(func.__doc__ is None) + ''') + + rc, out, err = assert_python_ok('-OO', '-c', code) + + @unittest.skipUnless(code_offset_to_line, '_testcapi required') + def test_co_branches(self): + + def get_line_branches(func): + code = func.__code__ + base = code.co_firstlineno + return [ + ( + code_offset_to_line(code, src) - base, + code_offset_to_line(code, left) - base, + code_offset_to_line(code, right) - base + ) for (src, left, right) in + code.co_branches() + ] + + def simple(x): + if x: + A + else: + B + + self.assertEqual( + get_line_branches(simple), + [(1,2,4)]) + + def with_extended_args(x): + if x: + A.x; A.x; A.x; A.x; A.x; A.x; + A.x; A.x; A.x; A.x; A.x; A.x; + A.x; A.x; A.x; A.x; A.x; A.x; + A.x; A.x; A.x; A.x; A.x; A.x; + A.x; A.x; A.x; A.x; A.x; A.x; + else: + B + + self.assertEqual( + get_line_branches(with_extended_args), + [(1,2,8)]) + + async def afunc(): + async for letter in async_iter1: + 2 + 3 + + self.assertEqual( + get_line_branches(afunc), + [(1,1,3)]) if check_impl_detail(cpython=True) and ctypes is not None: py = ctypes.pythonapi @@ -794,6 +1565,11 @@ def myfree(ptr): FREE_FUNC = freefunc(myfree) FREE_INDEX = RequestCodeExtraIndex(FREE_FUNC) + # Make sure myfree sticks around at least as long as the interpreter, + # since we (currently) can't unregister the function and leaving a + # dangling pointer will cause a crash on deallocation of code objects if + # something else uses co_extras, like test_capi.test_misc. + late_deletion(myfree) class CoExtra(unittest.TestCase): def get_func(self): @@ -824,6 +1600,7 @@ def test_free_called(self): SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(100)) del f + gc_collect() # For free-threaded build self.assertEqual(LAST_FREED, 100) def test_get_set(self): @@ -854,13 +1631,19 @@ def __init__(self, f, test): self.test = test def run(self): del self.f - self.test.assertEqual(LAST_FREED, 500) + gc_collect() + # gh-117683: In the free-threaded build, the code object's + # destructor may still be running concurrently in the main + # thread. + if not Py_GIL_DISABLED: + self.test.assertEqual(LAST_FREED, 500) SetExtra(f.__code__, FREE_INDEX, ctypes.c_voidp(500)) tt = ThreadTest(f, self) del f tt.start() tt.join() + gc_collect() # For free-threaded build self.assertEqual(LAST_FREED, 500) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index bbc4602140..1297612224 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -30,7 +30,6 @@ def assertInvalid(self, str, symbol='single', is_syntax=1): except OverflowError: self.assertTrue(not is_syntax) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: at 0xc99532080 file "", line 1> != at 0xc99532f80 file "", line 1> def test_valid(self): av = self.assertValid diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 8ba783bf14..41e72df53f 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -1131,7 +1131,6 @@ def test_bug_46724(self): # Test that negative operargs are handled properly self.do_disassembly_test(bug46724, dis_bug46724) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_kw_names(self): # Test that value is displayed for keyword argument names: self.do_disassembly_test(wrap_func_w_kwargs, dis_kw_names) @@ -1179,7 +1178,6 @@ def test_disassemble_str(self): self.do_disassembly_test(fn_with_annotate_str, dis_fn_with_annotate_str) self.do_disassembly_test(compound_stmt_str, dis_compound_stmt_str) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_disassemble_bytes(self): self.do_disassembly_test(_f.__code__.co_code, dis_f_co_code) diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index 7afcb5e6f5..b8cdc4febc 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -123,7 +123,6 @@ def test_exceptions(self): self.assertEqual(StopIteration, new) class CodeTestCase(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_code(self): co = ExceptionTestCase.test_exceptions.__code__ new = marshal.loads(marshal.dumps(co)) diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 94c6642aac..3292ab419c 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -1081,6 +1081,14 @@ impl Compiler { ), }; + // Set CO_NESTED for scopes defined inside another function/class/etc. + // (i.e., not at module level) + let flags = if self.code_stack.len() > 1 { + flags | bytecode::CodeFlags::NESTED + } else { + flags + }; + // Get private name from parent scope let private = if !self.code_stack.is_empty() { self.code_stack.last().unwrap().private.clone() @@ -1202,7 +1210,8 @@ impl Compiler { // enter_scope sets default values based on scope_type, but push_output // allows callers to specify exact values if let Some(info) = self.code_stack.last_mut() { - info.flags = flags; + // Preserve NESTED flag set by enter_scope + info.flags = flags | (info.flags & bytecode::CodeFlags::NESTED); info.metadata.argcount = arg_count; info.metadata.posonlyargcount = posonlyarg_count; info.metadata.kwonlyargcount = kwonlyarg_count; @@ -2179,18 +2188,26 @@ impl Compiler { } } ast::Stmt::Expr(ast::StmtExpr { value, .. }) => { - self.compile_expression(value)?; + // Optimize away constant expressions with no side effects. + // In interactive mode, always compile (to print the result). + let dominated_by_interactive = + self.interactive && !self.ctx.in_func() && !self.ctx.in_class; + if !dominated_by_interactive && Self::is_const_expression(value) { + // Skip compilation entirely - the expression has no side effects + } else { + self.compile_expression(value)?; - if self.interactive && !self.ctx.in_func() && !self.ctx.in_class { - emit!( - self, - Instruction::CallIntrinsic1 { - func: bytecode::IntrinsicFunction1::Print - } - ); - } + if dominated_by_interactive { + emit!( + self, + Instruction::CallIntrinsic1 { + func: bytecode::IntrinsicFunction1::Print + } + ); + } - emit!(self, Instruction::PopTop); + emit!(self, Instruction::PopTop); + } } ast::Stmt::Global(_) | ast::Stmt::Nonlocal(_) => { // Handled during symbol table construction. @@ -3748,19 +3765,23 @@ impl Compiler { }); self.current_code_info().flags |= bytecode::CodeFlags::HAS_DOCSTRING; } - // If no docstring, don't add None to co_consts - // Note: RETURN_GENERATOR + POP_TOP for async functions is emitted in enter_scope() - // Compile body statements self.compile_statements(body)?; - // Emit None at end if needed + // Emit implicit `return None` if the body doesn't end with return. + // Also ensure None is in co_consts even when not emitting return + // (matching CPython: functions without explicit constants always + // have None in co_consts). match body.last() { Some(ast::Stmt::Return(_)) => {} _ => { self.emit_return_const(ConstantData::None); } } + // Functions with no other constants should still have None in co_consts + if self.current_code_info().metadata.consts.is_empty() { + self.arg_constant(ConstantData::None); + } // Exit scope and create function object let code = self.exit_scope(); @@ -6882,6 +6903,19 @@ impl Compiler { Ok(send_block) } + /// Returns true if the expression is a constant with no side effects. + fn is_const_expression(expr: &ast::Expr) -> bool { + matches!( + expr, + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::NumberLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_) + | ast::Expr::EllipsisLiteral(_) + ) + } + fn compile_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> { trace!("Compiling {expression:?}"); let range = expression.range(); diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 67c60dd561..8a34fced54 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -190,13 +190,7 @@ impl CodeInfo { ) -> crate::InternalResult { // Always fold tuple constants self.fold_tuple_constants(); - // Python only applies LOAD_SMALL_INT conversion to module-level code - // (not inside functions). Module code lacks OPTIMIZED flag. - // Note: RustPython incorrectly sets NEWLOCALS on modules, so only check OPTIMIZED - let is_module_level = !self.flags.contains(CodeFlags::OPTIMIZED); - if is_module_level { - self.convert_to_load_small_int(); - } + self.convert_to_load_small_int(); self.remove_unused_consts(); self.remove_nops(); @@ -786,8 +780,8 @@ impl CodeInfo { continue; }; - // Check if it's in small int range: -5 to 256 (_PY_IS_SMALL_INT) - if let Some(small) = value.to_i32().filter(|v| (-5..=256).contains(v)) { + // LOAD_SMALL_INT oparg is unsigned, so only 0..=255 can be encoded + if let Some(small) = value.to_i32().filter(|v| (0..=255).contains(v)) { // Convert LOAD_CONST to LOAD_SMALL_INT instr.instr = Instruction::LoadSmallInt { i: Arg::marker() }.into(); // The arg is the i32 value stored as u32 (two's complement) diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index 5120d371a3..fff56de5f5 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -371,6 +371,7 @@ bitflags! { const NEWLOCALS = 0x0002; const VARARGS = 0x0004; const VARKEYWORDS = 0x0008; + const NESTED = 0x0010; const GENERATOR = 0x0020; const COROUTINE = 0x0080; const ITERABLE_COROUTINE = 0x0100; diff --git a/crates/compiler-core/src/bytecode/instruction.rs b/crates/compiler-core/src/bytecode/instruction.rs index ea5fe18186..ed5cfb7245 100644 --- a/crates/compiler-core/src/bytecode/instruction.rs +++ b/crates/compiler-core/src/bytecode/instruction.rs @@ -446,13 +446,9 @@ impl TryFrom for Instruction { let instrumented_start = u8::from(Self::InstrumentedEndFor); let instrumented_end = u8::from(Self::InstrumentedLine); - // No RustPython-only opcodes anymore - all opcodes match CPython 3.14 - let custom_ops: &[u8] = &[]; - if (cpython_start..=cpython_end).contains(&value) || value == resume_id || value == enter_executor_id - || custom_ops.contains(&value) || (specialized_start..=specialized_end).contains(&value) || (instrumented_start..=instrumented_end).contains(&value) { diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index a7ef4c08a2..4ab4c7fefd 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -10,7 +10,7 @@ use crate::{ convert::{ToPyException, ToPyObject}, frozen, function::OptionalArg, - types::{Constructor, Representable}, + types::{Comparable, Constructor, Hashable, Representable}, }; use alloc::fmt; use core::{ @@ -447,6 +447,75 @@ impl Representable for PyCode { } } +impl Comparable for PyCode { + fn cmp( + zelf: &Py, + other: &PyObject, + op: crate::types::PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + let other = class_or_notimplemented!(Self, other); + let a = &zelf.code; + let b = &other.code; + let eq = a.obj_name == b.obj_name + && a.arg_count == b.arg_count + && a.posonlyarg_count == b.posonlyarg_count + && a.kwonlyarg_count == b.kwonlyarg_count + && a.flags == b.flags + && a.first_line_number == b.first_line_number + && a.instructions.original_bytes() == b.instructions.original_bytes() + && a.linetable == b.linetable + && a.exceptiontable == b.exceptiontable + && a.names == b.names + && a.varnames == b.varnames + && a.freevars == b.freevars + && a.cellvars == b.cellvars + && { + let a_consts: Vec<_> = a.constants.iter().map(|c| c.0.clone()).collect(); + let b_consts: Vec<_> = b.constants.iter().map(|c| c.0.clone()).collect(); + if a_consts.len() != b_consts.len() { + false + } else { + let mut eq = true; + for (ac, bc) in a_consts.iter().zip(b_consts.iter()) { + if !vm.bool_eq(ac, bc)? { + eq = false; + break; + } + } + eq + } + }; + Ok(eq.into()) + }) + } +} + +impl Hashable for PyCode { + fn hash(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let code = &zelf.code; + // Hash a tuple of key attributes, matching CPython's code_hash + let tuple = vm.ctx.new_tuple(vec![ + vm.ctx.new_str(code.obj_name.as_str()).into(), + vm.ctx.new_int(code.arg_count).into(), + vm.ctx.new_int(code.posonlyarg_count).into(), + vm.ctx.new_int(code.kwonlyarg_count).into(), + vm.ctx.new_int(code.varnames.len()).into(), + vm.ctx.new_int(code.flags.bits()).into(), + vm.ctx + .new_int(code.first_line_number.map_or(0, |n| n.get()) as i64) + .into(), + vm.ctx.new_bytes(code.instructions.original_bytes()).into(), + { + let consts: Vec<_> = code.constants.iter().map(|c| c.0.clone()).collect(); + vm.ctx.new_tuple(consts).into() + }, + ]); + tuple.as_object().hash(vm) + } +} + // Arguments for code object constructor #[derive(FromArgs)] pub struct PyCodeNewArgs { @@ -595,7 +664,10 @@ impl Constructor for PyCode { } } -#[pyclass(with(Representable, Constructor), flags(HAS_WEAKREF))] +#[pyclass( + with(Representable, Constructor, Comparable, Hashable), + flags(HAS_WEAKREF) +)] impl PyCode { #[pygetset] const fn co_posonlyargcount(&self) -> usize { @@ -721,6 +793,11 @@ impl PyCode { vm.ctx.new_bytes(self.code.exceptiontable.to_vec()) } + // spell-checker: ignore lnotab + // co_lnotab is intentionally not implemented. + // It was deprecated since 3.12 and scheduled for removal in 3.14. + // Use co_lines() or co_linetable instead. + #[pymethod] pub fn co_lines(&self, vm: &VirtualMachine) -> PyResult { // TODO: Implement lazy iterator (lineiterator) like CPython for better performance @@ -992,6 +1069,11 @@ impl PyCode { vm.call_method(list.as_object(), "__iter__", ()) } + #[pymethod] + pub fn __replace__(&self, args: ReplaceArgs, vm: &VirtualMachine) -> PyResult { + self.replace(args, vm) + } + #[pymethod] pub fn replace(&self, args: ReplaceArgs, vm: &VirtualMachine) -> PyResult { let ReplaceArgs {