Align compiler and AST behavior with CPython (#8138)

* Align compiler and AST behavior with CPython

Update the compiler pipeline, AST module support, and related VM compile/eval paths toward CPython 3.14 behavior.

Use upstream Ruff parser crates directly and keep RustPython-specific syntax preflight handling in the compile path.

Refresh opcode metadata, snapshots, and targeted tests for the aligned bytecode and AST behavior.

* fix

* Fix PR 8138 CI failures

* Fix match mapping wildcard scan complexity

* fixes

* Use Ruff runtime AST fields

* Minimize residual patch diff

* refactor error handling

* Remove _ast conversion context wrappers

* Clean up _ast conversion glue and compiler warning escalation

Rename the Node conversion parameter `ctx` to `vm` throughout the _ast
module.

Remove the unused compile_program, compile_program_single,
compile_block_expression, and compile_expression forwarders; compile_top
stays as the entry point.

Stash an escalated compiler SyntaxWarning so a non-SyntaxWarning category
propagates unchanged instead of being rewritten to SyntaxError, matching
PyErr_ExceptionMatches(SyntaxWarning) in compiler_warn. Drop the now-unused
CompileWarningError::into_codegen_error.

Replace the PositionalArguments two-variant enum with a struct holding a
shared range and a PositionalArgumentsKind enum.

Assisted-by: Claude

* Represent compiler flags as a CompilerFlags bitflags

Replace the loose PY_CF_*/CO_* i32 constants in compile_mode with a
CompilerFlags bitflags covering the PyCF_* flags and __future__
CO_FUTURE_* bits. The Python-visible ast.PyCF_* attribute values are
derived from it via bits(); parser input modes stay plain consts.

builtins::compile and the compile_string flag handling test flags
through CompilerFlags::contains instead of raw bit math.

Assisted-by: Claude

* Fix PR 8138 test regressions

* Fix PR 8138 CI failures

* Avoid shell interpolation in Linux deps action

* Fix review findings in compiler/AST changes

- frame.rs: compute the MATCH_CLASS class name lazily so a successful
  match no longer takes the type name lock or allocates; the
  __match_args__-not-a-tuple branch reuses it instead of recomputing.
- _ast/constant.rs: add the missing leading space to the with_recursion
  label ("maximum recursion depth exceeded during compilation").
- sys.set_int_max_str_digits: restore the "maxdigits must be 0 or larger
  than {n}" error text.
- codegen: set CO_MAXBLOCKS to 20 in both ir.rs and compile.rs.
- _ast/exception.rs: extract the duplicated excepthandler node guard
  into ensure_excepthandler_node.
- codegen: correct the validate_keywords doc comment.
- Drop redundant "CPython" qualifiers from comments added in this branch.

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-06-26 18:22:08 +09:00
1 parent 473ba7391a
commit a1ba6a662c
105 files changed
+22864 -6645

No files matched your search

+3
View File
@@ -66,8 +66,11 @@
"deoptimize",
"emscripten",
"excs",
"flufl",
"fnfe",
"fsdefault",
"ifexp",
"implicits",
"interps",
"jitted",
"jitting",
+32 -7
View File
@@ -39,11 +39,36 @@ runs:
- name: Install Linux dependencies
shell: bash
if: ${{ runner.os == 'Linux' }}
run: >
sudo apt-get update
env:
GCC_MULTILIB: ${{ inputs.gcc-multilib }}
MUSL_TOOLS: ${{ inputs.musl-tools }}
CLANG: ${{ inputs.clang }}
GCC_AARCH64_LINUX_GNU: ${{ inputs.gcc-aarch64-linux-gnu }}
run: |
if ! sudo apt-get update; then
echo "::warning::apt-get update failed; disabling nonessential Microsoft apt sources and retrying"
for source in /etc/apt/sources.list.d/*microsoft* /etc/apt/sources.list.d/*azure-cli*; do
if [ -e "$source" ]; then
sudo mv "$source" "$source.disabled"
fi
done
sudo apt-get update
fi
sudo apt-get install --no-install-recommends
${{ fromJSON(inputs.gcc-multilib) && 'gcc-multilib' || '' }}
${{ fromJSON(inputs.musl-tools) && 'musl-tools' || '' }}
${{ fromJSON(inputs.clang) && 'clang' || '' }}
${{ fromJSON(inputs.gcc-aarch64-linux-gnu) && 'gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross' || '' }}
packages=()
if [[ "$GCC_MULTILIB" == "true" ]]; then
packages+=(gcc-multilib)
fi
if [[ "$MUSL_TOOLS" == "true" ]]; then
packages+=(musl-tools)
fi
if [[ "$CLANG" == "true" ]]; then
packages+=(clang)
fi
if [[ "$GCC_AARCH64_LINUX_GNU" == "true" ]]; then
packages+=(gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross)
fi
if ((${#packages[@]})); then
sudo apt-get install --no-install-recommends "${packages[@]}"
fi
Generated
+18 -15
View File
@@ -3631,9 +3631,8 @@ dependencies = [
[[package]]
name = "rustpython-ruff_python_ast"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f021ff72cabf5e2cd6d8ec8813d376a8445a228dc610ab56c27bd9054cda70d4"
version = "0.15.9"
source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
dependencies = [
"aho-corasick",
"bitflags 2.13.0",
@@ -3650,9 +3649,8 @@ dependencies = [
[[package]]
name = "rustpython-ruff_python_parser"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e6ee78bd9671fb5766664b2695fe1f2a92a961f4d9101646c570d8acdb1e0b"
version = "0.15.9"
source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
dependencies = [
"bitflags 2.13.0",
"bstr",
@@ -3671,9 +3669,8 @@ dependencies = [
[[package]]
name = "rustpython-ruff_python_trivia"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e7cfd1056f3a02ff0d2d0e4474286ca963260782f878b7b81c1dd87432e682"
version = "0.15.9"
source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
dependencies = [
"itertools 0.14.0",
"rustpython-ruff_source_file",
@@ -3683,9 +3680,8 @@ dependencies = [
[[package]]
name = "rustpython-ruff_source_file"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "948107aad62ddb12a11fc7bf68a49e52a0b0a3737d415a2505e54f5a9edac737"
version = "0.15.9"
source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
dependencies = [
"memchr",
"rustpython-ruff_text_size",
@@ -3693,9 +3689,8 @@ dependencies = [
[[package]]
name = "rustpython-ruff_text_size"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8291ee0f5a779e54ccd4e0151a0c426f8b49a123f99b5b6545db17ccdd4277aa"
version = "0.15.9"
source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
dependencies = [
"get-size2",
]
@@ -3855,6 +3850,7 @@ dependencies = [
"static_assertions",
"strum",
"strum_macros",
"thin-vec",
"thiserror",
"timsort",
"wasm-bindgen",
@@ -3881,6 +3877,7 @@ dependencies = [
"js-sys",
"rustpython-common",
"rustpython-pylib",
"rustpython-ruff_text_size",
"rustpython-stdlib",
"rustpython-vm",
"serde-wasm-bindgen",
@@ -4363,6 +4360,12 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "thin-vec"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482"
[[package]]
name = "thiserror"
version = "2.0.18"
+6 -13
View File
@@ -183,19 +183,11 @@ rustpython-sre_engine = { path = "crates/sre_engine", version = "0.5.0" }
rustpython-wtf8 = { path = "crates/wtf8", version = "0.5.0" }
rustpython-doc = { path = "crates/doc", version = "0.5.0" }
# Use RustPython-packaged Ruff crates from the published fork while keeping
# existing crate names in the codebase.
ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.15.8" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.15.8" }
ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.15.8" }
ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8" }
# To update ruff crates, comment out the above lines and uncomment the following lines to pull directly from the Ruff repository at the specified commit hash.
# Ruff tag 0.15.8 is based on commit c2a8815842f9dc5d24ec19385eae0f1a7188b0d9
# at the time of this capture. We use the commit hash to ensure reproducible builds.
# ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# Use the RustPython Ruff fork for RustPython public `_ast` metadata.
ruff_python_parser = { package = "rustpython-ruff_python_parser", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_text_size = { package = "rustpython-ruff_text_size", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_source_file = { package = "rustpython-ruff_source_file", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] }
phf = { version = "0.13.1", default-features = false, features = ["macros"]}
@@ -311,6 +303,7 @@ tcl-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0"
textwrap = { version = "0.16.2", default-features = false }
termios = "0.3.3"
thiserror = "2.0"
thin-vec = "0.2.14"
timsort = "0.1.2"
tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" }
icu_casemap = "2"
-10
View File
@@ -150,7 +150,6 @@ class AST_Tests(unittest.TestCase):
self.assertRaises(TypeError, ast.parse, ast.Constant(42),
optimize=optval)
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_optimization_levels__debug__(self):
cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)]
for (optval, expected) in cases:
@@ -586,7 +585,6 @@ class AST_Tests(unittest.TestCase):
compile(m, "<test>", "exec")
self.assertIn("but got expr()", str(cm.exception))
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: expected str for name
def test_invalid_identifier(self):
m = ast.Module([ast.Expr(ast.Name(42, ast.Load()))], [])
ast.fix_missing_locations(m)
@@ -1365,7 +1363,6 @@ class CopyTests(unittest.TestCase):
self.assertIs(repl.ctx, context)
self.assertRaises(AttributeError, getattr, repl, 'extra')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "Name\.__replace__\ missing\ 1\ keyword\ argument:\ 'id'\." does not match "replace() does not support Name objects"
def test_replace_reject_missing_field(self):
# case: warn if deleted field is not replaced
node = ast.parse('x').body[0].value
@@ -1700,7 +1697,6 @@ Module(
full="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)], type_ignores=[])",
)
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^
def test_copy_location(self):
src = ast.parse('1 + 1', mode='eval')
src.body.right = ast.copy_location(ast.Constant(2), src.body.right)
@@ -1737,7 +1733,6 @@ Module(
"end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)])"
)
@unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
def test_increment_lineno(self):
src = ast.parse('1 + 1', mode='eval')
self.assertEqual(ast.increment_lineno(src, n=3), src)
@@ -1959,7 +1954,6 @@ Module(
(\
\ ''')
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: required field "lineno" missing from alias
def test_bad_integer(self):
# issue13436: Bad error message with invalid numeric values
body = [ast.ImportFrom(module='time',
@@ -3259,7 +3253,6 @@ class ASTConstructorTests(unittest.TestCase):
r"MyAttrs.__init__ got an unexpected keyword argument 'c'."):
obj = MyAttrs(c=3)
@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_fields_and_types_no_default(self):
class FieldsAndTypesNoDefault(ast.AST):
_fields = ('a',)
@@ -3273,7 +3266,6 @@ class ASTConstructorTests(unittest.TestCase):
obj = FieldsAndTypesNoDefault(a=1)
self.assertEqual(obj.a, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_incomplete_field_types(self):
class MoreFieldsThanTypes(ast.AST):
_fields = ('a', 'b')
@@ -3293,7 +3285,6 @@ class ASTConstructorTests(unittest.TestCase):
self.assertEqual(obj.a, 1)
self.assertEqual(obj.b, 2)
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'str' but 'bytes' found.
def test_malformed_fields_with_bytes(self):
class BadFields(ast.AST):
_fields = (b'\xff'*64,)
@@ -3713,7 +3704,6 @@ class ASTOptimizationTests(unittest.TestCase):
f"{ast.dump(optimized_tree)}",
)
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_format(self):
code = "'%s' % (a,)"
-1
View File
@@ -77,7 +77,6 @@ class AuditTest(unittest.TestCase):
def test_open(self):
self.do_test("test_open", os_helper.TESTFN)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_cantrace(self):
self.do_test("test_cantrace")
-3
View File
@@ -486,7 +486,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
msg=f"source={source} mode={mode}")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_top_level_await(self):
"""Test whether code with top level await can be compiled.
@@ -627,7 +626,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
exec(co, glob)
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <_ast.Name object at 0xb40000731e3d1360> is not an instance of <class '_ast.Constant'>
def test_compile_ast(self):
args = ("a*__debug__", "f.py", "exec")
raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0]
@@ -1020,7 +1018,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
finally:
sys.stdout = savestdout
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument closure
def test_exec_closure(self):
def function_without_closures():
return 3 * 5
-1
View File
@@ -645,7 +645,6 @@ class CmdLineTest(unittest.TestCase):
self.assertNotIn("\f", text)
self.assertIn("\n 1 + 1 = 2\n ^^^^^\n", text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntaxerror_multi_line_fstring(self):
script = 'foo = f"""{}\nfoo"""\n'
with os_helper.temp_dir() as script_dir:
-1
View File
@@ -279,7 +279,6 @@ class CodeopTests(unittest.TestCase):
self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename,
compile("a = 1\n", "def", 'single').co_filename)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 2
def test_warning(self):
# Test that the warning is only returned once.
with warnings_helper.check_warnings(
-12
View File
@@ -209,7 +209,6 @@ class TestSpecifics(unittest.TestCase):
self.assertEqual(eval("0o777"), 511)
self.assertEqual(eval("-0o0000010"), -8)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_int_literals_too_long(self):
n = 3000
source = f"a = 1\nb = 2\nc = {'3'*n}\nd = 4"
@@ -283,7 +282,6 @@ class TestSpecifics(unittest.TestCase):
self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'single')
self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile
def test_import(self):
succeed = [
'import sys',
@@ -348,7 +346,6 @@ class TestSpecifics(unittest.TestCase):
l = lambda: "this is the only const"
self.assertEqual(l.__code__.co_consts, ("this is the only const",))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile
def test_encoding(self):
code = b'# -*- coding: badencoding -*-\npass\n'
self.assertRaises(SyntaxError, compile, code, 'tmp', 'exec')
@@ -465,7 +462,6 @@ class TestSpecifics(unittest.TestCase):
# See gh-113054
compile('if (5 if 5 else T): 0', '<eval>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_condition_expression_with_redundant_comparisons_compiles(self):
# See gh-113054, gh-114083
exprs = [
@@ -580,7 +576,6 @@ class TestSpecifics(unittest.TestCase):
compile(ast.fix_missing_locations(tree), "<file>", "exec")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <code object <module> at 0xb77555080 file "<assign>1", line 1> != <code object <module> at 0xb77554f00 file "<assign>3", line 1>
def test_compile_ast(self):
fname = __file__
if fname.lower().endswith('pyc'):
@@ -696,7 +691,6 @@ class TestSpecifics(unittest.TestCase):
self.compile_single("class T:\n pass")
self.compile_single("c = '''\na=1\nb=2\nc=3\n'''")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile_single
def test_bad_single_statement(self):
self.assertInvalidSingle('1\n2')
self.assertInvalidSingle('def f(): pass')
@@ -708,7 +702,6 @@ class TestSpecifics(unittest.TestCase):
self.assertInvalidSingle('x = 5 # comment\nx = 6\n')
self.assertInvalidSingle("c = '''\nd=1\n'''\na = 1\n\nb = 2\n")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'source code cannot contain null bytes' not found in b'OSError: stream did not contain valid UTF-8\n'
def test_particularly_evil_undecodable(self):
# Issue 24022
src = b'0000\x00\n00000000000\n\x00\n\x9e\n'
@@ -719,7 +712,6 @@ class TestSpecifics(unittest.TestCase):
res = script_helper.run_python_until_end(fn)[0]
self.assertIn(b"source code cannot contain null bytes", res.err)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'source code cannot contain null bytes' not found in b'OSError: stream did not contain valid UTF-8\n'
def test_yet_more_evil_still_undecodable(self):
# Issue #25388
src = b"#\x00\n#\xfd\n"
@@ -756,7 +748,6 @@ class TestSpecifics(unittest.TestCase):
# check_limit("a", " if a else a")
# check_limit("if a: pass", "\nelif a: pass", mode="exec")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot contain null" does not match "invalid syntax (<dummy>, line 1)"
def test_null_terminated(self):
# The source code is null-terminated internally, but bytes-like
# objects are accepted, which could be not terminated.
@@ -1673,7 +1664,6 @@ class TestSpecifics(unittest.TestCase):
self.assertRaises(NameError, ns['foo'])
@unittest.expectedFailure # TODO: RUSTPYTHON; + [3, 5, 3, 5]
def test_compile_warnings(self):
# Each invocation of compile() emits compiler warnings, even if they
# have the same message and line number.
@@ -1691,7 +1681,6 @@ class TestSpecifics(unittest.TestCase):
self.assertEqual([wm.lineno for wm in caught], [3, 5] * 2)
@unittest.expectedFailure # TODO: RUSTPYTHON; + [5, 9]
def test_compile_warning_in_finally(self):
# Ensure that warnings inside finally blocks are
# only emitted once despite the block being
@@ -1742,7 +1731,6 @@ class TestSpecifics(unittest.TestCase):
self.assertEqual(wm.category, SyntaxWarning)
self.assertIn("\"is\" with 'int' literal", str(wm.message))
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.subTests('src', [
textwrap.dedent("""
def f():
-1
View File
@@ -231,7 +231,6 @@ class ExceptionTests(unittest.TestCase):
line = line.removeprefix('\ufeff')
self.assertIn(line, cm.exception.text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_error_offset_continuation_characters(self):
check = self.check
check('"\\\n"(1 for c in I,\\\n\\', 2, 2)
-5
View File
@@ -701,7 +701,6 @@ x = (
["f'{ {{}} }'", # dict in a set
])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_time_concat(self):
x = 'def'
self.assertEqual('abc' f'## {x}ghi', 'abc## defghi')
@@ -816,7 +815,6 @@ x = (
s = "f'{1}' 'x' 'y'" * 1024
self.assertEqual(eval(s), '1xy' * 1024)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_format_specifier_expressions(self):
width = 10
precision = 4
@@ -947,7 +945,6 @@ x = (
["f'{3)+(4}'",
])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_newlines_before_syntax_error(self):
self.assertAllRaise(SyntaxError,
"f-string: expecting a valid expression after '{'",
@@ -1031,7 +1028,6 @@ x = (
r"'\N{GREEK CAPITAL LETTER DELTA'",
])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_backslashes_in_expression_part(self):
self.assertEqual(f"{(
1 +
@@ -1732,7 +1728,6 @@ x = (
with self.assertRaisesRegex(ValueError, error_msg):
f'{1:_,}'
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (?, line 1)"
def test_syntax_error_for_starred_expressions(self):
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
compile("f'{*a}'", "?", "exec")
-5
View File
@@ -81,7 +81,6 @@ class FutureTest(unittest.TestCase):
):
from test.test_future_stmt import test_future_multiple_features # noqa: F401
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 24
def test_unknown_future_flag(self):
code = """
from __future__ import nested_scopes
@@ -135,14 +134,12 @@ class FutureTest(unittest.TestCase):
"""
self.assertSyntaxError(code, offset=54)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 24
def test_future_import_star(self):
code = """
from __future__ import *
"""
self.assertSyntaxError(code, message='future feature * is not defined', offset=24)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_import_braces(self):
code = """
from __future__ import braces
@@ -188,7 +185,6 @@ class FutureTest(unittest.TestCase):
out = kill_python(p)
self.assertNotIn(b'SyntaxError: invalid syntax', out)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_dotted_import(self):
with self.assertRaises(ImportError):
exec("from .__future__ import spam")
@@ -480,7 +476,6 @@ class AnnotationsFutureTestCase(unittest.TestCase):
self.assertEqual(foo.__code__.co_cellvars, ())
self.assertEqual(foo().__code__.co_freevars, ())
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_annotations_forbidden(self):
with self.assertRaises(SyntaxError):
self._exec_future("test: (yield)")
+1 -1
View File
@@ -159,7 +159,7 @@ Verify that syntax error's are raised for genexps used as lvalues
...
SyntaxError: cannot assign to generator expression
>>> (y for y in (1,2)) += 10 # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> (y for y in (1,2)) += 10
Traceback (most recent call last):
...
SyntaxError: 'generator expression' is an illegal expression for augmented assignment
-4
View File
@@ -28,7 +28,6 @@ class GlobalTests(unittest.TestCase):
### Syntax error cases as covered in Python/symtable.c
######################################################
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_param(self):
prog_text = """\
def fn(name_param):
@@ -36,7 +35,6 @@ def fn(name_param):
"""
check_syntax_error(self, prog_text, lineno=2, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_after_assign(self):
prog_text = """\
def fn():
@@ -45,7 +43,6 @@ def fn():
"""
check_syntax_error(self, prog_text, lineno=3, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_after_use(self):
prog_text = """\
def fn():
@@ -54,7 +51,6 @@ def fn():
"""
check_syntax_error(self, prog_text, lineno=3, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_annot(self):
prog_text_3 = """\
def fn():
-11
View File
@@ -114,7 +114,6 @@ class TokenTests(unittest.TestCase):
# Sanity check: no literal begins with an underscore
self.assertRaises(NameError, eval, "_0")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bad_numerical_literals(self):
check = self.check_syntax_error
check("0b12", "invalid digit '2' in binary literal")
@@ -137,7 +136,6 @@ class TokenTests(unittest.TestCase):
check("1e2_", "invalid decimal literal")
check("1e+", "invalid decimal literal")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_end_of_numerical_literals(self):
def check(test, error=False):
with self.subTest(expr=test):
@@ -251,7 +249,6 @@ the \'lazy\' dog.\n\
compile(s, "<test>", "exec")
self.assertIn("was never closed", str(cm.exception))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
@skip_wasi_stack_overflow()
def test_max_level(self):
# Macro defined in Parser/lexer/state.h
@@ -298,7 +295,6 @@ class GrammarTests(unittest.TestCase):
my_lst[one()-1]: int = 5
self.assertEqual(my_lst, [5])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_var_annot_syntax_errors(self):
# parser pass
check_syntax_error(self, "def f: int")
@@ -751,7 +747,6 @@ class GrammarTests(unittest.TestCase):
# Check the heuristic for print & exec covers significant cases
# As well as placing some limits on false positives
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_former_statements_refer_to_builtins(self):
keywords = "print", "exec"
# Cases where we want the custom error
@@ -1165,7 +1160,6 @@ class GrammarTests(unittest.TestCase):
""",
True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_yield(self):
# Allowed as standalone statement
def g(): yield 1
@@ -1205,7 +1199,6 @@ class GrammarTests(unittest.TestCase):
# Check annotation refleak on SyntaxError
check_syntax_error(self, "def g(a:(yield)): pass")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_yield_in_comprehensions(self):
# Check yield in comprehensions
def g(): [x for x in [(yield 1)]]
@@ -1302,7 +1295,6 @@ class GrammarTests(unittest.TestCase):
else:
self.fail("AssertionError not raised by 'assert False'")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_assert_syntax_warnings(self):
# Ensure that we warn users if they provide a non-zero length tuple as
# the assertion test.
@@ -1317,7 +1309,6 @@ class GrammarTests(unittest.TestCase):
compile('assert x, "msg"', '<testcase>', 'exec')
compile('assert False, "msg"', '<testcase>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_assert_warning_promotes_to_syntax_error(self):
# If SyntaxWarning is configured to be an error, it actually raises a
# SyntaxError.
@@ -1496,7 +1487,6 @@ class GrammarTests(unittest.TestCase):
if 1 not in (): pass
if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in x is x is not x: pass
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_comparison_is_literal(self):
def check(test, msg):
self.check_syntax_warning(test, msg)
@@ -1526,7 +1516,6 @@ class GrammarTests(unittest.TestCase):
compile('True is x', '<testcase>', 'exec')
compile('... is x', '<testcase>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_warn_missed_comma(self):
def check(test):
self.check_syntax_warning(test, msg)
-1
View File
@@ -219,7 +219,6 @@ class ListComprehensionTest(unittest.TestCase):
"""
self._check_in_scopes(code, raises=NameError)
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: compiler_make_closure: cannot find '__conditional_annotations__' in parent vars
def test_references___conditional_annotations___nested(self):
code = """
class i: [lambda: __conditional_annotations__ for x in y]
-5
View File
@@ -4,35 +4,30 @@ GLOBAL_VAR = None
class NamedExpressionInvalidTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_01(self):
code = """x := 0"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_02(self):
code = """x = y := 0"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_03(self):
code = """y := f(x)"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_04(self):
code = """y0 = y1 := f(x)"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_06(self):
code = """((a, b) := (1, 2))"""
-14
View File
@@ -82,7 +82,6 @@ class TestInheritance(unittest.TestCase):
self.assertEqual(self.check_mapping_then_sequence(S3()), "seq")
self.assertEqual(self.check_mapping_then_sequence(S4()), "seq")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_late_registration_mapping(self):
class Parent:
pass
@@ -106,7 +105,6 @@ class TestInheritance(unittest.TestCase):
self.assertEqual(self.check_mapping_then_sequence(ChildPost()), "map")
self.assertEqual(self.check_mapping_then_sequence(GrandchildPost()), "map")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_late_registration_sequence(self):
class Parent:
pass
@@ -2246,7 +2244,6 @@ class TestPatma(unittest.TestCase):
self.assertEqual(f(None), {})
self.assertEqual(f((1, 2)), {})
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patma_210(self):
def f(w):
match w:
@@ -2955,7 +2952,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_invalid_syntax_3(self):
self.assert_syntax_error("""
match ...:
@@ -3075,7 +3071,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patterns_may_only_match_literals_and_attribute_lookups_0(self):
self.assert_syntax_error("""
match ...:
@@ -3083,7 +3078,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patterns_may_only_match_literals_and_attribute_lookups_1(self):
self.assert_syntax_error("""
match ...:
@@ -3126,7 +3120,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_real_number_wrong_ops(self):
for op in ["*", "/", "@", "**", "%", "//"]:
with self.subTest(op=op):
@@ -3202,7 +3195,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case0(self):
self.assert_syntax_error("""
match ...:
@@ -3210,7 +3202,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case1(self):
self.assert_syntax_error("""
match ...:
@@ -3225,8 +3216,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass
""")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case3(self):
self.assert_syntax_error("""
match ...:
@@ -3258,7 +3247,6 @@ class TestTypeErrors(unittest.TestCase):
self.assertEqual(x, range(10))
self.assertIs(y, None)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_got_multiple_subpatterns_for_attribute_0(self):
class Class:
__match_args__ = ("a", "a")
@@ -3273,7 +3261,6 @@ class TestTypeErrors(unittest.TestCase):
self.assertIs(y, None)
self.assertIs(z, None)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_got_multiple_subpatterns_for_attribute_1(self):
class Class:
__match_args__ = ("a",)
@@ -3379,7 +3366,6 @@ class TestTypeErrors(unittest.TestCase):
class TestValueErrors(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_checks_duplicate_key_1(self):
class Keys:
KEY = "a"
+3 -3
View File
@@ -2165,7 +2165,7 @@ if not SKIP_CORO_TESTS:
>>> def test_function():
... asyncio.run(main(), loop_factory=asyncio.EventLoop)
>>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE
>>> with PdbTestInput([ # doctest: +ELLIPSIS
... 'x = await task',
... 'p x',
... 'x = await test()',
@@ -2280,7 +2280,7 @@ if not SKIP_CORO_TESTS:
>>> def test_function():
... asyncio.run(main(), loop_factory=asyncio.EventLoop)
>>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> with PdbTestInput([
... 'p var.get()',
... 'print(await get_var())',
... 'print(await asyncio.create_task(set_var(100)))',
@@ -2768,7 +2768,7 @@ def test_pdb_multiline_statement():
>>> def test_function():
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace()
>>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE
>>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE
... 'def f(x):',
... ' return x * 2',
... '',
-1
View File
@@ -157,7 +157,6 @@ class TestTranforms(BytecodeTestCase):
self.assertNotInBytecode(code, 'UNPACK_SEQUENCE')
self.check_lnotab(code)
@unittest.expectedFailure # TODO: RUSTPYTHON; LOAD_CONST count mismatch in long-tuple branch
def test_constant_folding_tuples_of_constants(self):
for line, elem in (
('a = 1,2,3', (1, 2, 3)),
+1 -1
View File
@@ -312,7 +312,7 @@ is *not* valid syntax.)
>>> f4.__annotations__
{'args': StarredB, 'arg1': <class 'int'>}
>>> def f5(*args: *b = (1,)): pass # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> def f5(*args: *b = (1,)): pass
Traceback (most recent call last):
...
SyntaxError: invalid syntax
-1
View File
@@ -932,7 +932,6 @@ class PydocDocTest(unittest.TestCase):
synopsis = pydoc.synopsis(TESTFN, {})
self.assertEqual(synopsis, 'line 1: h\xe9')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_source_synopsis(self):
def check(source, expected, encoding=None):
if isinstance(source, str):
-7
View File
@@ -117,7 +117,6 @@ class TestSimpleInteract(unittest.TestCase):
SyntaxError: duplicate argument 'x' in function definition"""
self.assertIn(r, f.getvalue())
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_runsource_shows_syntax_error_for_failed_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!'"
@@ -133,7 +132,6 @@ SyntaxError: duplicate argument 'x' in function definition"""
console.runsource(source)
mock_showsyntaxerror.assert_called_once()
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_runsource_survives_null_bytes(self):
console = InteractiveColoredConsole()
source = "\x00\n"
@@ -155,7 +153,6 @@ SyntaxError: duplicate argument 'x' in function definition"""
self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_annotations(self):
console = InteractiveColoredConsole()
source = dedent("""\
@@ -210,7 +207,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiline_single_block(self):
namespace = {}
code = dedent("""\
@@ -227,7 +223,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiple_statements(self):
namespace = {}
code = dedent("""\
@@ -237,7 +232,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiple_blocks(self):
namespace = {}
code = dedent("""\
@@ -285,7 +279,6 @@ class TestMoreLines(unittest.TestCase):
class TestWarnings(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_pep_765_warning(self):
"""
Test that a SyntaxWarning emitted from the
-10
View File
@@ -466,7 +466,6 @@ class TestPyReplAutoindent(TestCase):
reader = ReadlineAlikeReader(console=console, config=config)
return reader
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_default(self):
# fmt: off
input_code = (
@@ -486,7 +485,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_continuation(self):
# auto indenting according to previous user indentation
# fmt: off
@@ -514,7 +512,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_prev_block(self):
# auto indenting according to indentation in different block
# fmt: off
@@ -546,7 +543,6 @@ class TestPyReplAutoindent(TestCase):
output2 = multiline_input(reader)
self.assertEqual(output2, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_multiline(self):
# fmt: off
events = itertools.chain(
@@ -586,7 +582,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_with_comment(self):
# fmt: off
events = code_to_events(
@@ -605,7 +600,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_with_multicomment(self):
# fmt: off
events = code_to_events(
@@ -680,7 +674,6 @@ class TestPyReplOutput(ScreenEqualMixin, TestCase):
wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
self.assertIs(type(wrapper.get_line_buffer()), str)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiline_edit(self):
events = itertools.chain(
code_to_events("def f():\n...\n\n"),
@@ -744,7 +737,6 @@ class TestPyReplOutput(ScreenEqualMixin, TestCase):
self.assertEqual(output, "1+1")
self.assert_screen_equal(reader, "1+1", clean=True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_history_with_multiline_entries(self):
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
events = list(itertools.chain(
@@ -1426,7 +1418,6 @@ class TestPasteEvent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_paste_mid_newlines_not_in_paste_mode(self):
# fmt: off
code = (
@@ -1448,7 +1439,6 @@ class TestPasteEvent(TestCase):
output = multiline_input(reader)
self.assertEqual(output, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_paste_not_in_paste_mode(self):
# fmt: off
input_code = (
-1
View File
@@ -180,7 +180,6 @@ class TestReader(ScreenEqualMixin, TestCase):
reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, "")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Lists differ
def test_newline_within_block_trailing_whitespace(self):
# fmt: off
code = (
-1
View File
@@ -437,7 +437,6 @@ class TestAsyncioREPL(unittest.TestCase):
expected = "toplevel contextvar test: ok"
self.assertIn(expected, output, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 != 0
def test_toplevel_contextvars_async(self):
user_input = dedent("""\
from contextvars import ContextVar
-1
View File
@@ -198,7 +198,6 @@ class SymtableTest(unittest.TestCase):
T = find_block(GenericMine, "T")
U = find_block(GenericMine, "U")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <SymbolTableType.ANNOTATION: 'annotation'> != 'type alias'
def test_type(self):
self.assertEqual(self.top.get_type(), "module")
self.assertEqual(self.Mine.get_type(), "class")
+146 -161
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -169,7 +169,6 @@ class ProfileHookTestCase(TestCaseBase):
(1, 'return', g_ident),
])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_exception_propagation(self):
def f(p):
1/0
-15
View File
@@ -252,7 +252,6 @@ class TypeCommentTests(unittest.TestCase):
def classic_parse(self, source):
return ast.parse(source)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment'
def test_funcdef(self):
for tree in self.parse_all(funcdef):
self.assertEqual(tree.body[0].type_comment, "() -> int")
@@ -261,7 +260,6 @@ class TypeCommentTests(unittest.TestCase):
self.assertEqual(tree.body[0].type_comment, None)
self.assertEqual(tree.body[1].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_asyncdef(self):
for tree in self.parse_all(asyncdef, minver=5):
self.assertEqual(tree.body[0].type_comment, "() -> int")
@@ -274,12 +272,10 @@ class TypeCommentTests(unittest.TestCase):
with self.assertRaises(SyntaxError):
self.classic_parse(asyncvar)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_asynccomp(self):
for tree in self.parse_all(asynccomp, minver=6):
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_matmul(self):
for tree in self.parse_all(matmul, minver=5):
pass
@@ -288,37 +284,31 @@ class TypeCommentTests(unittest.TestCase):
for tree in self.parse_all(fstring):
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_underscorednumber(self):
for tree in self.parse_all(underscorednumber, minver=6):
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_redundantdef(self):
for tree in self.parse_all(redundantdef, maxver=0,
expected_regex="^Cannot have two type comments on def"):
pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment'
def test_nonasciidef(self):
for tree in self.parse_all(nonasciidef):
self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt")
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'For' object has no attribute 'type_comment'
def test_forstmt(self):
for tree in self.parse_all(forstmt):
self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(forstmt)
self.assertEqual(tree.body[0].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'With' object has no attribute 'type_comment'
def test_withstmt(self):
for tree in self.parse_all(withstmt):
self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(withstmt)
self.assertEqual(tree.body[0].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'With' object has no attribute 'type_comment'
def test_parenthesized_withstmt(self):
for tree in self.parse_all(parenthesized_withstmt):
self.assertEqual(tree.body[0].type_comment, "int")
@@ -327,14 +317,12 @@ class TypeCommentTests(unittest.TestCase):
self.assertEqual(tree.body[0].type_comment, None)
self.assertEqual(tree.body[1].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'int'
def test_vardecl(self):
for tree in self.parse_all(vardecl):
self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(vardecl)
self.assertEqual(tree.body[0].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; + (11, ' whatever')]
def test_ignores(self):
for tree in self.parse_all(ignores):
self.assertEqual(
@@ -350,7 +338,6 @@ class TypeCommentTests(unittest.TestCase):
tree = self.classic_parse(ignores)
self.assertEqual(tree.type_ignores, [])
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_longargs(self):
for tree in self.parse_all(longargs, minver=8):
for t in tree.body:
@@ -381,7 +368,6 @@ class TypeCommentTests(unittest.TestCase):
self.assertIsNone(arg.type_comment, "%s(%s:%r)" %
(t.name, arg.arg, arg.type_comment))
@unittest.expectedFailure # TODO: RUSTPYTHON; Tests for inappropriately-placed type comments.
def test_inappropriate_type_comments(self):
"""Tests for inappropriately-placed type comments.
@@ -416,7 +402,6 @@ class TypeCommentTests(unittest.TestCase):
_testcapi.Py_CompileStringExFlags(
b"def a(f=8, #type: \x80\n\x80", "<test>", 256, flags)
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: mode must be "exec", "eval", "ipython", or "single"
def test_func_type_input(self):
def parse_func_type_input(source):
-3
View File
@@ -683,7 +683,6 @@ class TypeParamsClassScopeTest(unittest.TestCase):
self.assertIs(X.foo.__type_params__[0].__bound__, float)
self.assertIs(X.Alias.__value__, float)
@unittest.expectedFailure # TODO: RUSTPYTHON; + global
def test_binding_uses_global(self):
ns = run_code("""
x = "global"
@@ -1076,7 +1075,6 @@ class TypeParamsTypeVarTest(unittest.TestCase):
class TypeParamsTypeVarTupleTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot use bound with TypeVarTuple" does not match "invalid syntax (<test string>, line 1)"
def test_typevartuple_01(self):
code = """def func1[*A: str](): pass"""
check_syntax_error(self, code, "cannot use bound with TypeVarTuple")
@@ -1100,7 +1098,6 @@ class TypeParamsTypeVarTupleTest(unittest.TestCase):
class TypeParamsTypeVarParamSpecTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot use bound with ParamSpec" does not match "invalid syntax (<test string>, line 1)"
def test_paramspec_01(self):
code = """def func1[**A: str](): pass"""
check_syntax_error(self, code, "cannot use bound with ParamSpec")
+4 -4
View File
@@ -168,7 +168,7 @@ List comprehension element unpacking
...
SyntaxError: iterable unpacking cannot be used in comprehension
>>> {**{} for a in [1]} # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE
>>> {**{} for a in [1]}
Traceback (most recent call last):
...
SyntaxError: dict unpacking cannot be used in dict comprehension
@@ -356,7 +356,7 @@ Now some general starred expressions (all fail).
...
SyntaxError: can't use starred expression here
>>> (*x),y = 1, 2 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE
>>> (*x),y = 1, 2
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
@@ -366,12 +366,12 @@ Now some general starred expressions (all fail).
...
SyntaxError: cannot use starred expression here
>>> z,(*x),y = 1, 2, 4 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE
>>> z,(*x),y = 1, 2, 4
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
>>> z,(*x) = 1, 2 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE
>>> z,(*x) = 1, 2
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
+7 -27
View File
@@ -1,17 +1,13 @@
use crate::pystate::with_vm;
use crate::unicodeobject::decode_fsdefault_and_size;
use core::ffi::{CStr, c_char, c_int};
use core::ptr::NonNull;
use rustpython_vm::builtins::{PyCode, PyDict};
use rustpython_vm::compiler::Mode;
use rustpython_vm::function::ArgMapping;
use rustpython_vm::scope::Scope;
use rustpython_vm::version;
use rustpython_vm::{AsObject, PyObject, TryFromObject};
const PY_SINGLE_INPUT: c_int = 256;
const PY_FILE_INPUT: c_int = 257;
const PY_EVAL_INPUT: c_int = 258;
const PY_FUNC_TYPE_INPUT: c_int = 345;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Py_CompileString(
code: *const c_char,
@@ -19,27 +15,11 @@ pub unsafe extern "C" fn Py_CompileString(
start: c_int,
) -> *mut PyObject {
with_vm(|vm| {
let code = unsafe { CStr::from_ptr(code) }.to_str().map_err(|_| {
vm.new_system_error("Py_CompileString called with non UTF-8 code string")
})?;
let filename = unsafe { CStr::from_ptr(filename) }
.to_str()
.map_err(|_| vm.new_system_error("Py_CompileString called with non UTF-8 filename"))?;
let mode = match start {
PY_SINGLE_INPUT => Mode::Single,
PY_FILE_INPUT => Mode::Exec,
PY_EVAL_INPUT => Mode::Eval,
PY_FUNC_TYPE_INPUT => Mode::BlockExpr,
_ => {
return Err(
vm.new_system_error("Invalid start argument passed to Py_CompileString")
);
}
};
vm.compile(code, mode, filename)
.map_err(|err| vm.new_syntax_error(&err, Some(code)))
let code = unsafe { CStr::from_ptr(code) }.to_bytes();
let filename_size = unsafe { CStr::from_ptr(filename) }.to_bytes().len();
let filename = decode_fsdefault_and_size(vm, filename, filename_size)?;
let filename = filename.to_string_lossy();
vm.compile_string_object_with_flags(code, &filename, start, 0, version::MINOR as c_int, -1)
})
}
+35 -19
View File
@@ -4,8 +4,8 @@ use core::ffi::{CStr, c_char, c_int};
use core::ptr::NonNull;
use core::slice;
use core::str;
use rustpython_vm::PyObjectRef;
use rustpython_vm::builtins::PyStr;
use rustpython_vm::builtins::{PyStr, PyStrRef};
use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine};
define_py_check!(fn PyUnicode_Check, types.str_type);
define_py_check!(exact fn PyUnicode_CheckExact, types.str_type);
@@ -113,26 +113,42 @@ pub unsafe extern "C" fn PyUnicode_DecodeFSDefaultAndSize(
.try_into()
.map_err(|_| vm.new_system_error("size must be non-negative"))?;
let bytes = if s.is_null() {
if size != 0 {
return Err(vm.new_system_error(
"PyUnicode_DecodeFSDefaultAndSize called with null data and non-zero size",
));
}
&[][..]
} else {
unsafe { slice::from_raw_parts(s.cast::<u8>(), size) }
};
vm.state.codec_registry.decode_text(
vm.ctx.new_bytes(bytes.to_vec()).into(),
vm.fs_encoding().as_str(),
Some(vm.fs_encode_errors().to_owned()),
vm,
)
decode_fsdefault_and_size(vm, s, size)
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyUnicode_DecodeFSDefault(s: *const c_char) -> *mut PyObject {
with_vm(|vm| {
let size = unsafe { CStr::from_ptr(s) }.to_bytes().len();
decode_fsdefault_and_size(vm, s, size)
})
}
pub(crate) fn decode_fsdefault_and_size(
vm: &VirtualMachine,
s: *const c_char,
size: usize,
) -> PyResult<PyStrRef> {
let bytes = if s.is_null() {
if size != 0 {
return Err(vm.new_system_error(
"PyUnicode_DecodeFSDefaultAndSize called with null data and non-zero size",
));
}
&[][..]
} else {
unsafe { slice::from_raw_parts(s.cast::<u8>(), size) }
};
vm.state.codec_registry.decode_text(
vm.ctx.new_bytes(bytes.to_vec()).into(),
vm.fs_encoding().as_str(),
Some(vm.fs_encode_errors().to_owned()),
vm,
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn PyUnicode_EncodeFSDefault(unicode: *mut PyObject) -> *mut PyObject {
with_vm(|vm| {
+5723 -1831
View File
File diff suppressed because it is too large. Load diff
+31 -29
View File
@@ -3,21 +3,6 @@ use core::fmt::Display;
use rustpython_compiler_core::SourceLocation;
use thiserror::Error;
#[derive(Clone, Copy, Debug)]
pub enum PatternUnreachableReason {
NameCapture,
Wildcard,
}
impl Display for PatternUnreachableReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NameCapture => write!(f, "name capture"),
Self::Wildcard => write!(f, "wildcard"),
}
}
}
// pub type CodegenError = rustpython_parser_core::source_code::LocatedError<CodegenErrorType>;
#[derive(Error, Debug)]
@@ -70,6 +55,8 @@ pub enum CodegenErrorType {
SyntaxError(String),
/// Multiple `*` detected
MultipleStarArgs,
MultipleStarredExpressionsInSequencePattern,
MultipleStarredNamesInSequencePattern,
/// Misplaced `*` expression
InvalidStarExpr,
/// Break statement outside of loop.
@@ -87,14 +74,17 @@ pub enum CodegenErrorType {
AsyncReturnValue,
InvalidFuturePlacement,
InvalidFutureFeature(String),
FunctionImportStar,
InvalidFutureBraces,
RecursionError,
TooManyStarUnpack,
TooManyExpressionsInStarUnpackingSequencePattern,
EmptyWithItems,
EmptyWithBody,
ForbiddenName,
DuplicateStore(String),
UnreachablePattern(PatternUnreachableReason),
RepeatedAttributePattern,
UnreachableWildcardPattern,
UnreachableNameCapturePattern(String),
RepeatedAttributePattern(String),
ConflictingNameBindPattern,
/// break/continue/return inside except* block
BreakContinueReturnInExceptStar,
@@ -112,6 +102,12 @@ impl fmt::Display for CodegenErrorType {
Self::MultipleStarArgs => {
write!(f, "multiple starred expressions in assignment")
}
Self::MultipleStarredExpressionsInSequencePattern => {
write!(f, "multiple starred expressions in sequence pattern")
}
Self::MultipleStarredNamesInSequencePattern => {
write!(f, "multiple starred names in sequence pattern")
}
Self::InvalidStarExpr => write!(f, "can't use starred expression here"),
Self::InvalidBreak => write!(f, "'break' outside loop"),
Self::InvalidContinue => write!(f, "'continue' not properly in loop"),
@@ -128,9 +124,7 @@ impl fmt::Display for CodegenErrorType {
)
}
Self::AsyncYieldFrom => write!(f, "'yield from' inside async function"),
Self::AsyncReturnValue => {
write!(f, "'return' with value inside async generator")
}
Self::AsyncReturnValue => write!(f, "'return' with value in async generator"),
Self::InvalidFuturePlacement => write!(
f,
"from __future__ imports must occur at the beginning of the file"
@@ -138,12 +132,16 @@ impl fmt::Display for CodegenErrorType {
Self::InvalidFutureFeature(feat) => {
write!(f, "future feature {feat} is not defined")
}
Self::FunctionImportStar => {
write!(f, "import * only allowed at module level")
Self::InvalidFutureBraces => write!(f, "not a chance"),
Self::RecursionError => {
write!(f, "maximum recursion depth exceeded during compilation")
}
Self::TooManyStarUnpack => {
write!(f, "too many expressions in star-unpacking assignment")
}
Self::TooManyExpressionsInStarUnpackingSequencePattern => {
write!(f, "too many expressions in star-unpacking sequence pattern")
}
Self::EmptyWithItems => {
write!(f, "empty items on With")
}
@@ -153,14 +151,18 @@ impl fmt::Display for CodegenErrorType {
Self::ForbiddenName => {
write!(f, "forbidden attribute name")
}
Self::DuplicateStore(s) => {
write!(f, "duplicate store {s}")
Self::DuplicateStore(s) => write!(f, "multiple assignments to name '{s}' in pattern"),
Self::UnreachableWildcardPattern => {
write!(f, "wildcard makes remaining patterns unreachable")
}
Self::UnreachablePattern(reason) => {
write!(f, "{reason} makes remaining patterns unreachable")
Self::UnreachableNameCapturePattern(name) => {
write!(
f,
"name capture '{name}' makes remaining patterns unreachable"
)
}
Self::RepeatedAttributePattern => {
write!(f, "attribute name repeated in class pattern")
Self::RepeatedAttributePattern(name) => {
write!(f, "attribute name repeated in class pattern: {name}")
}
Self::ConflictingNameBindPattern => {
write!(f, "alternative patterns bind different names")
+335 -103
View File
@@ -78,15 +78,108 @@ impl ConstantPool {
}
}
pub fn insert_full(&mut self, constant: ConstantData) -> (usize, bool) {
// CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct
// because Python-level NaN keys do not compare equal.
if !Self::constant_contains_nan(&constant)
&& let Some(idx) = self
.constants
fn frozenset_key_contains(elements: &[ConstantData], needle: &ConstantData) -> bool {
if Self::constant_contains_nan(needle) {
return false;
}
elements.iter().any(|element| {
!Self::constant_contains_nan(element) && Self::constant_key_eq(element, needle)
})
}
fn frozenset_key_eq(left: &[ConstantData], right: &[ConstantData]) -> bool {
left.iter()
.all(|element| Self::frozenset_key_contains(right, element))
&& right
.iter()
.position(|existing| existing == &constant)
{
.all(|element| Self::frozenset_key_contains(left, element))
}
fn constant_key_eq(left: &ConstantData, right: &ConstantData) -> bool {
match (left, right) {
(ConstantData::Tuple { elements: left }, ConstantData::Tuple { elements: right }) => {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(left, right)| Self::constant_key_eq(left, right))
}
(
ConstantData::Frozenset { elements: left },
ConstantData::Frozenset { elements: right },
) => Self::frozenset_key_eq(left, right),
(ConstantData::Slice { elements: left }, ConstantData::Slice { elements: right }) => {
left.iter()
.zip(right.iter())
.all(|(left, right)| Self::constant_key_eq(left, right))
}
_ => left == right,
}
}
fn canonicalize_constant_key(constant: ConstantData) -> crate::InternalResult<ConstantData> {
match constant {
ConstantData::Tuple { elements } => {
let mut canonical = Vec::new();
canonical
.try_reserve_exact(elements.len())
.map_err(|_| InternalError::MalformedControlFlowGraph)?;
for element in elements {
canonical.push(Self::canonicalize_constant_key(element)?);
}
Ok(ConstantData::Tuple {
elements: canonical,
})
}
ConstantData::Slice { elements } => {
let [start, stop, step] = *elements;
Ok(ConstantData::Slice {
elements: Box::new([
Self::canonicalize_constant_key(start)?,
Self::canonicalize_constant_key(stop)?,
Self::canonicalize_constant_key(step)?,
]),
})
}
ConstantData::Frozenset { elements } => {
let mut canonical = Vec::new();
canonical
.try_reserve_exact(elements.len())
.map_err(|_| InternalError::MalformedControlFlowGraph)?;
for element in elements {
let element = Self::canonicalize_constant_key(element)?;
if !Self::frozenset_key_contains(&canonical, &element) {
canonical.push(element);
}
}
Ok(ConstantData::Frozenset {
elements: canonical,
})
}
other => Ok(other),
}
}
fn canonicalize_constant_key_infallible(constant: ConstantData) -> ConstantData {
Self::canonicalize_constant_key(constant)
.expect("constant key canonicalization only fails on allocation error")
}
/// Index of an already-stored constant equal to `constant`, if any.
/// _PyCode_ConstantKey() keeps NaN-bearing constants distinct because
/// Python-level NaN keys do not compare equal.
fn find_existing(&self, constant: &ConstantData) -> Option<usize> {
if Self::constant_contains_nan(constant) {
return None;
}
self.constants
.iter()
.position(|existing| Self::constant_key_eq(existing, constant))
}
pub fn insert_full(&mut self, constant: ConstantData) -> (usize, bool) {
let constant = Self::canonicalize_constant_key_infallible(constant);
if let Some(idx) = self.find_existing(&constant) {
return (idx, false);
}
let idx = self.constants.len();
@@ -95,14 +188,8 @@ impl ConstantPool {
}
fn try_insert_full(&mut self, constant: ConstantData) -> crate::InternalResult<(usize, bool)> {
// CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct
// because Python-level NaN keys do not compare equal.
if !Self::constant_contains_nan(&constant)
&& let Some(idx) = self
.constants
.iter()
.position(|existing| existing == &constant)
{
let constant = Self::canonicalize_constant_key(constant)?;
if let Some(idx) = self.find_existing(&constant) {
return Ok((idx, false));
}
self.constants
@@ -1485,6 +1572,7 @@ impl Blocks {
| PseudoInstruction::JumpIfTrue { .. }),
) => {
let opcode = pseudo.into();
let opcode_is_false = matches!(pseudo, PseudoInstruction::JumpIfFalse { .. });
match target.instr.pseudo().map(Into::into) {
Some(PseudoOpcode::Jump)
if self.jump_thread(block_idx, i, &target, opcode)? =>
@@ -1492,22 +1580,25 @@ impl Blocks {
continue;
}
Some(PseudoOpcode::JumpIfFalse)
if matches!(
opcode,
AnyInstruction::Pseudo(PseudoInstruction::JumpIfFalse { .. })
) && self.jump_thread(block_idx, i, &target, opcode)? =>
if opcode_is_false
&& self.jump_thread(block_idx, i, &target, opcode)? =>
{
continue;
}
Some(PseudoOpcode::JumpIfTrue)
if matches!(
opcode,
AnyInstruction::Pseudo(PseudoInstruction::JumpIfTrue { .. })
) && self.jump_thread(block_idx, i, &target, opcode)? =>
if !opcode_is_false
&& self.jump_thread(block_idx, i, &target, opcode)? =>
{
continue;
}
Some(PseudoOpcode::JumpIfFalse | PseudoOpcode::JumpIfTrue) => {
Some(PseudoOpcode::JumpIfTrue) if opcode_is_false => {
let next = self[inst.target].next;
debug_assert!(next != BlockIdx::NULL);
debug_assert!(next != inst.target);
self[block_idx].instructions[i].target = next;
continue;
}
Some(PseudoOpcode::JumpIfFalse) if !opcode_is_false => {
let next = self[inst.target].next;
debug_assert!(next != BlockIdx::NULL);
debug_assert!(next != inst.target);
@@ -2335,10 +2426,10 @@ impl Blocks {
Ok(())
}
/// flowgraph.c mark_cold (two-pass to match CPython).
/// flowgraph.c mark_cold (two-pass).
///
/// Phase 1 (mark_warm): propagate "warm" from entry via fall-through and
/// jump targets. CPython asserts while visiting warm blocks that they are not
/// jump targets. The pass asserts while visiting warm blocks that they are not
/// exception handlers.
///
/// Phase 2 (mark_cold): propagate "cold" from except_handler blocks via
@@ -2349,7 +2440,7 @@ impl Blocks {
/// empty unreachable placeholders left by remove_unreachable; they stay in
/// their original chain position (e.g. between entry and the post-try
/// continuation for a nested try/except whose inner_end was emptied by
/// optimize_cfg). This matches CPython's behavior and is necessary for
/// optimize_cfg). This is necessary for
/// optimize_load_fast to terminate fall-through at those placeholders.
/// flowgraph.c mark_warm
fn mark_warm(&mut self) -> crate::InternalResult<()> {
@@ -2981,42 +3072,12 @@ impl Blocks {
}
}
impl From<Vec<Block>> for Blocks {
fn from(value: Vec<Block>) -> Self {
Self(value)
}
}
impl From<Box<[Block]>> for Blocks {
fn from(value: Box<[Block]>) -> Self {
Self(value.into())
}
}
impl From<&[Block]> for Blocks {
fn from(value: &[Block]) -> Self {
Self(value.to_vec())
}
}
impl From<&mut [Block]> for Blocks {
fn from(value: &mut [Block]) -> Self {
Self(value.to_vec())
}
}
impl<const N: usize> From<[Block; N]> for Blocks {
fn from(value: [Block; N]) -> Self {
Self(value.into())
}
}
impl<const N: usize> From<&[Block; N]> for Blocks {
fn from(value: &[Block; N]) -> Self {
Self(value.to_vec())
}
}
impl Deref for Blocks {
type Target = [Block];
@@ -3060,7 +3121,7 @@ impl IndexMut<BlockIdx> for Blocks {
}
pub(crate) const START_DEPTH_UNSET: i32 = i32::MIN;
const CO_MAXBLOCKS: usize = 20;
const CO_MAXBLOCKS: usize = 21;
/// flowgraph.c struct _PyCfgExceptStack
#[derive(Clone, Debug)]
@@ -3099,7 +3160,7 @@ impl CfgTraversalStack {
#[derive(Clone, Debug)]
pub(crate) struct InstructionSequenceLabelMap {
block_labels: Vec<InstructionSequenceLabel>,
/// Codegen-side shadow of CPython's instruction-sequence label map.
/// Codegen-side shadow of the instruction-sequence label map.
///
/// `_PyInstructionSequence_UseLabel()` can map multiple labels to the same
/// instruction offset before `_PyCfg_FromInstructionSequence()` materializes
@@ -3305,7 +3366,7 @@ pub struct CodeInfo {
// Reference to the symbol table for this scope
pub symbol_table_index: usize,
// CPython compile.c uses PyList_GET_SIZE(u->u_ste->ste_varnames)
// compile.c uses PyList_GET_SIZE(u->u_ste->ste_varnames)
// when calling flowgraph.c _PyCfg_OptimizeCodeUnit().
pub nparams: usize,
@@ -3488,7 +3549,7 @@ impl CodeInfo {
}
fn prepare_cfg_from_codegen(&mut self) -> crate::InternalResult<InstructionSequence> {
// CPython compile.c optimize_and_assemble_code_unit passes
// compile.c optimize_and_assemble_code_unit passes
// u_instr_sequence directly into flowgraph.c _PyCfg_FromInstructionSequence().
self.take_recorded_instr_sequence()
}
@@ -3509,12 +3570,12 @@ fn optimize_code_unit(
optimize_cfg(metadata, blocks, metadata.firstlineno)?;
blocks.remove_unused_consts(&mut metadata.consts)?;
add_checks_for_loads_of_uninitialized_variables(blocks, nlocals, nparams)?;
// CPython inserts superinstructions in _PyCfg_OptimizeCodeUnit, before
// Superinstructions are inserted in _PyCfg_OptimizeCodeUnit, before
// later jump normalization / block reordering can create adjacencies
// that never exist at this stage in flowgraph.c.
blocks.insert_superinstructions()?;
blocks.push_cold_blocks_to_end()?;
// CPython resolves line numbers again after cold-block extraction.
// Line numbers are resolved again after cold-block extraction.
blocks.resolve_line_numbers(metadata.firstlineno)?;
Ok(())
}
@@ -3525,20 +3586,20 @@ fn optimize_cfg(
firstlineno: OneIndexed,
) -> crate::InternalResult<()> {
// flowgraph.c optimize_cfg
// CPython optimize_cfg() starts with check_cfg() and raises
// optimize_cfg() starts with check_cfg() and raises
// SystemError if a jump or scope exit is not the last instruction in
// its block.
blocks.check_cfg()?;
blocks.inline_small_or_no_lineno_blocks()?;
// CPython does not re-run instruction-sequence label-map/CFG conversion
// The instruction-sequence label-map/CFG conversion is not re-run
// after this point. Unreferenced label blocks left by jump inlining
// remain block boundaries and can preserve line-marker NOPs.
blocks.remove_unreachable()?;
// CPython optimize_cfg resolves line numbers before local checks and
// optimize_cfg resolves line numbers before local checks and
// superinstruction insertion, so fusion decisions see propagated
// source locations.
blocks.resolve_line_numbers(firstlineno)?;
// CPython optimize_cfg() runs optimize_load_const() and then
// optimize_cfg() runs optimize_load_const() and then
// optimize_basic_block() after line numbers are resolved.
optimize_load_const(metadata, blocks)?;
let mut block_idx = BlockIdx(0);
@@ -3548,7 +3609,7 @@ fn optimize_cfg(
block_idx = next_block;
}
blocks.remove_redundant_nops_and_pairs()?;
// CPython optimize_cfg() removes newly-unreachable blocks and
// optimize_cfg() removes newly-unreachable blocks and
// redundant NOP/jump chains before _PyCfg_OptimizeCodeUnit() prunes
// unused constants.
blocks.remove_unreachable()?;
@@ -3568,7 +3629,7 @@ fn optimized_cfg_to_instruction_sequence(
let max_stackdepth = blocks.calculate_stackdepth()?;
debug_assert!(!is_generator(flags) || max_stackdepth != 0);
let nlocalsplus = prepare_localsplus(metadata, blocks, flags)?;
// Match CPython order: pseudo ops are lowered after stackdepth and
// Pseudo ops are lowered after stackdepth and
// localsplus preparation, before normalize_jumps.
convert_pseudo_ops(blocks)?;
blocks.normalize_jumps()?;
@@ -3636,6 +3697,9 @@ impl CodeInfo {
kwonlyargcount: kwonlyarg_count,
firstlineno: first_line_number,
} = metadata;
let code_arg_count = posonlyarg_count
.checked_add(arg_count)
.ok_or(InternalError::MalformedControlFlowGraph)?;
resolve_unconditional_jumps(&mut instr_sequence)?;
resolve_jump_offsets(&mut instr_sequence)?;
@@ -3653,7 +3717,7 @@ impl CodeInfo {
Ok(CodeObject {
flags,
posonlyarg_count,
arg_count,
arg_count: code_arg_count,
kwonlyarg_count,
source_path,
first_line_number: Some(first_line_number),
@@ -4109,8 +4173,13 @@ fn const_folding_safe_multiply(left: &ConstantData, right: &ConstantData) -> Opt
const_folding_safe_multiply(right, left)
}
(ConstantData::Tuple { elements }, ConstantData::Integer { value: n }) => {
if elements.is_empty() {
return Some(ConstantData::Tuple {
elements: Vec::new(),
});
}
let n = n.to_usize()?;
if n != 0 && !elements.is_empty() {
if n != 0 {
if n > MAX_COLLECTION_SIZE / elements.len() {
return None;
}
@@ -4265,7 +4334,7 @@ fn eval_const_complex_binop(
BinOp::Add => left + right,
BinOp::Subtract => {
let re = left.re - right.re;
// Preserve CPython's signed-zero behavior for real-zero
// Preserve signed-zero behavior for real-zero
// minus zero-complex expressions such as `0 - 0j`.
let im = if left.re == 0.0
&& left.im == 0.0
@@ -4829,7 +4898,7 @@ fn fold_constant_intrinsic_list_to_tuple(
Ok(false)
}
/// Port of CPython's flowgraph.c optimize_lists_and_sets().
/// Port of flowgraph.c optimize_lists_and_sets().
fn optimize_lists_and_sets(
metadata: &mut CodeUnitMetadata,
block: &mut Block,
@@ -4889,8 +4958,7 @@ fn optimize_lists_and_sets(
if !contains_or_iter {
debug_assert!(i >= 2);
let folded_loc = block.instructions[i].location;
let end_loc = block.instructions[i].end_location;
let folded_loc = instr_location(&block.instructions[i]);
nop_out(block, &operand_indices);
@@ -4901,9 +4969,7 @@ fn optimize_lists_and_sets(
}
.into();
instr_set_op1(&mut block.instructions[i - 2], build_instr, OpArg::new(0));
block.instructions[i - 2].location = folded_loc;
block.instructions[i - 2].end_location = end_loc;
block.instructions[i - 2].lineno_override = None;
instr_set_location(&mut block.instructions[i - 2], folded_loc);
instr_set_op1(
&mut block.instructions[i - 1],
@@ -5152,7 +5218,7 @@ fn basicblock_optimize_load_const(
block: &mut Block,
) -> crate::InternalResult<()> {
let mut i = 0;
let mut effective_opcode = None;
let mut effective_opcode = Instruction::Nop.into();
let mut effective_oparg = OpArg::new(0);
while i < block.instruction_used {
if matches!(
@@ -5166,21 +5232,19 @@ fn basicblock_optimize_load_const(
let curr = block.instructions[i];
let curr_arg = curr.arg;
// Only combine if the source is a real instruction.
let Some(curr_instr) = curr.instr.real() else {
i += 1;
continue;
};
let is_copy_of_load_const = matches!(
(effective_opcode, curr_instr),
(Some(Instruction::LoadConst { .. }), Instruction::Copy { i }) if i.get(curr_arg) == 1
(effective_opcode, curr.instr.real()),
(AnyInstruction::Real(Instruction::LoadConst { .. }), Some(Instruction::Copy { i }))
if i.get(curr_arg) == 1
);
if !is_copy_of_load_const {
effective_opcode = Some(curr_instr);
effective_opcode = curr.instr;
effective_oparg = curr_arg;
}
let Some(const_instr) = effective_opcode else {
debug_assert!(!effective_opcode.is_assembler());
let Some(const_instr @ (Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. })) =
effective_opcode.real()
else {
i += 1;
continue;
};
@@ -5273,7 +5337,7 @@ fn basicblock_optimize_load_const(
Opcode::PopJumpIfNone
}
.into();
i = jump_idx;
i += 1;
continue;
}
}
@@ -6557,21 +6621,19 @@ fn get_max_label(blocks: &Blocks) -> i32 {
}
/// flowgraph.c make_except_stack
#[allow(clippy::unnecessary_wraps)]
fn make_except_stack() -> crate::InternalResult<CfgExceptStack> {
fn make_except_stack() -> CfgExceptStack {
let handlers = [BlockIdx::NULL; CO_MAXBLOCKS + 2];
debug_assert_eq!(handlers[0], BlockIdx::NULL);
Ok(CfgExceptStack { handlers, depth: 0 })
CfgExceptStack { handlers, depth: 0 }
}
/// flowgraph.c copy_except_stack
#[allow(clippy::unnecessary_wraps)]
fn copy_except_stack(stack: &CfgExceptStack) -> crate::InternalResult<CfgExceptStack> {
fn copy_except_stack(stack: &CfgExceptStack) -> CfgExceptStack {
debug_assert!(stack.depth <= CO_MAXBLOCKS + 1);
Ok(CfgExceptStack {
CfgExceptStack {
handlers: stack.handlers,
depth: stack.depth,
})
}
}
/// flowgraph.c except_stack_top
@@ -6623,7 +6685,7 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
todo.push(BlockIdx(0));
blocks[0].visited = true;
blocks[0].except_stack = Some(make_except_stack()?);
blocks[0].except_stack = Some(make_except_stack());
while let Some(block_idx) = todo.pop() {
let bi = block_idx.idx();
@@ -6650,7 +6712,7 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
if !blocks[target].visited {
blocks[target].except_stack = Some(copy_except_stack(
stack.as_ref().expect("active exception stack"),
)?);
));
todo.push(target);
blocks[target].visited = true;
}
@@ -6674,7 +6736,7 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
if bb_has_fallthrough(&blocks[bi]) {
blocks[target].except_stack = Some(copy_except_stack(
stack.as_ref().expect("active exception stack"),
)?);
));
} else {
blocks[target].except_stack = stack.take();
stack_transferred = true;
@@ -6852,6 +6914,58 @@ pub(crate) fn fix_cell_offsets(
#[cfg(test)]
mod tests {
use super::*;
use rustpython_compiler_core::bytecode::Arg;
fn int_const(value: i32) -> ConstantData {
ConstantData::Integer {
value: BigInt::from(value),
}
}
fn nan_const() -> ConstantData {
ConstantData::Float { value: f64::NAN }
}
#[test]
fn constant_pool_frozenset_key_ignores_order_and_duplicates_like_cpython() {
let mut pool = ConstantPool::default();
let (first, inserted) = pool.insert_full(ConstantData::Frozenset {
elements: vec![int_const(1), int_const(2)],
});
assert_eq!(first, 0);
assert!(inserted);
let (second, inserted) = pool.insert_full(ConstantData::Frozenset {
elements: vec![int_const(2), int_const(1), int_const(1)],
});
assert_eq!(
second, first,
"CPython _PyCode_ConstantKey uses frozenset item keys, not insertion order"
);
assert!(!inserted);
assert!(matches!(
&pool.constants[first],
ConstantData::Frozenset { elements } if elements.len() == 2
));
}
#[test]
fn constant_pool_frozenset_key_preserves_nan_duplicates_like_cpython() {
let mut pool = ConstantPool::default();
let (idx, inserted) = pool.insert_full(ConstantData::Frozenset {
elements: vec![nan_const(), nan_const()],
});
assert_eq!(idx, 0);
assert!(inserted);
assert!(matches!(
&pool.constants[idx],
ConstantData::Frozenset { elements }
if elements.iter().filter(|constant| {
matches!(constant, ConstantData::Float { value } if value.is_nan())
}).count() == 2
));
}
fn test_location(line: u32) -> SourceLocation {
SourceLocation {
@@ -6886,6 +7000,13 @@ mod tests {
instr
}
fn test_true_cond_jump(target: BlockIdx, line: u32) -> InstructionInfo {
let mut instr = test_instr(Instruction::Nop, line);
instr.instr = PseudoOpcode::JumpIfTrue.into();
instr.target = target;
instr
}
fn test_block_push(block: &mut Block, info: InstructionInfo) {
let off = basicblock_next_instr(block).expect("test block instruction slot");
block.instructions[off] = info;
@@ -6970,7 +7091,7 @@ mod tests {
#[test]
fn except_stack_tracks_cpython_depth_and_handler_slots() {
let mut stack = make_except_stack().unwrap();
let mut stack = make_except_stack();
assert_eq!(stack.depth, 0);
assert_eq!(stack.handlers.len(), CO_MAXBLOCKS + 2);
assert_eq!(stack.handlers[0], BlockIdx::NULL);
@@ -6994,7 +7115,7 @@ mod tests {
assert!(handler.preserve_lasti);
assert!(blocks[1].preserve_lasti);
let copy = copy_except_stack(&stack).unwrap();
let copy = copy_except_stack(&stack);
assert_eq!(copy.depth, stack.depth);
assert_eq!(copy.handlers, stack.handlers);
@@ -7399,6 +7520,52 @@ mod tests {
);
}
#[test]
fn optimize_load_const_pseudo_opcode_breaks_effective_load_const() {
let mut block = Block::default();
test_block_push(
&mut block,
test_instr(
Instruction::LoadConst {
consti: Arg::marker(),
},
90,
),
);
test_block_push(&mut block, test_true_cond_jump(BlockIdx::new(0), 90));
let mut copy = test_instr(Instruction::Copy { i: Arg::marker() }, 90);
copy.arg = OpArg::new(1);
test_block_push(&mut block, copy);
test_block_push(&mut block, test_instr(Instruction::ToBool, 90));
let mut code = test_code_info(block);
let (const_idx, _) = code.metadata.consts.insert_full(ConstantData::Tuple {
elements: vec![ConstantData::Integer {
value: BigInt::from(1),
}],
});
code.blocks[0].instructions[0].arg = OpArg::new(const_idx as u32);
optimize_load_const(&mut code.metadata, &mut code.blocks)
.expect("optimize_load_const succeeds");
// `basicblock_optimize_load_const()` assigns the current
// pseudo opcode to its effective opcode slot, so the following COPY 1
// is not treated as a copy of the earlier LOAD_CONST.
assert!(matches!(
code.blocks[0].instructions[1].instr.pseudo(),
Some(PseudoInstruction::Jump { .. })
));
assert!(matches!(
code.blocks[0].instructions[2].instr.real(),
Some(Instruction::Copy { .. })
));
assert!(matches!(
code.blocks[0].instructions[3].instr.real(),
Some(Instruction::ToBool)
));
}
#[test]
fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() {
let mut block = Block::default();
@@ -7488,6 +7655,24 @@ mod tests {
));
}
#[test]
fn empty_tuple_repeat_folds_negative_count_like_cpython() {
let folded = const_folding_safe_multiply(
&ConstantData::Tuple {
elements: Vec::new(),
},
&ConstantData::Integer {
value: BigInt::from(-1),
},
)
.expect("CPython skips repeat-count checks for empty tuples");
assert!(matches!(
folded,
ConstantData::Tuple { elements } if elements.is_empty()
));
}
#[test]
fn resolve_line_numbers_duplicates_exit_blocks_like_cpython() {
let exit = BlockIdx::new(2);
@@ -7621,4 +7806,51 @@ mod tests {
assert_eq!(threaded.target, BlockIdx::new(3));
assert_eq!(u32::from(threaded.arg), 3);
}
#[test]
fn same_direction_pseudo_conditional_jump_thread_false_keeps_target() {
let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]);
for (i, block) in blocks.iter_mut().enumerate() {
block.cpython_label = InstructionSequenceLabel::from_index(i as i32);
}
blocks[0].next = BlockIdx::new(1);
blocks[1].next = BlockIdx::new(2);
test_block_push(&mut blocks[0], test_cond_jump(BlockIdx::new(1), 10));
test_block_push(&mut blocks[1], test_cond_jump(BlockIdx::new(1), 20));
test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30));
let mut metadata = test_code_info(Block::default()).metadata;
blocks
.optimize_basic_block(&mut metadata, BlockIdx::new(0))
.expect("valid conditional jump chain");
// Only rewrite JUMP_IF_FALSE -> JUMP_IF_TRUE through
// target->b_next. For same-direction jumps, a failed jump_thread()
// leaves the original target unchanged.
assert_eq!(blocks[0].instructions[0].target, BlockIdx::new(1));
assert!(matches!(
blocks[0].instructions[0].instr.pseudo(),
Some(PseudoInstruction::JumpIfFalse { .. })
));
}
#[test]
fn opposite_direction_pseudo_conditional_uses_target_fallthrough() {
let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]);
for (i, block) in blocks.iter_mut().enumerate() {
block.cpython_label = InstructionSequenceLabel::from_index(i as i32);
}
blocks[0].next = BlockIdx::new(1);
blocks[1].next = BlockIdx::new(2);
test_block_push(&mut blocks[0], test_cond_jump(BlockIdx::new(1), 10));
test_block_push(&mut blocks[1], test_true_cond_jump(BlockIdx::new(2), 20));
test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30));
let mut metadata = test_code_info(Block::default()).metadata;
blocks
.optimize_basic_block(&mut metadata, BlockIdx::new(0))
.expect("valid conditional jump chain");
assert_eq!(blocks[0].instructions[0].target, BlockIdx::new(2));
}
}
+84 -2
View File
@@ -8,13 +8,15 @@ extern crate log;
extern crate alloc;
use rustpython_compiler_core::bytecode::ConstantData;
type IndexMap<K, V> = indexmap::IndexMap<K, V, rapidhash::quality::RandomState>;
type IndexSet<T> = indexmap::IndexSet<T, rapidhash::quality::RandomState>;
pub mod compile;
pub mod error;
pub mod ir;
mod preprocess;
pub mod preprocess;
mod string_parser;
pub mod symboltable;
mod unparse;
@@ -24,6 +26,73 @@ use ruff_python_ast as ast;
pub(crate) use compile::InternalResult;
#[cfg(test)]
pub(crate) fn constant_data_to_ast_constant_value(value: ConstantData) -> ast::ConstantValue {
match value {
ConstantData::None => ast::ConstantValue::None,
ConstantData::Boolean { value } => ast::ConstantValue::Boolean(value),
ConstantData::Str { value } => ast::ConstantValue::Str(value.to_string().into_boxed_str()),
ConstantData::Bytes { value } => ast::ConstantValue::Bytes(value.into_boxed_slice()),
ConstantData::Integer { value } => ast::ConstantValue::Integer(value.to_string().into()),
ConstantData::Tuple { elements } => ast::ConstantValue::Tuple(
elements
.into_iter()
.map(constant_data_to_ast_constant_value)
.collect(),
),
ConstantData::Frozenset { elements } => ast::ConstantValue::Frozenset(
elements
.into_iter()
.map(constant_data_to_ast_constant_value)
.collect(),
),
ConstantData::Float { value } => ast::ConstantValue::Float(value),
ConstantData::Complex { value } => ast::ConstantValue::Complex {
real: value.re,
imag: value.im,
},
ConstantData::Ellipsis => ast::ConstantValue::Ellipsis,
ConstantData::Code { .. } | ConstantData::Slice { .. } => {
unreachable!("ast.Constant values cannot contain code objects or slices")
}
}
}
pub(crate) fn ast_constant_value_to_constant_data(value: ast::ConstantValue) -> ConstantData {
match value {
ast::ConstantValue::None => ConstantData::None,
ast::ConstantValue::Boolean(value) => ConstantData::Boolean { value },
ast::ConstantValue::Str(value) => ConstantData::Str {
value: value.to_string().into(),
},
ast::ConstantValue::Bytes(value) => ConstantData::Bytes {
value: value.into_vec(),
},
ast::ConstantValue::Integer(value) => ConstantData::Integer {
value: value
.parse()
.expect("RustPython ast.Constant integer values are decimal integers"),
},
ast::ConstantValue::Tuple(elements) => ConstantData::Tuple {
elements: elements
.into_iter()
.map(ast_constant_value_to_constant_data)
.collect(),
},
ast::ConstantValue::Frozenset(elements) => ConstantData::Frozenset {
elements: elements
.into_iter()
.map(ast_constant_value_to_constant_data)
.collect(),
},
ast::ConstantValue::Float(value) => ConstantData::Float { value },
ast::ConstantValue::Complex { real, imag } => ConstantData::Complex {
value: num_complex::Complex::new(real, imag),
},
ast::ConstantValue::Ellipsis => ConstantData::Ellipsis,
}
}
pub trait ToPythonName {
/// Returns a short name for the node suitable for use in error messages.
fn python_name(&self) -> &'static str;
@@ -48,6 +117,19 @@ impl ToPythonName for ast::Expr {
}
Self::EllipsisLiteral(_) => "ellipsis",
Self::NoneLiteral(_) => "None",
Self::Constant(expr) => match &expr.value {
ast::ConstantValue::None => "None",
ast::ConstantValue::Boolean(true) => "True",
ast::ConstantValue::Boolean(false) => "False",
ast::ConstantValue::Ellipsis => "ellipsis",
ast::ConstantValue::Tuple(_) => "tuple",
ast::ConstantValue::Frozenset(_) => "literal",
ast::ConstantValue::Str(_)
| ast::ConstantValue::Bytes(_)
| ast::ConstantValue::Integer(_)
| ast::ConstantValue::Float(_)
| ast::ConstantValue::Complex { .. } => "literal",
},
Self::NumberLiteral(_) | Self::BytesLiteral(_) | Self::StringLiteral(_) => "literal",
Self::Tuple(_) => "tuple",
Self::List { .. } => "list",
@@ -65,7 +147,7 @@ impl ToPythonName for ast::Expr {
Self::Lambda { .. } => "lambda",
Self::If { .. } => "conditional expression",
Self::Named { .. } => "named expression",
Self::IpyEscapeCommand(_) => todo!(),
Self::IpyEscapeCommand(_) => "expression",
}
}
}
+781 -13
View File
@@ -8,29 +8,401 @@ use ruff_python_ast::{
visitor::transformer::{self, Transformer},
};
use ruff_text_size::{Ranged, TextRange};
use rustpython_compiler_core::bytecode;
const MAXDIGITS: usize = 3;
const F_LJUST: u8 = 1;
pub(crate) fn preprocess_mod(module: &mut ast::Mod) {
let preprocessor = AstPreprocessor;
/// ast_preprocess.c ControlFlowInFinallyContext
#[derive(Clone, Copy)]
struct ControlFlowInFinallyContext {
in_finally: bool,
in_funcdef: bool,
in_loop: bool,
}
/// ast_preprocess.c before_return
fn before_return<E>(
contexts: &[ControlFlowInFinallyContext],
range: TextRange,
warn: &mut impl FnMut(TextRange, String) -> Result<(), E>,
) -> Result<(), E> {
if let Some(ctx) = contexts.last()
&& ctx.in_finally
&& !ctx.in_funcdef
{
warn(range, "'return' in a 'finally' block".to_owned())?;
}
Ok(())
}
/// ast_preprocess.c before_loop_exit
fn before_loop_exit<E>(
contexts: &[ControlFlowInFinallyContext],
range: TextRange,
kw: &str,
warn: &mut impl FnMut(TextRange, String) -> Result<(), E>,
) -> Result<(), E> {
if let Some(ctx) = contexts.last()
&& ctx.in_finally
&& !ctx.in_loop
{
warn(range, format!("'{kw}' in a 'finally' block"))?;
}
Ok(())
}
fn visit_body_with_control_flow_context<E>(
body: &[ast::Stmt],
contexts: &mut Vec<ControlFlowInFinallyContext>,
warn: &mut impl FnMut(TextRange, String) -> Result<(), E>,
in_finally: bool,
in_funcdef: bool,
in_loop: bool,
) -> Result<(), E> {
contexts.push(ControlFlowInFinallyContext {
in_finally,
in_funcdef,
in_loop,
});
visit_body_for_control_flow_in_finally(body, contexts, warn)?;
contexts.pop();
Ok(())
}
fn visit_body_for_control_flow_in_finally<E>(
body: &[ast::Stmt],
contexts: &mut Vec<ControlFlowInFinallyContext>,
warn: &mut impl FnMut(TextRange, String) -> Result<(), E>,
) -> Result<(), E> {
for stmt in body {
visit_stmt_for_control_flow_in_finally(stmt, contexts, warn)?;
}
Ok(())
}
/// ast_preprocess.c astfold_stmt control-flow warning traversal.
fn visit_stmt_for_control_flow_in_finally<E>(
stmt: &ast::Stmt,
contexts: &mut Vec<ControlFlowInFinallyContext>,
warn: &mut impl FnMut(TextRange, String) -> Result<(), E>,
) -> Result<(), E> {
match stmt {
ast::Stmt::FunctionDef(function) => {
visit_body_with_control_flow_context(
&function.body,
contexts,
warn,
false,
true,
false,
)?;
}
ast::Stmt::ClassDef(class) => {
visit_body_for_control_flow_in_finally(&class.body, contexts, warn)?;
}
ast::Stmt::Return(return_stmt) => {
before_return(contexts, return_stmt.range, warn)?;
}
ast::Stmt::For(for_stmt) => {
visit_body_with_control_flow_context(
&for_stmt.body,
contexts,
warn,
false,
false,
true,
)?;
visit_body_for_control_flow_in_finally(&for_stmt.orelse, contexts, warn)?;
}
ast::Stmt::While(while_stmt) => {
visit_body_with_control_flow_context(
&while_stmt.body,
contexts,
warn,
false,
false,
true,
)?;
visit_body_for_control_flow_in_finally(&while_stmt.orelse, contexts, warn)?;
}
ast::Stmt::If(if_stmt) => {
visit_body_for_control_flow_in_finally(&if_stmt.body, contexts, warn)?;
for clause in &if_stmt.elif_else_clauses {
visit_body_for_control_flow_in_finally(&clause.body, contexts, warn)?;
}
}
ast::Stmt::Try(try_stmt) => {
visit_body_for_control_flow_in_finally(&try_stmt.body, contexts, warn)?;
for handler in &try_stmt.handlers {
match handler {
ast::ExceptHandler::ExceptHandler(handler) => {
visit_body_for_control_flow_in_finally(&handler.body, contexts, warn)?;
}
}
}
visit_body_for_control_flow_in_finally(&try_stmt.orelse, contexts, warn)?;
visit_body_with_control_flow_context(
&try_stmt.finalbody,
contexts,
warn,
true,
false,
false,
)?;
}
ast::Stmt::With(with_stmt) => {
visit_body_for_control_flow_in_finally(&with_stmt.body, contexts, warn)?;
}
ast::Stmt::Match(match_stmt) => {
for case in &match_stmt.cases {
visit_body_for_control_flow_in_finally(&case.body, contexts, warn)?;
}
}
ast::Stmt::Break(break_stmt) => {
before_loop_exit(contexts, break_stmt.range, "break", warn)?;
}
ast::Stmt::Continue(continue_stmt) => {
before_loop_exit(contexts, continue_stmt.range, "continue", warn)?;
}
_ => {}
}
Ok(())
}
/// ast_preprocess.c control_flow_in_finally_warning
pub fn warn_control_flow_in_finally<E>(
module: &ast::Mod,
mut warn: impl FnMut(TextRange, String) -> Result<(), E>,
) -> Result<(), E> {
let mut contexts = Vec::new();
match module {
ast::Mod::Module(module) => preprocessor.visit_body(&mut module.body),
ast::Mod::Module(module) => {
visit_body_for_control_flow_in_finally(&module.body, &mut contexts, &mut warn)?;
}
ast::Mod::Expression(_) => {}
}
Ok(())
}
pub fn has_future_annotations(module: &ast::Mod) -> bool {
future_features(module).contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS)
}
pub fn future_features(module: &ast::Mod) -> bytecode::CodeFlags {
checked_future_features(module).unwrap_or_else(|err| err.features)
}
pub struct FutureFeatureError {
pub features: bytecode::CodeFlags,
pub range: TextRange,
pub kind: FutureFeatureErrorKind,
}
pub enum FutureFeatureErrorKind {
InvalidFeature(String),
InvalidBraces,
}
pub fn checked_future_features(
module: &ast::Mod,
) -> Result<bytecode::CodeFlags, FutureFeatureError> {
let ast::Mod::Module(module) = module else {
return Ok(bytecode::CodeFlags::empty());
};
checked_future_features_in_body(&module.body)
}
pub fn checked_future_features_in_body(
body: &[ast::Stmt],
) -> Result<bytecode::CodeFlags, FutureFeatureError> {
let mut future_features = bytecode::CodeFlags::empty();
let mut statements = body.iter();
if let Some(ast::Stmt::Expr(ast::StmtExpr { value, .. })) = statements.clone().next()
&& string_literal_expr_value(value).is_some()
{
statements.next();
}
for statement in statements {
match statement {
ast::Stmt::ImportFrom(ast::StmtImportFrom {
module,
names,
level,
..
}) if *level == 0 && module.as_ref().map(|id| id.as_str()) == Some("__future__") => {
for alias in names {
match alias.name.as_str() {
"nested_scopes" | "generators" | "division" | "absolute_import"
| "with_statement" | "print_function" | "unicode_literals"
| "generator_stop" => {}
"annotations" => {
future_features.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
}
// Accept the future feature name, but leave it
// as a RustPython no-op.
"barry_as_FLUFL" => {}
"braces" => {
return Err(FutureFeatureError {
features: future_features,
range: alias.range,
kind: FutureFeatureErrorKind::InvalidBraces,
});
}
other => {
return Err(FutureFeatureError {
features: future_features,
range: alias.range,
kind: FutureFeatureErrorKind::InvalidFeature(other.to_owned()),
});
}
}
}
}
_ => return Ok(future_features),
}
}
Ok(future_features)
}
pub fn preprocess_statements(
body: &mut [ast::Stmt],
optimize: u8,
future_annotations: bool,
syntax_check_only: bool,
) {
let preprocessor = AstPreprocessor {
optimize,
future_annotations,
constant_folding: !syntax_check_only,
};
for stmt in body {
preprocessor.visit_stmt(stmt);
}
}
pub fn preprocess_mod(
module: &mut ast::Mod,
optimize: u8,
future_annotations: bool,
syntax_check_only: bool,
) {
let preprocessor = AstPreprocessor {
optimize,
future_annotations,
constant_folding: !syntax_check_only,
};
match module {
ast::Mod::Module(module) => preprocessor.visit_astfold_body(&mut module.body),
ast::Mod::Expression(expr) => preprocessor.visit_expr(&mut expr.body),
}
}
struct AstPreprocessor;
struct AstPreprocessor {
optimize: u8,
future_annotations: bool,
constant_folding: bool,
}
impl Transformer for AstPreprocessor {
fn visit_expr(&self, expr: &mut Expr) {
transformer::walk_expr(self, expr);
if let Some(optimized) = optimize_format(expr) {
*expr = optimized;
impl AstPreprocessor {
fn visit_astfold_body(&self, body: &mut ast::Suite) {
let mut docstring = body_starts_with_docstring(body);
if docstring && self.optimize >= 2 {
remove_docstring_from_body(body);
docstring = false;
}
for stmt in body.iter_mut() {
self.visit_stmt(stmt);
}
if !docstring && body_starts_with_docstring(body) {
wrap_first_docstring_as_fstring(body);
}
}
}
impl Transformer for AstPreprocessor {
fn visit_stmt(&self, stmt: &mut ast::Stmt) {
match stmt {
ast::Stmt::FunctionDef(function) => {
if let Some(type_params) = &mut function.type_params {
self.visit_type_params(type_params);
}
self.visit_parameters(&mut function.parameters);
self.visit_astfold_body(&mut function.body);
for decorator in &mut function.decorator_list {
self.visit_decorator(decorator);
}
if let Some(returns) = &mut function.returns {
self.visit_annotation(returns);
}
}
ast::Stmt::ClassDef(class) => {
if let Some(type_params) = &mut class.type_params {
self.visit_type_params(type_params);
}
if let Some(arguments) = &mut class.arguments {
self.visit_arguments(arguments);
}
self.visit_astfold_body(&mut class.body);
for decorator in &mut class.decorator_list {
self.visit_decorator(decorator);
}
}
_ => transformer::walk_stmt(self, stmt),
}
}
fn visit_annotation(&self, expr: &mut Expr) {
if !self.future_annotations {
transformer::walk_annotation(self, expr);
}
}
fn visit_pattern(&self, pattern: &mut ast::Pattern) {
transformer::walk_pattern(self, pattern);
if !self.constant_folding {
return;
}
match pattern {
ast::Pattern::MatchValue(value) => fold_match_value_constant_expr(&mut value.value),
ast::Pattern::MatchMapping(mapping) => {
for key in &mut mapping.keys {
fold_match_value_constant_expr(key);
}
}
_ => {}
}
}
fn visit_expr(&self, expr: &mut Expr) {
transformer::walk_expr(self, expr);
if self.constant_folding {
if let Some(optimized) = optimize_format(expr) {
*expr = optimized;
} else if let Some(optimized) = fold_debug_constant(expr, self.optimize) {
*expr = optimized;
}
}
}
}
fn fold_debug_constant(expr: &Expr, optimize: u8) -> Option<Expr> {
let Expr::Name(name) = expr else {
return None;
};
if !matches!(name.ctx, ast::ExprContext::Load) || name.id.as_str() != "__debug__" {
return None;
}
Some(Expr::BooleanLiteral(ast::ExprBooleanLiteral {
node_index: name.node_index.clone(),
range: name.range,
value: optimize == 0,
}))
}
fn optimize_format(expr: &Expr) -> Option<Expr> {
let Expr::BinOp(binop) = expr else {
return None;
@@ -38,9 +410,7 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
if !matches!(binop.op, Operator::Mod) {
return None;
}
let Expr::StringLiteral(format) = binop.left.as_ref() else {
return None;
};
let (format, _) = string_literal_expr_value(&binop.left)?;
let Expr::Tuple(tuple) = binop.right.as_ref() else {
return None;
};
@@ -52,7 +422,7 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
return None;
}
let elements = parse_format(format.value.to_str(), &tuple.elts)?;
let elements = parse_format(format, &tuple.elts)?;
Some(Expr::FString(ExprFString {
node_index: binop.node_index.clone(),
range: binop.range,
@@ -62,6 +432,8 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
elements: InterpolatedStringElements::from(elements),
flags: FStringFlags::empty(),
}),
runtime_joined_str: None,
runtime_values: None,
}))
}
@@ -163,6 +535,9 @@ fn parse_format_arg(chars: &[char], pos: &mut usize, arg: Expr) -> Option<Interp
debug_text: None,
conversion,
format_spec,
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec: None,
})
}
@@ -223,3 +598,396 @@ fn generated_literal(value: String) -> InterpolatedStringLiteralElement {
value: value.into_boxed_str(),
}
}
fn remove_docstring_from_body(body: &mut ast::Suite) {
if let Some(range) = take_docstring(body) {
if !body.is_empty() {
return;
}
let start = range.start();
let pass_range = TextRange::new(start, start + ruff_text_size::TextSize::from(4));
body.push(ast::Stmt::Pass(ast::StmtPass {
node_index: Default::default(),
range: pass_range,
}));
}
}
fn take_docstring(body: &mut ast::Suite) -> Option<TextRange> {
let ast::Stmt::Expr(expr_stmt) = body.first()? else {
return None;
};
if let Some((_, range)) = string_literal_expr_value(&expr_stmt.value) {
body.remove(0);
return Some(range);
}
None
}
fn body_starts_with_docstring(body: &[ast::Stmt]) -> bool {
let Some(ast::Stmt::Expr(expr_stmt)) = body.first() else {
return false;
};
string_literal_expr_value(&expr_stmt.value).is_some()
}
fn wrap_first_docstring_as_fstring(body: &mut [ast::Stmt]) {
let Some(ast::Stmt::Expr(expr_stmt)) = body.first_mut() else {
return;
};
let Some((value, range)) = string_literal_expr_value(&expr_stmt.value) else {
return;
};
let value = value.to_string();
*expr_stmt.value = ast::Expr::FString(ast::ExprFString {
node_index: AtomicNodeIndex::NONE,
range,
value: FStringValue::single(FString {
range,
node_index: AtomicNodeIndex::NONE,
elements: InterpolatedStringElements::from(vec![InterpolatedStringElement::Literal(
InterpolatedStringLiteralElement {
range,
node_index: AtomicNodeIndex::NONE,
value: value.into_boxed_str(),
},
)]),
flags: FStringFlags::empty(),
}),
runtime_joined_str: None,
runtime_values: None,
});
}
fn string_literal_expr_value(expr: &Expr) -> Option<(&str, TextRange)> {
match expr {
Expr::StringLiteral(string) => Some((string.value.to_str(), expr.range())),
Expr::Constant(ast::ExprConstant {
value: ast::ConstantValue::Str(value),
..
}) => Some((value.as_ref(), expr.range())),
_ => None,
}
}
fn fold_match_value_constant_expr(expr: &mut ast::Expr) {
match expr {
ast::Expr::UnaryOp(unary)
if matches!(unary.op, ast::UnaryOp::USub)
&& matches!(unary.operand.as_ref(), ast::Expr::NumberLiteral(_)) =>
{
if let Some(number) = negate_match_number(&unary.operand) {
*expr = ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: unary.node_index.clone(),
range: unary.range,
value: number,
});
}
}
ast::Expr::BinOp(binop) if matches!(binop.op, ast::Operator::Add | ast::Operator::Sub) => {
fold_match_value_constant_expr(&mut binop.left);
if let Some(number) = fold_match_number_binop(&binop.left, binop.op, &binop.right) {
*expr = ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: binop.node_index.clone(),
range: binop.range,
value: number,
});
}
}
_ => {}
}
}
fn negate_match_number(expr: &ast::Expr) -> Option<ast::Number> {
let ast::Expr::NumberLiteral(number) = expr else {
return None;
};
Some(match &number.value {
ast::Number::Int(value) => {
if *value == ast::Int::ZERO {
ast::Number::Int(ast::Int::ZERO)
} else {
return None;
}
}
ast::Number::Float(value) => ast::Number::Float(-value),
ast::Number::Complex { real, imag } => ast::Number::Complex {
real: -real,
imag: -imag,
},
})
}
fn fold_match_number_binop(
left: &ast::Expr,
op: ast::Operator,
right: &ast::Expr,
) -> Option<ast::Number> {
let ast::Expr::NumberLiteral(left) = left else {
return None;
};
let ast::Expr::NumberLiteral(right) = right else {
return None;
};
let right = match right.value {
ast::Number::Complex { real, imag } => (real, imag),
_ => return None,
};
enum MatchNumberLeft {
Real(f64),
Complex { real: f64, imag: f64 },
}
let left = match &left.value {
ast::Number::Int(value) => MatchNumberLeft::Real(value.as_i64()? as f64),
ast::Number::Float(value) => MatchNumberLeft::Real(*value),
ast::Number::Complex { real, imag } => MatchNumberLeft::Complex {
real: *real,
imag: *imag,
},
};
let (real, imag) = match (left, op) {
(MatchNumberLeft::Real(left), ast::Operator::Add) => (left + right.0, right.1),
(MatchNumberLeft::Real(left), ast::Operator::Sub) => (left - right.0, -right.1),
(MatchNumberLeft::Complex { real, imag }, ast::Operator::Add) => {
(real + right.0, imag + right.1)
}
(MatchNumberLeft::Complex { real, imag }, ast::Operator::Sub) => {
(real - right.0, imag - right.1)
}
_ => return None,
};
Some(ast::Number::Complex { real, imag })
}
#[cfg(test)]
mod tests {
use super::*;
fn first_match_value(source: &str) -> ast::Expr {
let parsed = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into())
.unwrap()
.into_syntax();
let mut module = parsed;
let future_annotations = has_future_annotations(&module);
preprocess_mod(&mut module, 0, future_annotations, false);
let ast::Mod::Module(module) = module else {
panic!("expected module");
};
let [ast::Stmt::Match(match_stmt)] = &module.body[..] else {
panic!("expected a single match statement");
};
let ast::Pattern::MatchValue(value) = &match_stmt.cases[0].pattern else {
panic!("expected a value pattern");
};
*value.value.clone()
}
fn preprocess_source(source: &str) -> ast::Mod {
let mut module = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into())
.unwrap()
.into_syntax();
let future_annotations = has_future_annotations(&module);
preprocess_mod(&mut module, 0, future_annotations, false);
module
}
fn preprocess_source_with_optimize(source: &str, optimize: u8) -> ast::Mod {
let mut module = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into())
.unwrap()
.into_syntax();
let future_annotations = has_future_annotations(&module);
preprocess_mod(&mut module, optimize, future_annotations, false);
module
}
fn preprocess_source_syntax_check_only(source: &str, optimize: u8) -> ast::Mod {
let mut module = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into())
.unwrap()
.into_syntax();
let future_annotations = has_future_annotations(&module);
preprocess_mod(&mut module, optimize, future_annotations, true);
module
}
#[test]
fn folds_match_value_negative_float_in_preprocess() {
let value = first_match_value(
"\
match value:
case -1.5:
pass
",
);
let ast::Expr::NumberLiteral(number) = value else {
panic!("expected folded number literal, got {value:?}");
};
assert!(matches!(number.value, ast::Number::Float(value) if value == -1.5));
}
#[test]
fn folds_match_value_complex_binop_in_preprocess() {
let value = first_match_value(
"\
match value:
case 1 + 2j:
pass
",
);
let ast::Expr::NumberLiteral(number) = value else {
panic!("expected folded number literal, got {value:?}");
};
assert!(
matches!(number.value, ast::Number::Complex { real, imag } if real == 1.0 && imag == 2.0)
);
}
#[test]
fn folds_match_value_complex_complex_binop_in_preprocess() {
let left = ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: AtomicNodeIndex::NONE,
range: TextRange::default(),
value: ast::Number::Complex {
real: 0.0,
imag: 1.0,
},
});
let right = ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: AtomicNodeIndex::NONE,
range: TextRange::default(),
value: ast::Number::Complex {
real: 0.0,
imag: 2.0,
},
});
let number = fold_match_number_binop(&left, ast::Operator::Add, &right)
.expect("CPython fold_const_match_patterns() uses PyNumber_Add");
assert!(
matches!(number, ast::Number::Complex { real, imag } if real == 0.0 && imag == 3.0)
);
}
#[test]
fn folds_match_value_real_minus_zero_complex_preserves_negative_zero_in_preprocess() {
let value = first_match_value(
"\
match value:
case 0 - 0j:
pass
",
);
let ast::Expr::NumberLiteral(number) = value else {
panic!("expected folded number literal, got {value:?}");
};
assert!(matches!(number.value, ast::Number::Complex { real, imag }
if real == 0.0 && imag == 0.0 && imag.is_sign_negative()));
}
#[test]
fn future_annotations_skip_annotation_preprocess_like_cpython() {
let module = preprocess_source(
"\
from __future__ import annotations
def f(x: __debug__) -> __debug__:
pass
y: __debug__
z = __debug__
",
);
let ast::Mod::Module(module) = module else {
panic!("expected module");
};
let ast::Stmt::FunctionDef(function) = &module.body[1] else {
panic!("expected function");
};
let annotation = function.parameters.args[0]
.parameter
.annotation
.as_deref()
.expect("missing parameter annotation");
assert!(
matches!(annotation, ast::Expr::Name(name) if name.id.as_str() == "__debug__"),
"future annotations should skip parameter annotation folding, got {annotation:?}"
);
let returns = function
.returns
.as_deref()
.expect("missing return annotation");
assert!(
matches!(returns, ast::Expr::Name(name) if name.id.as_str() == "__debug__"),
"future annotations should skip return annotation folding, got {returns:?}"
);
let ast::Stmt::AnnAssign(ann_assign) = &module.body[2] else {
panic!("expected annotated assignment");
};
assert!(
matches!(ann_assign.annotation.as_ref(), ast::Expr::Name(name) if name.id.as_str() == "__debug__"),
"future annotations should skip annotated assignment annotation folding, got {:?}",
ann_assign.annotation
);
let ast::Stmt::Assign(assign) = &module.body[3] else {
panic!("expected assignment");
};
assert!(
matches!(assign.value.as_ref(), ast::Expr::BooleanLiteral(boolean) if boolean.value),
"non-annotation expression should still fold __debug__, got {:?}",
assign.value
);
}
#[test]
fn late_future_annotations_do_not_affect_preprocess_like_cpython() {
let module = preprocess_source(
"\
x = 1
from __future__ import annotations
y: __debug__
",
);
let ast::Mod::Module(module) = module else {
panic!("expected module");
};
let ast::Stmt::AnnAssign(ann_assign) = &module.body[2] else {
panic!("expected annotated assignment");
};
assert!(
matches!(ann_assign.annotation.as_ref(), ast::Expr::BooleanLiteral(boolean) if boolean.value),
"late future import should not disable annotation folding, got {:?}",
ann_assign.annotation
);
}
#[test]
fn optimize_two_wraps_new_docstring_after_removing_original() {
let module = preprocess_source_with_optimize("\"first\"\n\"second\"\n", 2);
let ast::Mod::Module(module) = module else {
panic!("expected module");
};
let [ast::Stmt::Expr(expr)] = &module.body[..] else {
panic!("expected only the second statement to remain");
};
assert!(
matches!(expr.value.as_ref(), ast::Expr::FString(_)),
"CPython wraps the new leading string as JoinedStr so it is not a docstring"
);
}
#[test]
fn syntax_check_only_disables_constant_folding_but_keeps_docstring_strip() {
let module = preprocess_source_syntax_check_only("\"doc\"\nvalue = __debug__\n", 2);
let ast::Mod::Module(module) = module else {
panic!("expected module");
};
assert!(
matches!(module.body[0], ast::Stmt::Assign(_)),
"optimize=2 should still strip docstrings in syntax_check_only mode"
);
let ast::Stmt::Assign(assign) = &module.body[0] else {
panic!("expected assignment");
};
assert!(
matches!(assign.value.as_ref(), ast::Expr::Name(name) if name.id.as_str() == "__debug__"),
"syntax_check_only should skip __debug__ folding, got {:?}",
assign.value
);
}
}
File diff suppressed because it is too large. Load diff
+85 -21
View File
@@ -58,6 +58,63 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
self.f.write_fmt(f)
}
fn unparse_float(&mut self, value: f64) -> fmt::Result {
#[allow(clippy::correctness, clippy::assertions_on_constants)]
const {
assert!(f64::MAX_10_EXP == 308)
};
if value.is_infinite() {
self.p("1e309")
} else {
self.p(&rustpython_literal::float::to_string(value))
}
}
fn unparse_complex(&mut self, real: f64, imag: f64) -> fmt::Result {
self.p(&rustpython_literal::complex::to_string(real, imag).replace("inf", "1e309"))
}
fn unparse_constant_value(&mut self, value: &ast::ConstantValue) -> fmt::Result {
match value {
ast::ConstantValue::None => self.p("None"),
ast::ConstantValue::Boolean(value) => self.p(if *value { "True" } else { "False" }),
ast::ConstantValue::Str(value) => UnicodeEscape::new_repr(value.as_ref().into())
.str_repr()
.fmt(self.f),
ast::ConstantValue::Bytes(value) => AsciiEscape::new_repr(value.as_ref())
.bytes_repr()
.fmt(self.f),
ast::ConstantValue::Integer(value) => self.p(value.as_ref()),
ast::ConstantValue::Tuple(elements) => {
self.p("(")?;
let mut first = true;
for element in elements {
self.p_delim(&mut first, ", ")?;
self.unparse_constant_value(element)?;
}
self.p_if(elements.len() == 1, ",")?;
self.p(")")
}
ast::ConstantValue::Frozenset(elements) => {
if elements.is_empty() {
self.p("frozenset()")
} else {
self.p("frozenset({")?;
let mut first = true;
for element in elements {
self.p_delim(&mut first, ", ")?;
self.unparse_constant_value(element)?;
}
self.p("})")
}
}
ast::ConstantValue::Float(value) => self.unparse_float(*value),
ast::ConstantValue::Complex { real, imag } => self.unparse_complex(*real, *imag),
ast::ConstantValue::Ellipsis => self.p("..."),
}
}
fn unparse_expr(&mut self, ast: &ast::Expr, level: u8) -> fmt::Result {
macro_rules! op_prec {
($op_ty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => {
@@ -87,6 +144,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
values,
node_index: _,
range: _range,
..
}) => {
let (op, prec) = op_prec!(bin, op, ast::BoolOp, And("and", AND), Or("or", OR));
group_if!(prec, {
@@ -102,6 +160,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value,
node_index: _,
range: _range,
..
}) => {
group_if!(precedence::TUPLE, {
self.unparse_expr(target, precedence::ATOM)?;
@@ -115,6 +174,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
right,
node_index: _,
range: _range,
..
}) => {
let right_associative = matches!(op, ast::Operator::Pow);
let (op, prec) = op_prec!(
@@ -146,6 +206,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
operand,
node_index: _,
range: _range,
..
}) => {
let (op, prec) = op_prec!(
un,
@@ -166,6 +227,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
body,
node_index: _,
range: _range,
..
}) => {
group_if!(precedence::TEST, {
if let Some(parameters) = parameters {
@@ -183,6 +245,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
orelse,
node_index: _,
range: _range,
..
}) => {
group_if!(precedence::TEST, {
self.unparse_expr(body, precedence::TEST + 1)?;
@@ -196,6 +259,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
items,
node_index: _,
range: _range,
..
}) => {
self.p("{")?;
let mut first = true;
@@ -214,6 +278,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
elts,
node_index: _,
range: _range,
..
}) => {
self.p("{")?;
let mut first = true;
@@ -228,6 +293,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators,
node_index: _,
range: _range,
..
}) => {
self.p("[")?;
self.unparse_expr(elt, precedence::TEST)?;
@@ -239,6 +305,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators,
node_index: _,
range: _range,
..
}) => {
self.p("{")?;
self.unparse_expr(elt, precedence::TEST)?;
@@ -251,6 +318,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators,
node_index: _,
range: _range,
..
}) => {
self.p("{")?;
self.unparse_expr(key, precedence::TEST)?;
@@ -265,6 +333,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators,
node_index: _,
range: _range,
..
}) => {
self.p("(")?;
self.unparse_expr(elt, precedence::TEST)?;
@@ -275,6 +344,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value,
node_index: _,
range: _range,
..
}) => {
group_if!(precedence::AWAIT, {
self.p("await ")?;
@@ -285,6 +355,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value,
node_index: _,
range: _range,
..
}) => {
if let Some(value) = value {
write!(self, "(yield {})", UnparseExpr::new(value, self.source))?;
@@ -296,6 +367,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value,
node_index: _,
range: _range,
..
}) => {
write!(
self,
@@ -309,6 +381,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
comparators,
node_index: _,
range: _range,
..
}) => {
group_if!(precedence::CMP, {
let new_lvl = precedence::CMP + 1;
@@ -326,6 +399,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
arguments: ast::Arguments { args, keywords, .. },
node_index: _,
range: _range,
..
}) => {
self.unparse_expr(func, precedence::ATOM)?;
self.p("(")?;
@@ -379,26 +453,13 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
.bytes_repr()
.fmt(self.f)?
}
ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
#[allow(clippy::correctness, clippy::assertions_on_constants)]
const {
assert!(f64::MAX_10_EXP == 308)
};
let inf_str = "1e309";
match value {
ast::Number::Int(int) => int.fmt(self.f)?,
&ast::Number::Float(fp) => {
if fp.is_infinite() {
self.p(inf_str)?
} else {
self.p(&rustpython_literal::float::to_string(fp))?
}
}
&ast::Number::Complex { real, imag } => self
.p(&rustpython_literal::complex::to_string(real, imag)
.replace("inf", inf_str))?,
}
ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value {
ast::Number::Int(int) => int.fmt(self.f)?,
&ast::Number::Float(fp) => self.unparse_float(fp)?,
&ast::Number::Complex { real, imag } => self.unparse_complex(real, imag)?,
},
ast::Expr::Constant(ast::ExprConstant { value, .. }) => {
self.unparse_constant_value(value)?
}
ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => {
self.p(if *value { "True" } else { "False" })?
@@ -460,6 +521,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
step,
node_index: _,
range: _range,
..
}) => {
if let Some(lower) = lower {
self.unparse_expr(lower, precedence::TEST)?;
@@ -554,7 +616,9 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
let buffered =
fmt::from_fn(|f| Unparser::new(f, self.source).unparse_expr(val, precedence::TEST + 1))
.to_string();
if let Some(ast::DebugText { leading, trailing }) = debug_text {
if let Some(debug_text) = debug_text {
let leading = debug_text.leading.as_str();
let trailing = debug_text.trailing.as_str();
self.p(leading)?;
self.p(self.source.slice(val.range()))?;
self.p(trailing)?;
+7
View File
@@ -468,6 +468,13 @@ bitflags! {
const COROUTINE = 0x0080;
const ITERABLE_COROUTINE = 0x0100;
const ASYNC_GENERATOR = 0x0200;
const FUTURE_DIVISION = 0x20000;
const FUTURE_ABSOLUTE_IMPORT = 0x40000;
const FUTURE_WITH_STATEMENT = 0x80000;
const FUTURE_PRINT_FUNCTION = 0x100000;
const FUTURE_UNICODE_LITERALS = 0x200000;
const FUTURE_BARRY_AS_BDFL = 0x400000;
const FUTURE_GENERATOR_STOP = 0x800000;
const FUTURE_ANNOTATIONS = 0x1000000;
/// If a code object represents a function and has a docstring,
/// this bit is set and the first item in co_consts is the docstring.
@@ -754,9 +754,8 @@ impl Opcode {
/// Stack effect when the instruction takes its branch (jump=true).
///
/// CPython equivalent: `stack_effect(opcode, oparg, jump=True)`.
/// For most instructions this equals the fallthrough effect.
/// Override for instructions where branch and fallthrough differ
/// (e.g. [`Self::ForIter`]: fallthrough = +1, branch = −1).
/// Current opcode metadata has the same real-opcode stack effect
/// for jump and fallthrough stack-depth calculation.
#[must_use]
pub fn stack_effect_jump(&self, oparg: u32) -> i32 {
self.stack_effect(oparg)
@@ -1415,6 +1414,26 @@ mod tests {
assert!(!AnyInstruction::from(PseudoOpcode::Jump).has_const());
}
#[test]
fn stack_effects_match_cpython_opcode_metadata() {
assert_eq!(Opcode::ForIter.stack_effect_info(0).popped(), 1);
assert_eq!(Opcode::ForIter.stack_effect_info(0).pushed(), 2);
assert_eq!(Opcode::ForIter.stack_effect(0), 1);
assert_eq!(Opcode::ForIter.stack_effect_jump(0), 1);
assert_eq!(Opcode::EndAsyncFor.stack_effect_info(0).popped(), 2);
assert_eq!(Opcode::EndAsyncFor.stack_effect_info(0).pushed(), 0);
assert_eq!(Opcode::PopJumpIfFalse.stack_effect(0), -1);
assert_eq!(Opcode::PopJumpIfFalse.stack_effect_jump(0), -1);
assert_eq!(PseudoOpcode::SetupFinally.stack_effect_info(0).pushed(), 1);
assert_eq!(PseudoOpcode::SetupFinally.stack_effect(0), 0);
assert_eq!(PseudoOpcode::SetupFinally.stack_effect_jump(0), 1);
assert_eq!(PseudoOpcode::SetupCleanup.stack_effect_info(0).pushed(), 2);
assert_eq!(PseudoOpcode::SetupCleanup.stack_effect(0), 0);
assert_eq!(PseudoOpcode::SetupCleanup.stack_effect_jump(0), 2);
}
#[test]
fn no_fallthrough_flags_match_cpython_basicblock_nofallthrough() {
assert!(Opcode::JumpForward.is_no_fallthrough());
+4 -3
View File
@@ -777,19 +777,20 @@ oparg_enum!(
#[derive(Copy, Clone)]
pub struct UnpackExArgs {
pub before: u8,
pub after: u8,
pub after: u32,
}
impl From<u32> for UnpackExArgs {
fn from(value: u32) -> Self {
let [before, after, ..] = value.to_le_bytes();
let before = (value & 0xFF) as u8;
let after = value >> 8;
Self { before, after }
}
}
impl From<UnpackExArgs> for u32 {
fn from(value: UnpackExArgs) -> Self {
Self::from_le_bytes([value.before, value.after, 0, 0])
Self::from(value.before) | (value.after << 8)
}
}
+5055 -150
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -205,7 +205,7 @@ mod tests {
let scope = vm.new_scope_with_builtins();
let code_obj = vm
.compile(source.trim(), Mode::Exec, FNAME)
.map_err(|err| vm.new_syntax_error(&err, Some(source)))
.map_err(|err| err.into_pyexception(vm, Some(source)))
.unwrap();
scope.globals.set_item("code", code_obj.into(), vm).unwrap();
@@ -228,7 +228,7 @@ output = re.sub(r'(<code object \w+ at )0x[0-9a-fA-F]+', r'\g<1>0xdeadbeef', tmp
let py_code_obj = vm
.compile(py_source, Mode::Exec, FNAME)
.map_err(|err| vm.new_syntax_error(&err, Some(py_source)))
.map_err(|err| err.into_pyexception(vm, Some(py_source)))
.unwrap();
vm.run_code_obj(py_code_obj, scope.clone()).unwrap();
@@ -1,5 +1,6 @@
---
source: crates/stdlib/src/_opcode.rs
assertion_line: 318
expression: "dis(r#\"\ndef f(one: int):\n int.new_attr: int\n [list][0].new_attr: [int, str]\n my_lst = [1]\n my_lst[one]: int\n return my_lst\n\"#)"
---
0 RESUME 0
@@ -15,7 +16,7 @@ expression: "dis(r#\"\ndef f(one: int):\n int.new_attr: int\n [list][0].ne
Disassembly of <code object __annotate__ at 0xdeadbeef file "<?>", line 1>:
1 RESUME 0
LOAD_FAST_BORROW 0 (format)
LOAD_FAST_CHECK 0 (format)
LOAD_SMALL_INT 2
COMPARE_OP 132 (>)
POP_JUMP_IF_FALSE 3 (to L1)
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: x = not True
assertion_line: 281
expression: "dis(r#\"\nx = not True\n\"#)"
---
0 RESUME 0
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: "if 1:\n pass"
assertion_line: 290
expression: "dis(r#\"\nif 1:\n pass\n\"#)"
---
0 RESUME 0
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: "if True and False and False:\n pass"
assertion_line: 252
expression: "dis(r#\"\nif True and False and False:\n pass\n\"#)"
---
0 RESUME 0
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: "if (True and False) or (False and True):\n pass"
assertion_line: 262
expression: "dis(r#\"\nif (True and False) or (False and True):\n pass\n\"#)"
---
0 RESUME 0
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: "if True or False or False:\n pass"
assertion_line: 242
expression: "dis(r#\"\nif True or False or False:\n pass\n\"#)"
---
0 RESUME 0
@@ -1,6 +1,7 @@
---
source: crates/stdlib/src/_opcode.rs
expression: x = Test() and False or False
assertion_line: 272
expression: "dis(r#\"\nx = Test() and False or False\n\"#)"
---
0 RESUME 0
+1
View File
@@ -70,6 +70,7 @@ static_assertions = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
thin-vec = { workspace = true }
memchr = { workspace = true }
flamer = { workspace = true, optional = true }
+68 -10
View File
@@ -34,7 +34,7 @@ use core::{
ops::Deref,
pin::Pin,
ptr::NonNull,
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering},
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering},
};
use indexmap::{IndexMap, map::Entry};
use itertools::Itertools;
@@ -53,6 +53,7 @@ pub struct PyType {
pub heaptype_ext: Option<Pin<Box<HeapTypeExt>>>,
/// Type version tag for inline caching. 0 means unassigned/invalidated.
pub tp_version_tag: AtomicU32,
pub abc_tpflags: AtomicU64,
}
/// Monotonic counter for type version tags. Once it reaches `u32::MAX`,
@@ -590,7 +591,9 @@ impl PyType {
// Check each base in order and inherit the first collection flag found
for base in bases {
let base_flags = base.slots.flags & COLLECTION_FLAGS;
let base_flags = (base.slots.flags
| PyTypeFlags::from_bits_truncate(base.abc_tpflags.load(Ordering::Acquire)))
& COLLECTION_FLAGS;
if !base_flags.is_empty() {
slots.flags |= base_flags;
return;
@@ -598,6 +601,58 @@ impl PyType {
}
}
fn inherited_abc_tpflags(bases: &[PyRef<Self>]) -> u64 {
const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate(
PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(),
);
for base in bases {
let base_flags =
PyTypeFlags::from_bits_truncate(base.abc_tpflags.load(Ordering::Acquire))
& COLLECTION_FLAGS;
if !base_flags.is_empty() {
return base_flags.bits();
}
}
0
}
pub fn has_patma_collection_flag(&self, flag: PyTypeFlags) -> bool {
debug_assert!(matches!(flag, PyTypeFlags::SEQUENCE | PyTypeFlags::MAPPING));
const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate(
PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(),
);
let slot_flags = self.slots.flags & COLLECTION_FLAGS;
if !slot_flags.is_empty() {
return slot_flags.contains(flag);
}
PyTypeFlags::from_bits_truncate(self.abc_tpflags.load(Ordering::Acquire)).contains(flag)
}
pub fn set_abc_collection_flags_recursive(&self, flags: PyTypeFlags) {
const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate(
PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(),
);
let flags = flags & COLLECTION_FLAGS;
if flags.is_empty() {
return;
}
let collection_bits = COLLECTION_FLAGS.bits();
let flags_bits = flags.bits();
let _ = self
.abc_tpflags
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |old| {
Some((old & !collection_bits) | flags_bits)
});
self.modified();
for weak_ref in self.subclasses.read().iter() {
if let Some(subclass) = weak_ref.upgrade()
&& let Some(subclass) = subclass.downcast_ref::<Self>()
{
subclass.set_abc_collection_flags_recursive(flags);
}
}
}
/// Check for __abc_tpflags__ and set the appropriate flags
/// This checks in attrs and all base classes for __abc_tpflags__
fn check_abc_tpflags(
@@ -626,21 +681,19 @@ impl PyType {
.to_owned(),
);
}
// Don't override flags already inherited from a base class.
if !slots.flags.intersects(COLLECTION_FLAGS) {
slots.flags |= masked;
}
slots.flags.remove(COLLECTION_FLAGS);
slots.flags |= masked;
return Ok(());
}
// No __abc_tpflags__ on this class — inheritance already happened
// in inherit_patma_flags, so nothing more to do if those bits are set.
// No __abc_tpflags__ on this class. Inheritance already happened in
// inherit_patma_flags, using base order and including ABC markers.
if slots.flags.intersects(COLLECTION_FLAGS) {
return Ok(());
}
// Then check in base classes (legacy path for cases that bypass
// inherit_patma_flags).
// Then check in base classes for legacy paths that bypassed
// inherit_patma_flags.
for base in bases {
if let Some(abc_tpflags_obj) = base.find_name_in_mro(abc_tpflags_name)
&& let Some(int_obj) = abc_tpflags_obj.downcast_ref::<crate::builtins::int::PyInt>()
@@ -654,6 +707,7 @@ impl PyType {
.to_owned(),
);
}
slots.flags.remove(COLLECTION_FLAGS);
slots.flags |= masked;
return Ok(());
}
@@ -716,6 +770,7 @@ impl PyType {
));
}
let inherited_abc_tpflags = Self::inherited_abc_tpflags(&bases);
let new_type = PyRef::new_ref(
Self {
base: Some(base),
@@ -726,6 +781,7 @@ impl PyType {
slots,
heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))),
tp_version_tag: AtomicU32::new(0),
abc_tpflags: AtomicU64::new(inherited_abc_tpflags),
},
metaclass,
None,
@@ -775,6 +831,7 @@ impl PyType {
slots.flags |= PyTypeFlags::MANAGED_WEAKREF;
}
let inherited_abc_tpflags = Self::inherited_abc_tpflags(core::slice::from_ref(&base));
let bases = PyRwLock::new(vec![base.clone()]);
let mro = base.mro_map_collect(|x| x.to_owned());
@@ -788,6 +845,7 @@ impl PyType {
slots,
heaptype_ext: None,
tp_version_tag: AtomicU32::new(0),
abc_tpflags: AtomicU64::new(inherited_abc_tpflags),
},
metaclass,
None,
+1 -1
View File
@@ -6,7 +6,7 @@ pub fn eval(vm: &VirtualMachine, source: &str, scope: Scope, source_path: &str)
debug!("Code object: {bytecode:?}");
vm.run_code_obj(bytecode, scope)
}
Err(err) => Err(vm.new_syntax_error(&err, Some(source))),
Err(err) => Err(err.into_pyexception(vm, Some(source))),
}
}
+12 -6
View File
@@ -2480,12 +2480,18 @@ pub(super) mod types {
let maybe_lineno = zelf
.as_object()
.get_attr("lineno", vm)
.and_then(|obj| obj.str_utf8(vm))
.ok();
let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| {
obj.str(vm)
.unwrap_or_else(|_| vm.ctx.new_str("<filename str() failed>"))
});
.ok()
.filter(|obj| !vm.is_none(obj))
.and_then(|obj| obj.str_utf8(vm).ok());
let maybe_filename = zelf
.as_object()
.get_attr("filename", vm)
.ok()
.filter(|obj| !vm.is_none(obj))
.map(|obj| {
obj.str(vm)
.unwrap_or_else(|_| vm.ctx.new_str("<filename str() failed>"))
});
let msg = match zelf.as_object().get_attr("msg", vm) {
Ok(obj) => obj
+69 -23
View File
@@ -3050,9 +3050,17 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value();
let nargs_val = nargs.get(arg) as usize;
let Some(cls_type) = cls.downcast_ref::<PyType>() else {
return Err(vm.new_type_error("called match pattern must be a class"));
};
// Only the error paths need the class name; compute it lazily so a
// successful match does not take the name lock or allocate.
let type_name = || cls_type.name().to_string();
// Check if subject is an instance of cls
if subject.is_instance(cls.as_ref(), vm)? {
let mut extracted = vec![];
let seen_attrs = PySet::default().into_ref(&vm.ctx);
// Get __match_args__ for positional arguments if nargs > 0
if nargs_val > 0 {
@@ -3066,12 +3074,7 @@ impl ExecutingFrame<'_> {
Ok(tuple) => tuple,
Err(match_args) => {
// __match_args__ must be a tuple
// Get type names for error message
let type_name = cls
.downcast::<crate::builtins::PyType>()
.ok()
.and_then(|t| t.__name__(vm).to_str().map(str::to_owned))
.unwrap_or_else(|| String::from("?"));
let type_name = type_name();
let match_args_type_name = match_args.class().__name__(vm);
return Err(vm.new_type_error(format!(
"{type_name}.__match_args__ must be a tuple (got {match_args_type_name})"
@@ -3081,9 +3084,12 @@ impl ExecutingFrame<'_> {
// Check if we have enough match args
if match_args.len() < nargs_val {
let type_name = type_name();
let plural = if match_args.len() == 1 { "" } else { "s" };
return Err(vm.new_type_error(format!(
"class pattern accepts at most {} positional sub-patterns ({} given)",
"{type_name}() accepts {} positional sub-pattern{} ({} given)",
match_args.len(),
plural,
nargs_val
)));
}
@@ -3094,11 +3100,20 @@ impl ExecutingFrame<'_> {
let attr_name_str = match attr_name.downcast_ref::<PyStr>() {
Some(s) => s,
None => {
return Err(vm.new_type_error(
"__match_args__ elements must be strings",
));
let attr_type_name = attr_name.class().name();
return Err(vm.new_type_error(format!(
"__match_args__ elements must be strings (got {attr_type_name})"
)));
}
};
if seen_attrs.__contains__(attr_name.as_object(), vm)? {
let type_name = type_name();
let attr_repr = attr_name.as_object().repr(vm)?;
return Err(vm.new_type_error(format!(
"{type_name}() got multiple sub-patterns for attribute {attr_repr}"
)));
}
seen_attrs.add(attr_name.clone(), vm)?;
match subject.get_attr(attr_name_str, vm) {
Ok(value) => extracted.push(value),
Err(e)
@@ -3115,9 +3130,8 @@ impl ExecutingFrame<'_> {
// No __match_args__, check if this is a type with MATCH_SELF behavior
// For built-in types like bool, int, str, list, tuple, dict, etc.
// they match the subject itself as the single positional argument
let is_match_self_type = cls
.downcast::<PyType>()
.is_ok_and(|t| t.slots.flags.contains(PyTypeFlags::_MATCH_SELF));
let is_match_self_type =
cls_type.slots.flags.contains(PyTypeFlags::_MATCH_SELF);
if is_match_self_type {
if nargs_val == 1 {
@@ -3125,16 +3139,18 @@ impl ExecutingFrame<'_> {
extracted.push(subject.clone());
} else if nargs_val > 1 {
// Too many positional arguments for MATCH_SELF
return Err(vm.new_type_error(
"class pattern accepts at most 1 positional sub-pattern for MATCH_SELF types",
));
let type_name = type_name();
return Err(vm.new_type_error(format!(
"{type_name}() accepts 1 positional sub-pattern ({nargs_val} given)"
)));
}
} else {
// No __match_args__ and not a MATCH_SELF type
if nargs_val > 0 {
return Err(vm.new_type_error(
"class pattern defines no positional sub-patterns (__match_args__ missing)",
));
let type_name = type_name();
return Err(vm.new_type_error(format!(
"{type_name}() accepts 0 positional sub-patterns ({nargs_val} given)"
)));
}
}
}
@@ -3143,6 +3159,14 @@ impl ExecutingFrame<'_> {
// Extract keyword attributes
for name in kwd_attrs {
let name_str = name.downcast_ref::<PyStr>().unwrap();
if seen_attrs.__contains__(name_str.as_object(), vm)? {
let type_name = type_name();
let attr_repr = name.as_object().repr(vm)?;
return Err(vm.new_type_error(format!(
"{type_name}() got multiple sub-patterns for attribute {attr_repr}"
)));
}
seen_attrs.add(name.clone(), vm)?;
match subject.get_attr(name_str, vm) {
Ok(value) => extracted.push(value),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => {
@@ -3166,10 +3190,14 @@ impl ExecutingFrame<'_> {
let subject = self.nth_value(1); // stack[-2]
// Check if subject is a mapping and extract values for keys
if subject.class().slots.flags.contains(PyTypeFlags::MAPPING) {
if subject
.class()
.has_patma_collection_flag(PyTypeFlags::MAPPING)
{
let keys = keys_tuple.downcast_ref::<PyTuple>().unwrap();
let mut values = Vec::new();
let mut all_match = true;
let seen_keys = PySet::default().into_ref(&vm.ctx);
// We use the two argument form of map.get(key, default) for two reasons:
// - Atomically check for a key and get its value without error handling.
@@ -3186,6 +3214,13 @@ impl ExecutingFrame<'_> {
.new_base_object(vm.ctx.types.object_type.to_owned(), None);
for key in keys {
if seen_keys.__contains__(key.as_object(), vm)? {
return Err(vm.new_value_error(format!(
"mapping pattern checks duplicate key ({})",
key.as_object().repr(vm)?
)));
}
seen_keys.add(key.as_object().to_owned(), vm)?;
// value = map.get(key, dummy)
match get_method.call((key.as_object(), dummy.clone()), vm) {
Ok(value) => {
@@ -3202,6 +3237,13 @@ impl ExecutingFrame<'_> {
} else {
// Fallback if .get() method is not available (shouldn't happen for mappings)
for key in keys {
if seen_keys.__contains__(key.as_object(), vm)? {
return Err(vm.new_value_error(format!(
"mapping pattern checks duplicate key ({})",
key.as_object().repr(vm)?
)));
}
seen_keys.add(key.as_object().to_owned(), vm)?;
match subject.get_item(key.as_object(), vm) {
Ok(value) => values.push(value),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
@@ -3231,7 +3273,9 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value();
// Check if the type has the MAPPING flag
let is_mapping = subject.class().slots.flags.contains(PyTypeFlags::MAPPING);
let is_mapping = subject
.class()
.has_patma_collection_flag(PyTypeFlags::MAPPING);
self.push_value(subject);
self.push_value(vm.ctx.new_bool(is_mapping).into());
@@ -3242,7 +3286,9 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value();
// Check if the type has the SEQUENCE flag
let is_sequence = subject.class().slots.flags.contains(PyTypeFlags::SEQUENCE);
let is_sequence = subject
.class()
.has_patma_collection_flag(PyTypeFlags::SEQUENCE);
self.push_value(subject);
self.push_value(vm.ctx.new_bool(is_sequence).into());
@@ -6845,7 +6891,7 @@ impl ExecutingFrame<'_> {
}
}
fn execute_unpack_ex(&mut self, vm: &VirtualMachine, before: u8, after: u8) -> FrameResult {
fn execute_unpack_ex(&mut self, vm: &VirtualMachine, before: u8, after: u32) -> FrameResult {
let (before, after) = (before as usize, after as usize);
let value = self.pop_value();
let not_iterable = value.class().slots.iter.load().is_none()
+2 -2
View File
@@ -141,7 +141,7 @@ pub fn import_file(
file_path,
vm.compile_opts(),
)
.map_err(|err| vm.new_syntax_error(&err, Some(content)))?;
.map_err(|err| err.into_pyexception(vm, Some(content)))?;
import_code_obj(vm, module_name, code, true)
}
@@ -154,7 +154,7 @@ pub fn import_source(vm: &VirtualMachine, module_name: &str, content: &str) -> P
"<source>",
vm.compile_opts(),
)
.map_err(|err| vm.new_syntax_error(&err, Some(content)))?;
.map_err(|err| err.into_pyexception(vm, Some(content)))?;
import_code_obj(vm, module_name, code, false)
}
+3
View File
@@ -2479,6 +2479,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
slots: PyType::make_slots(),
heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
abc_tpflags: core::sync::atomic::AtomicU64::new(0),
};
let object_payload = PyType {
base: None,
@@ -2489,6 +2490,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
slots: object::PyBaseObject::make_slots(),
heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
abc_tpflags: core::sync::atomic::AtomicU64::new(0),
};
// Both type_type and object_type are instances of `type`, which has
// HAS_DICT and HAS_WEAKREF, so they need both ObjExt and WeakRefList prefixes.
@@ -2585,6 +2587,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
slots: PyWeak::make_slots(),
heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0),
abc_tpflags: core::sync::atomic::AtomicU64::new(0),
};
let weakref_type = PyRef::new_ref(weakref_type, type_type.clone(), None);
// Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE)
+5 -1
View File
@@ -207,7 +207,7 @@ impl VirtualMachine {
event: TraceEvent,
arg: Option<PyObjectRef>,
) -> PyResult<Option<PyObjectRef>> {
if self.use_tracing.get() {
if self.use_tracing.get() && !self.tracing_is_suppressed() {
self._trace_event_inner(event, arg)
} else {
Ok(None)
@@ -247,7 +247,9 @@ impl VirtualMachine {
// tracing function itself.
if is_trace_event && !self.is_none(&trace_func) {
self.use_tracing.set(false);
self.enter_tracing();
let res = trace_func.call(args.clone(), self);
self.leave_tracing();
self.use_tracing.set(true);
match res {
Ok(result) => {
@@ -268,7 +270,9 @@ impl VirtualMachine {
if is_profile_event && !self.is_none(&profile_func) {
self.use_tracing.set(false);
self.enter_tracing();
let res = profile_func.call(args, self);
self.leave_tracing();
self.use_tracing.set(true);
if res.is_err() {
*self.profile_func.borrow_mut() = self.ctx.none();
+19 -2
View File
@@ -9,11 +9,11 @@ pub(crate) use _abc::module_def;
mod _abc {
use crate::{
AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyFrozenSet, PyList, PySet, PyStr, PyTupleRef, PyTypeRef, PyWeak},
builtins::{PyFrozenSet, PyList, PySet, PyStr, PyTupleRef, PyType, PyTypeRef, PyWeak},
common::lock::PyRwLock,
convert::ToPyObject,
protocol::PyIterReturn,
types::Constructor,
types::{Constructor, PyTypeFlags},
};
use core::sync::atomic::{AtomicU64, Ordering};
@@ -238,6 +238,23 @@ mod _abc {
// Invalidate negative cache
increment_invalidation_counter();
if let Some(cls_type) = cls.downcast_ref::<PyType>()
&& let Some(subclass_type) = subclass.downcast_ref::<PyType>()
{
// _abc_register propagates Py_TPFLAGS_SEQUENCE/MAPPING
// recursively so MATCH_SEQUENCE/MATCH_MAPPING see ABC registration.
let collection_mask = PyTypeFlags::SEQUENCE | PyTypeFlags::MAPPING;
let collection_flags = (cls_type.slots.flags
| PyTypeFlags::from_bits_truncate(cls_type.abc_tpflags.load(Ordering::Acquire)))
& collection_mask;
if !subclass_type.is(vm.ctx.types.str_type)
&& !subclass_type.is(vm.ctx.types.bytes_type)
&& !subclass_type.is(vm.ctx.types.bytearray_type)
{
subclass_type.set_abc_collection_flags_recursive(collection_flags);
}
}
Ok(subclass)
}
+1800 -152
View File
File diff suppressed because it is too large. Load diff
+109 -24
View File
@@ -2,14 +2,78 @@ use super::*;
use rustpython_compiler_core::SourceFile;
pub(super) struct PositionalArguments {
pub range: TextRange,
pub args: Box<[ast::Expr]>,
range: TextRange,
kind: PositionalArgumentsKind,
}
enum PositionalArgumentsKind {
Args(Box<[ast::Expr]>),
RuntimeValues(Vec<Option<ast::Expr>>),
}
impl PositionalArguments {
pub(super) fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
let values: Vec<Option<ast::Expr>> =
get_node_list_field(vm, source_file, object, field, typ)?;
Ok(Self::from_values(TextRange::default(), values))
}
fn from_args(range: TextRange, args: Box<[ast::Expr]>) -> Self {
Self {
range,
kind: PositionalArgumentsKind::Args(args),
}
}
fn from_runtime_values(range: TextRange, values: Vec<Option<ast::Expr>>) -> Self {
Self {
range,
kind: PositionalArgumentsKind::RuntimeValues(values),
}
}
fn from_values(range: TextRange, values: Vec<Option<ast::Expr>>) -> Self {
if values.iter().any(Option::is_none) {
Self::from_runtime_values(range, values)
} else {
Self::from_args(
range,
values
.into_iter()
.flatten()
.collect::<Vec<_>>()
.into_boxed_slice(),
)
}
}
fn range(&self) -> TextRange {
self.range
}
fn into_args_and_runtime_values(self) -> (Box<[ast::Expr]>, Option<Vec<Option<ast::Expr>>>) {
match self.kind {
PositionalArgumentsKind::Args(args) => (args, None),
PositionalArgumentsKind::RuntimeValues(values) => (
lower_runtime_expr_list(values.clone()).into_boxed_slice(),
Some(values),
),
}
}
}
impl Node for PositionalArguments {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { args, range: _ } = self;
BoxedSlice(args).ast_to_object(vm, source_file)
match self.kind {
PositionalArgumentsKind::Args(args) => BoxedSlice(args).ast_to_object(vm, source_file),
PositionalArgumentsKind::RuntimeValues(values) => values.ast_to_object(vm, source_file),
}
}
fn ast_from_object(
@@ -18,10 +82,7 @@ impl Node for PositionalArguments {
object: PyObjectRef,
) -> PyResult<Self> {
let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self {
args: args.0,
range: TextRange::default(), // TODO
})
Ok(Self::from_args(TextRange::default(), args.0))
}
}
@@ -30,6 +91,21 @@ pub(super) struct KeywordArguments {
pub keywords: Box<[ast::Keyword]>,
}
impl KeywordArguments {
pub(super) fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
Ok(Self {
keywords: get_node_boxed_slice_field(vm, source_file, object, field, typ)?,
range: TextRange::default(),
})
}
}
impl Node for KeywordArguments {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { keywords, range: _ } = self;
@@ -45,7 +121,7 @@ impl Node for KeywordArguments {
let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self {
keywords: keywords.0,
range: TextRange::default(), // TODO
range: TextRange::default(),
})
}
}
@@ -54,13 +130,16 @@ pub(super) fn merge_function_call_arguments(
pos_args: PositionalArguments,
key_args: KeywordArguments,
) -> ast::Arguments {
let range = pos_args.range.cover(key_args.range);
let range = pos_args.range().cover(key_args.range);
let (args, runtime_args) = pos_args.into_args_and_runtime_values();
ast::Arguments {
node_index: Default::default(),
range,
args: pos_args.args,
args,
keywords: key_args.keywords,
runtime_args,
runtime_bases: None,
}
}
@@ -68,10 +147,12 @@ pub(super) fn split_function_call_arguments(
args: ast::Arguments,
) -> (PositionalArguments, KeywordArguments) {
let ast::Arguments {
node_index: _,
range: _,
args,
keywords,
runtime_args,
runtime_bases: _,
..
} = args;
let positional_arguments_range = args
@@ -80,10 +161,10 @@ pub(super) fn split_function_call_arguments(
.reduce(|acc, next| acc.cover(next))
.unwrap_or_default();
// debug_assert!(range.contains_range(positional_arguments_range));
let positional_arguments = PositionalArguments {
range: positional_arguments_range,
args,
};
let positional_arguments = runtime_args.map_or_else(
|| PositionalArguments::from_args(positional_arguments_range, args),
|values| PositionalArguments::from_runtime_values(positional_arguments_range, values),
);
let keyword_arguments_range = keywords
.iter()
@@ -107,10 +188,12 @@ pub(super) fn split_class_def_args(
Some(args) => *args,
};
let ast::Arguments {
node_index: _,
range: _,
args,
keywords,
runtime_args: _,
runtime_bases,
..
} = args;
let positional_arguments_range = args
@@ -119,10 +202,10 @@ pub(super) fn split_class_def_args(
.reduce(|acc, next| acc.cover(next))
.unwrap_or_default();
// debug_assert!(range.contains_range(positional_arguments_range));
let positional_arguments = PositionalArguments {
range: positional_arguments_range,
args,
};
let positional_arguments = runtime_bases.map_or_else(
|| PositionalArguments::from_args(positional_arguments_range, args),
|values| PositionalArguments::from_runtime_values(positional_arguments_range, values),
);
let keyword_arguments_range = keywords
.iter()
@@ -146,10 +229,10 @@ pub(super) fn merge_class_def_args(
return None;
}
let args = if let Some(positional_arguments) = positional_arguments {
positional_arguments.args
let (args, runtime_bases) = if let Some(positional_arguments) = positional_arguments {
positional_arguments.into_args_and_runtime_values()
} else {
vec![].into_boxed_slice()
(vec![].into_boxed_slice(), None)
};
let keywords = if let Some(keyword_arguments) = keyword_arguments {
keyword_arguments.keywords
@@ -162,5 +245,7 @@ pub(super) fn merge_class_def_args(
range: Default::default(), // TODO
args,
keywords,
runtime_args: None,
runtime_bases,
}))
}
+7 -3
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::builtins::PyIntRef;
use rustpython_codegen::compile::ruff_int_to_bigint;
use rustpython_compiler_core::SourceFile;
@@ -13,7 +14,11 @@ impl Node for ast::Identifier {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let py_str = PyUtf8StrRef::try_from_object(vm, object)?;
if !object.class().is(vm.ctx.types.str_type) {
return Err(vm.new_type_error("AST identifier must be of type str"));
}
let py_str = PyUtf8StrRef::try_from_object(vm, object)
.map_err(|_| vm.new_type_error("AST identifier must be of type str"))?;
Ok(Self::new(py_str.as_str(), TextRange::default()))
}
}
@@ -28,7 +33,6 @@ impl Node for ast::Int {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
// FIXME: performance
let value: PyIntRef = object.try_into_value(vm)?;
let value = value.as_bigint().to_string();
Ok(value.parse().unwrap())
@@ -45,6 +49,6 @@ impl Node for bool {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
i32::try_from_object(vm, object).map(|i| i != 0)
node_object_to_i32(vm, object).map(|i| i != 0)
}
}
+402 -135
View File
@@ -1,12 +1,15 @@
use super::*;
use crate::builtins::{PyComplex, PyFrozenSet, PyTuple};
use ast::str_prefix::StringLiteralPrefix;
use rustpython_compiler_core::SourceFile;
use rustpython_codegen::compile::ruff_int_to_bigint;
use rustpython_compiler_core::{SourceFile, bytecode::ConstantData};
#[derive(Debug)]
pub(super) struct Constant {
pub(super) range: TextRange,
pub(super) value: ConstantLiteral,
kind: Option<Box<str>>,
invalid_type: Option<String>,
}
impl Constant {
@@ -19,6 +22,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Str { value, prefix },
kind: None,
invalid_type: None,
}
}
@@ -26,6 +31,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Int(value),
kind: None,
invalid_type: None,
}
}
@@ -33,6 +40,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Float(value),
kind: None,
invalid_type: None,
}
}
@@ -40,6 +49,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Complex { real, imag },
kind: None,
invalid_type: None,
}
}
@@ -47,6 +58,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Bytes(value),
kind: None,
invalid_type: None,
}
}
@@ -54,6 +67,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Bool(value),
kind: None,
invalid_type: None,
}
}
@@ -61,6 +76,8 @@ impl Constant {
Self {
range,
value: ConstantLiteral::None,
kind: None,
invalid_type: None,
}
}
@@ -68,15 +85,29 @@ impl Constant {
Self {
range,
value: ConstantLiteral::Ellipsis,
kind: None,
invalid_type: None,
}
}
pub(crate) fn into_expr(self) -> ast::Expr {
constant_to_ruff_expr(self)
let Self {
range,
value,
kind,
invalid_type,
} = self;
ast::Expr::Constant(ast::ExprConstant {
node_index: Default::default(),
range,
value: constant_data_to_ast_constant_value(constant_literal_to_constant_data(&value)),
kind: kind.or_else(|| constant_literal_kind(&value)),
invalid_type: invalid_type.map(String::into_boxed_str),
})
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) enum ConstantLiteral {
None,
Bool(bool),
@@ -96,20 +127,361 @@ pub(crate) enum ConstantLiteral {
Ellipsis,
}
pub(super) fn invalid_constant_type(expr: &ast::Expr) -> Option<Box<str>> {
match expr {
ast::Expr::Constant(expr) => expr.invalid_type.clone(),
_ => None,
}
}
pub(super) fn runtime_string_from_pyobject(
vm: &VirtualMachine,
object: PyObjectRef,
) -> (Option<Box<str>>, Option<Vec<u8>>) {
runtime_string_from_object(vm, object)
}
pub(super) fn runtime_string_object(
vm: &VirtualMachine,
value: Option<Box<str>>,
bytes: Option<Vec<u8>>,
) -> Option<PyObjectRef> {
runtime_string_to_object(vm, value, bytes)
}
pub(super) fn expr_constant_to_object(
vm: &VirtualMachine,
source_file: &SourceFile,
expr: ast::ExprConstant,
) -> PyObjectRef {
let ast::ExprConstant {
node_index: _,
range,
value,
kind,
invalid_type: _,
} = expr;
let constant = ast_constant_value_to_constant_data(value);
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprConstant::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("value", constant_data_to_object(vm, constant), vm)
.unwrap();
let kind = kind.map_or_else(|| vm.ctx.none(), |kind| vm.ctx.new_str(kind).into());
dict.set_item("kind", kind, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
pub(super) fn runtime_interpolation_object(
vm: &VirtualMachine,
str: Option<ast::ConstantValue>,
format_spec: Option<Box<ast::Expr>>,
) -> Option<(PyObjectRef, Option<Box<ast::Expr>>)> {
let str = str?;
Some((
constant_data_to_object(vm, ast_constant_value_to_constant_data(str)),
format_spec,
))
}
pub(super) fn runtime_stmt_type_comment_object(
vm: &VirtualMachine,
value: Option<Box<str>>,
bytes: Option<Vec<u8>>,
) -> Option<PyObjectRef> {
runtime_string_object(vm, value, bytes)
}
fn constant_literal_to_constant_data(value: &ConstantLiteral) -> ConstantData {
match value {
ConstantLiteral::None => ConstantData::None,
ConstantLiteral::Bool(value) => ConstantData::Boolean { value: *value },
ConstantLiteral::Str { value, .. } => ConstantData::Str {
value: value.as_ref().into(),
},
ConstantLiteral::Bytes(value) => ConstantData::Bytes {
value: value.to_vec(),
},
ConstantLiteral::Int(value) => ConstantData::Integer {
value: ruff_int_to_bigint(value).unwrap(),
},
ConstantLiteral::Tuple(value) => ConstantData::Tuple {
elements: value
.iter()
.map(constant_literal_to_constant_data)
.collect(),
},
ConstantLiteral::FrozenSet(value) => ConstantData::Frozenset {
elements: value
.iter()
.map(constant_literal_to_constant_data)
.collect(),
},
ConstantLiteral::Float(value) => ConstantData::Float { value: *value },
ConstantLiteral::Complex { real, imag } => ConstantData::Complex {
value: num_complex::Complex::new(*real, *imag),
},
ConstantLiteral::Ellipsis => ConstantData::Ellipsis,
}
}
fn constant_literal_kind(value: &ConstantLiteral) -> Option<Box<str>> {
match value {
ConstantLiteral::Str {
prefix: StringLiteralPrefix::Unicode,
..
} => Some("u".into()),
_ => None,
}
}
pub(super) fn constant_data_to_ast_constant_value(value: ConstantData) -> ast::ConstantValue {
match value {
ConstantData::None => ast::ConstantValue::None,
ConstantData::Boolean { value } => ast::ConstantValue::Boolean(value),
ConstantData::Str { value } => ast::ConstantValue::Str(value.to_string().into_boxed_str()),
ConstantData::Bytes { value } => ast::ConstantValue::Bytes(value.into_boxed_slice()),
ConstantData::Integer { value } => ast::ConstantValue::Integer(value.to_string().into()),
ConstantData::Tuple { elements } => ast::ConstantValue::Tuple(
elements
.into_iter()
.map(constant_data_to_ast_constant_value)
.collect(),
),
ConstantData::Frozenset { elements } => ast::ConstantValue::Frozenset(
elements
.into_iter()
.map(constant_data_to_ast_constant_value)
.collect(),
),
ConstantData::Float { value } => ast::ConstantValue::Float(value),
ConstantData::Complex { value } => ast::ConstantValue::Complex {
real: value.re,
imag: value.im,
},
ConstantData::Ellipsis => ast::ConstantValue::Ellipsis,
ConstantData::Code { .. } | ConstantData::Slice { .. } => {
unreachable!("ast.Constant values cannot contain code objects or slices")
}
}
}
pub(super) fn ast_constant_value_to_constant_data(value: ast::ConstantValue) -> ConstantData {
match value {
ast::ConstantValue::None => ConstantData::None,
ast::ConstantValue::Boolean(value) => ConstantData::Boolean { value },
ast::ConstantValue::Str(value) => ConstantData::Str {
value: value.to_string().into(),
},
ast::ConstantValue::Bytes(value) => ConstantData::Bytes {
value: value.into_vec(),
},
ast::ConstantValue::Integer(value) => ConstantData::Integer {
value: value
.parse()
.expect("RustPython ast.Constant integer values are decimal integers"),
},
ast::ConstantValue::Tuple(elements) => ConstantData::Tuple {
elements: elements
.into_iter()
.map(ast_constant_value_to_constant_data)
.collect(),
},
ast::ConstantValue::Frozenset(elements) => ConstantData::Frozenset {
elements: elements
.into_iter()
.map(ast_constant_value_to_constant_data)
.collect(),
},
ast::ConstantValue::Float(value) => ConstantData::Float { value },
ast::ConstantValue::Complex { real, imag } => ConstantData::Complex {
value: num_complex::Complex::new(real, imag),
},
ast::ConstantValue::Ellipsis => ConstantData::Ellipsis,
}
}
pub(super) fn constant_object_to_constant_data(
vm: &VirtualMachine,
source_file: &SourceFile,
value_object: PyObjectRef,
) -> PyResult<ConstantData> {
let value = ConstantLiteral::ast_from_object(vm, source_file, value_object)?;
Ok(constant_literal_to_constant_data(&value))
}
fn runtime_string_from_object(
vm: &VirtualMachine,
object: PyObjectRef,
) -> (Option<Box<str>>, Option<Vec<u8>>) {
if object.class().is(vm.ctx.types.str_type) {
(
Some(
object
.try_to_value::<String>(vm)
.expect("AST string field was validated as str")
.into_boxed_str(),
),
None,
)
} else {
(
None,
Some(
object
.try_to_value::<Vec<u8>>(vm)
.expect("AST string field was validated as bytes"),
),
)
}
}
fn runtime_string_to_object(
vm: &VirtualMachine,
value: Option<Box<str>>,
bytes: Option<Vec<u8>>,
) -> Option<PyObjectRef> {
if let Some(bytes) = bytes {
Some(vm.ctx.new_bytes(bytes).into())
} else {
value.map(|value| vm.ctx.new_str(value).into())
}
}
fn first_invalid_constant_type(vm: &VirtualMachine, value_object: PyObjectRef) -> PyResult<String> {
let cls = value_object.class();
let class_name = cls.name().to_owned();
if cls.is(vm.ctx.types.tuple_type) {
vm.with_recursion(" during compilation", || {
let tuple = value_object.clone().downcast::<PyTuple>().map_err(|obj| {
vm.new_type_error(format!(
"Expected type {}, not {}",
PyTuple::static_type().name(),
obj.class().name()
))
})?;
for item in tuple.iter() {
if let Some(invalid_type) = first_invalid_constant_type_opt(vm, item.clone())? {
return Ok(invalid_type);
}
}
Ok(class_name)
})
} else if cls.is(vm.ctx.types.frozenset_type) {
vm.with_recursion(" during compilation", || {
let set = value_object.clone().downcast::<PyFrozenSet>().unwrap();
for item in set.elements() {
if let Some(invalid_type) = first_invalid_constant_type_opt(vm, item)? {
return Ok(invalid_type);
}
}
Ok(class_name)
})
} else {
Ok(class_name)
}
}
fn first_invalid_constant_type_opt(
vm: &VirtualMachine,
value_object: PyObjectRef,
) -> PyResult<Option<String>> {
let cls = value_object.class();
if cls.is(vm.ctx.types.none_type)
|| cls.is(vm.ctx.types.bool_type)
|| cls.is(vm.ctx.types.str_type)
|| cls.is(vm.ctx.types.bytes_type)
|| cls.is(vm.ctx.types.int_type)
|| cls.is(vm.ctx.types.float_type)
|| cls.is(vm.ctx.types.complex_type)
|| cls.is(vm.ctx.types.ellipsis_type)
{
return Ok(None);
}
if cls.is(vm.ctx.types.tuple_type) || cls.is(vm.ctx.types.frozenset_type) {
return first_invalid_constant_type(vm, value_object).map(Some);
}
Ok(Some(cls.name().to_owned()))
}
fn constant_data_to_object(vm: &VirtualMachine, constant: ConstantData) -> PyObjectRef {
match constant {
ConstantData::None => vm.ctx.none(),
ConstantData::Boolean { value } => vm.ctx.new_bool(value).to_pyobject(vm),
ConstantData::Str { value } => vm.ctx.new_str(value.to_string()).to_pyobject(vm),
ConstantData::Bytes { value } => vm.ctx.new_bytes(value).to_pyobject(vm),
ConstantData::Integer { value } => vm.ctx.new_int(value).into(),
ConstantData::Tuple { elements } => {
let value = elements
.into_iter()
.map(|c| constant_data_to_object(vm, c))
.collect();
vm.ctx.new_tuple(value).to_pyobject(vm)
}
ConstantData::Frozenset { elements } => PyFrozenSet::from_iter(
vm,
elements.into_iter().map(|c| constant_data_to_object(vm, c)),
)
.unwrap()
.into_pyobject(vm),
ConstantData::Float { value } => vm.ctx.new_float(value).into_pyobject(vm),
ConstantData::Complex { value } => vm.ctx.new_complex(value).into_pyobject(vm),
ConstantData::Ellipsis => vm.ctx.ellipsis.clone().into(),
ConstantData::Code { .. } | ConstantData::Slice { .. } => {
unreachable!("ast.Constant values cannot contain code objects or slices")
}
}
}
// constructor
pub(super) fn constant_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<Constant> {
let value_object = get_node_field(vm, &object, "value", "Constant")?;
let (value, invalid_type) =
match ConstantLiteral::ast_from_object(vm, source_file, value_object.clone()) {
Ok(value) => (value, None),
Err(_) => (
ConstantLiteral::None,
Some(first_invalid_constant_type(vm, value_object)?),
),
};
let kind = get_node_field_opt(vm, &object, "kind")?
.map(|object| {
if !object.class().is(vm.ctx.types.str_type) {
return Err(vm.new_type_error("AST string must be of type str"));
}
Ok(object.try_to_value::<String>(vm)?.into_boxed_str())
})
.transpose()?;
Ok(Constant {
range,
value,
kind,
invalid_type,
})
}
impl Node for Constant {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { range, value } = self;
let Self {
range,
value,
kind,
invalid_type: _,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprConstant::static_type().to_owned())
.unwrap();
let kind = match &value {
ConstantLiteral::Str {
prefix: StringLiteralPrefix::Unicode,
..
} => vm.ctx.new_str("u").into(),
_ => vm.ctx.none(),
};
let kind = kind
.or_else(|| constant_literal_kind(&value))
.map_or_else(|| vm.ctx.none(), |kind| vm.ctx.new_str(kind).into());
let value = value.ast_to_object(vm, source_file);
let dict = node.as_object().dict().unwrap();
dict.set_item("value", value, vm).unwrap();
@@ -123,16 +495,8 @@ impl Node for Constant {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let value_object = get_node_field(vm, &object, "value", "Constant")?;
let value = Node::ast_from_object(vm, source_file, value_object)?;
Ok(Self {
value,
// kind: get_node_field_opt(_vm, &_object, "kind")?
// .map(|obj| Node::ast_from_object(_vm, obj))
// .transpose()?,
range: range_from_object(vm, source_file, object, "Constant")?,
})
let range = range_from_object(vm, source_file, object.clone(), "Constant")?;
constant_from_object_with_range(vm, source_file, object, range)
}
}
@@ -201,8 +565,12 @@ impl Node for ConstantLiteral {
})?;
let tuple = tuple
.into_iter()
.cloned()
.map(|object| Node::ast_from_object(vm, source_file, object))
.map(|object| {
let object = object.clone();
vm.with_recursion(" during compilation", || {
Node::ast_from_object(vm, source_file, object)
})
})
.collect::<PyResult<_>>()?;
Self::Tuple(tuple)
} else if cls.is(vm.ctx.types.frozenset_type) {
@@ -210,7 +578,11 @@ impl Node for ConstantLiteral {
let elements = set
.elements()
.into_iter()
.map(|object| Node::ast_from_object(vm, source_file, object))
.map(|object| {
vm.with_recursion(" during compilation", || {
Node::ast_from_object(vm, source_file, object)
})
})
.collect::<PyResult<_>>()?;
Self::FrozenSet(elements)
} else if cls.is(vm.ctx.types.float_type) {
@@ -244,117 +616,6 @@ impl Node for ConstantLiteral {
}
}
fn constant_to_ruff_expr(value: Constant) -> ast::Expr {
let Constant { value, range } = value;
match value {
ConstantLiteral::None => ast::Expr::NoneLiteral(ast::ExprNoneLiteral {
node_index: Default::default(),
range,
}),
ConstantLiteral::Bool(value) => ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral {
node_index: Default::default(),
range,
value,
}),
ConstantLiteral::Str { value, prefix } => {
ast::Expr::StringLiteral(ast::ExprStringLiteral {
node_index: Default::default(),
range,
value: ast::StringLiteralValue::single(ast::StringLiteral {
node_index: Default::default(),
range,
value,
flags: ast::StringLiteralFlags::empty().with_prefix(prefix),
}),
})
}
ConstantLiteral::Bytes(value) => {
ast::Expr::BytesLiteral(ast::ExprBytesLiteral {
node_index: Default::default(),
range,
value: ast::BytesLiteralValue::single(ast::BytesLiteral {
node_index: Default::default(),
range,
value,
flags: ast::BytesLiteralFlags::empty(), // TODO
}),
})
}
ConstantLiteral::Int(value) => ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: Default::default(),
range,
value: ast::Number::Int(value),
}),
ConstantLiteral::Tuple(value) => ast::Expr::Tuple(ast::ExprTuple {
node_index: Default::default(),
range,
elts: value
.into_iter()
.map(|value| {
constant_to_ruff_expr(Constant {
range: TextRange::default(),
value,
})
})
.collect(),
ctx: ast::ExprContext::Load,
// TODO: Does this matter?
parenthesized: true,
}),
ConstantLiteral::FrozenSet(value) => {
let args = if value.is_empty() {
Vec::new()
} else {
vec![ast::Expr::Set(ast::ExprSet {
node_index: Default::default(),
range: TextRange::default(),
elts: value
.into_iter()
.map(|value| {
constant_to_ruff_expr(Constant {
range: TextRange::default(),
value,
})
})
.collect(),
})]
};
ast::Expr::Call(ast::ExprCall {
node_index: Default::default(),
range,
func: Box::new(ast::Expr::Name(ast::ExprName {
node_index: Default::default(),
range: TextRange::default(),
id: ast::name::Name::new_static("frozenset"),
ctx: ast::ExprContext::Load,
})),
arguments: ast::Arguments {
node_index: Default::default(),
range,
args: args.into(),
keywords: Box::default(),
},
})
}
ConstantLiteral::Float(value) => ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: Default::default(),
range,
value: ast::Number::Float(value),
}),
ConstantLiteral::Complex { real, imag } => {
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: Default::default(),
range,
value: ast::Number::Complex { real, imag },
})
}
ConstantLiteral::Ellipsis => ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral {
node_index: Default::default(),
range,
}),
}
}
pub(super) fn number_literal_to_object(
vm: &VirtualMachine,
source_file: &SourceFile,
@@ -364,6 +625,7 @@ pub(super) fn number_literal_to_object(
node_index: _,
range,
value,
..
} = constant;
let c = match value {
ast::Number::Int(n) => Constant::new_int(n, range),
@@ -382,6 +644,7 @@ pub(super) fn string_literal_to_object(
node_index: _,
range,
value,
..
} = constant;
let prefix = value
.iter()
@@ -400,6 +663,7 @@ pub(super) fn bytes_literal_to_object(
node_index: _,
range,
value,
..
} = constant;
let bytes = value.as_slice().iter().flat_map(|b| b.value.iter());
let c = Constant::new_bytes(bytes.copied().collect(), range);
@@ -415,6 +679,7 @@ pub(super) fn boolean_literal_to_object(
node_index: _,
range,
value,
..
} = constant;
let c = Constant::new_bool(value, range);
c.ast_to_object(vm, source_file)
@@ -428,6 +693,7 @@ pub(super) fn none_literal_to_object(
let ast::ExprNoneLiteral {
node_index: _,
range,
..
} = constant;
let c = Constant::new_none(range);
c.ast_to_object(vm, source_file)
@@ -441,6 +707,7 @@ pub(super) fn ellipsis_literal_to_object(
let ast::ExprEllipsisLiteral {
node_index: _,
range,
..
} = constant;
let c = Constant::new_ellipsis(range);
c.ast_to_object(vm, source_file)
+37 -16
View File
@@ -12,10 +12,15 @@ pub(super) fn ast_to_object(
range,
test,
body,
runtime_body,
runtime_orelse,
} = clause;
let Some(test) = test else {
assert!(rest.len() == 0);
return body.ast_to_object(vm, source_file);
return runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
};
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeStmtIf::static_type().to_owned())
@@ -24,10 +29,15 @@ pub(super) fn ast_to_object(
dict.set_item("test", test.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
let body = runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
let orelse = if let Some(next) = rest.next() {
let orelse = if let Some(values) = runtime_orelse {
values.ast_to_object(vm, source_file)
} else if let Some(next) = rest.next() {
if next.test.is_some() {
let next = ast::ElifElseClause {
range: TextRange::new(next.range.start(), range.end()),
@@ -37,7 +47,10 @@ pub(super) fn ast_to_object(
.new_list(vec![ast_to_object(next, rest, vm, source_file)])
.into()
} else {
next.body.ast_to_object(vm, source_file)
next.runtime_body.map_or_else(
|| next.body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
)
}
} else {
vm.ctx.new_list(vec![]).into()
@@ -48,40 +61,45 @@ pub(super) fn ast_to_object(
node.into()
}
pub(super) fn ast_from_object(
pub(super) fn ast_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::StmtIf> {
let test = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "test", "If")?)?;
let body = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "body", "If")?)?;
let orelse: Vec<ast::Stmt> = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "orelse", "If")?,
)?;
let range = range_from_object(vm, source_file, object, "If")?;
let test = get_required_node_field(vm, source_file, &object, "test", "If")?;
let body: Vec<Option<ast::Stmt>> = get_node_list_field(vm, source_file, &object, "body", "If")?;
let orelse: Vec<Option<ast::Stmt>> =
get_node_list_field(vm, source_file, &object, "orelse", "If")?;
let runtime_body = runtime_stmt_list_metadata(&body);
let runtime_orelse = runtime_stmt_list_metadata(&orelse);
let body = lower_runtime_stmt_list(body);
let orelse = lower_runtime_stmt_list(orelse);
let elif_else_clauses = if orelse.is_empty() {
vec![]
} else if let [ast::Stmt::If(_)] = &*orelse {
let Some(ast::Stmt::If(ast::StmtIf {
node_index: _,
node_index,
range,
test,
body,
mut elif_else_clauses,
runtime_body,
})) = orelse.into_iter().next()
else {
unreachable!()
};
debug_assert!(runtime_orelse.is_none());
elif_else_clauses.insert(
0,
ast::ElifElseClause {
node_index: Default::default(),
node_index,
range,
test: Some(*test),
body,
runtime_body,
runtime_orelse: None,
},
);
elif_else_clauses
@@ -91,6 +109,8 @@ pub(super) fn ast_from_object(
range,
test: None,
body: orelse,
runtime_body: runtime_orelse,
runtime_orelse: None,
}]
};
@@ -100,5 +120,6 @@ pub(super) fn ast_from_object(
body,
elif_else_clauses,
range,
runtime_body,
})
}
+67 -32
View File
@@ -1,6 +1,22 @@
use super::*;
use rustpython_compiler_core::SourceFile;
fn ensure_excepthandler_node(vm: &VirtualMachine, object: &PyObjectRef) -> PyResult<()> {
if vm.is_none(object)
|| !is_node_instance(
vm,
object,
pyast::NodeExceptHandlerExceptHandler::static_type(),
)?
{
return Err(vm.new_type_error(format!(
"expected some sort of excepthandler, but got {}",
object.repr(vm)?
)));
}
Ok(())
}
// sum
impl Node for ast::ExceptHandler {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
@@ -13,25 +29,53 @@ impl Node for ast::ExceptHandler {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(
if cls.is(pyast::NodeExceptHandlerExceptHandler::static_type()) {
Self::ExceptHandler(ast::ExceptHandlerExceptHandler::ast_from_object(
vm,
source_file,
object,
)?)
} else {
return Err(vm.new_type_error(format!(
"expected some sort of excepthandler, but got {}",
object.repr(vm)?
)));
},
)
ensure_excepthandler_node(vm, &object)?;
let range = excepthandler_range_from_object(vm, source_file, object.clone())?;
Ok(Self::ExceptHandler(except_handler_from_object_with_range(
vm,
source_file,
object,
range,
)?))
}
}
// constructor
fn except_handler_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::ExceptHandlerExceptHandler> {
let body: Vec<Option<ast::Stmt>> =
get_node_list_field(vm, source_file, &object, "body", "ExceptHandler")?;
let (runtime_body, body) = runtime_stmt_list_from_values(body);
Ok(ast::ExceptHandlerExceptHandler {
node_index: Default::default(),
type_: get_node_field_opt(vm, &object, "type")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
body,
range,
runtime_body,
})
}
pub(super) fn except_handler_from_object_unvalidated_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<ast::ExceptHandler> {
ensure_excepthandler_node(vm, &object)?;
let range = excepthandler_range_from_object_unvalidated(vm, source_file, object.clone())?;
Ok(ast::ExceptHandler::ExceptHandler(
except_handler_from_object_with_range(vm, source_file, object, range)?,
))
}
impl Node for ast::ExceptHandlerExceptHandler {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -40,6 +84,7 @@ impl Node for ast::ExceptHandlerExceptHandler {
name,
body,
range,
runtime_body,
} = self;
let node = NodeAst
.into_ref_with_type(
@@ -52,8 +97,11 @@ impl Node for ast::ExceptHandlerExceptHandler {
.unwrap();
dict.set_item("name", name.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
let body = runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -63,20 +111,7 @@ impl Node for ast::ExceptHandlerExceptHandler {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
type_: get_node_field_opt(vm, &object, "type")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
body: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "body", "ExceptHandler")?,
)?,
range: range_from_object(vm, source_file, object, "ExceptHandler")?,
})
let range = range_from_object(vm, source_file, object.clone(), "ExceptHandler")?;
except_handler_from_object_with_range(vm, source_file, object, range)
}
}
File diff suppressed because it is too large. Load diff
+86 -81
View File
@@ -18,7 +18,7 @@ use rustpython_compiler_core::SourceFile;
/// - `FunctionType`: A function signature with argument and return type
/// annotations, representing the type hints of a function (e.g., `def add(x: int, y: int) -> int`).
pub(super) enum Mod {
Module(ast::ModModule),
Module(ModModule),
Interactive(ModInteractive),
Expression(ast::ModExpression),
FunctionType(ModFunctionType),
@@ -40,53 +40,66 @@ impl Node for Mod {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeModModule::static_type()) {
Self::Module(ast::ModModule::ast_from_object(vm, source_file, object)?)
} else if cls.is(pyast::NodeModInteractive::static_type()) {
Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?)
} else if cls.is(pyast::NodeModExpression::static_type()) {
Self::Expression(ast::ModExpression::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodeModFunctionType::static_type()) {
Self::FunctionType(ModFunctionType::ast_from_object(vm, source_file, object)?)
} else {
return Err(vm.new_type_error(format!(
"expected some sort of mod, but got {}",
object.repr(vm)?
)));
})
Ok(
if object.is_instance(pyast::NodeModModule::static_type().as_object(), vm)? {
Self::Module(ModModule::ast_from_object(vm, source_file, object)?)
} else if object
.is_instance(pyast::NodeModInteractive::static_type().as_object(), vm)?
{
Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?)
} else if object.is_instance(pyast::NodeModExpression::static_type().as_object(), vm)? {
Self::Expression(ast::ModExpression::ast_from_object(
vm,
source_file,
object,
)?)
} else if object
.is_instance(pyast::NodeModFunctionType::static_type().as_object(), vm)?
{
Self::FunctionType(ModFunctionType::ast_from_object(vm, source_file, object)?)
} else {
return Err(vm.new_type_error(format!(
"expected some sort of mod, but got {}",
object.repr(vm)?
)));
},
)
}
}
pub(super) struct ModModule {
pub(crate) module: ast::ModModule,
pub(crate) type_ignores: Vec<TypeIgnore>,
}
// constructor
impl Node for ast::ModModule {
impl Node for ModModule {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
module,
type_ignores,
} = self;
let ast::ModModule {
node_index: _,
body,
// type_ignores,
range,
} = self;
range: _,
runtime_body,
} = module;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModModule::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
// TODO: Improve ruff API
// ruff ignores type_ignore comments currently.
let type_ignores: Vec<TypeIgnore> = vec![];
let body = runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
dict.set_item(
"type_ignores",
type_ignores.ast_to_object(vm, source_file),
vm,
)
.unwrap();
let _ = range;
node.into()
}
@@ -95,38 +108,45 @@ impl Node for ast::ModModule {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let body: Vec<Option<ast::Stmt>> =
get_node_list_field(vm, source_file, &object, "body", "Module")?;
let (runtime_body, body) = runtime_stmt_list_from_values(body);
let type_ignores = get_node_list_field(vm, source_file, &object, "type_ignores", "Module")?;
Ok(Self {
node_index: Default::default(),
body: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "body", "Module")?,
)?,
// type_ignores: Node::ast_from_object(
// _vm,
// get_node_field(_vm, &_object, "type_ignores", "Module")?,
// )?,
range: Default::default(),
module: ast::ModModule {
node_index: Default::default(),
body,
range: Default::default(),
runtime_body,
},
type_ignores,
})
}
}
pub(super) struct ModInteractive {
pub(crate) range: TextRange,
pub(crate) body: Vec<ast::Stmt>,
pub(crate) body: ast::Suite,
pub(crate) runtime_body: Option<Vec<Option<ast::Stmt>>>,
}
// constructor
impl Node for ModInteractive {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { body, range } = self;
let Self {
body,
range: _,
runtime_body,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModInteractive::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
let _ = range;
let body = runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
node.into()
}
@@ -135,13 +155,13 @@ impl Node for ModInteractive {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let body: Vec<Option<ast::Stmt>> =
get_node_list_field(vm, source_file, &object, "body", "Interactive")?;
let (runtime_body, body) = runtime_stmt_list_from_values(body);
Ok(Self {
body: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "body", "Interactive")?,
)?,
body,
range: Default::default(),
runtime_body,
})
}
}
@@ -152,7 +172,7 @@ impl Node for ast::ModExpression {
let Self {
node_index: _,
body,
range,
range: _,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModExpression::static_type().to_owned())
@@ -160,7 +180,6 @@ impl Node for ast::ModExpression {
let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
let _ = range;
node.into()
}
@@ -171,11 +190,7 @@ impl Node for ast::ModExpression {
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
body: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "body", "Expression")?,
)?,
body: get_required_node_field(vm, source_file, &object, "body", "Expression")?,
range: Default::default(),
})
}
@@ -184,7 +199,7 @@ impl Node for ast::ModExpression {
pub(super) struct ModFunctionType {
pub(crate) argtypes: Box<[ast::Expr]>,
pub(crate) returns: ast::Expr,
pub(crate) range: TextRange,
pub(crate) runtime_argtypes: Option<Vec<Option<ast::Expr>>>,
}
// constructor
@@ -193,21 +208,19 @@ impl Node for ModFunctionType {
let Self {
argtypes,
returns,
range,
runtime_argtypes,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModFunctionType::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item(
"argtypes",
BoxedSlice(argtypes).ast_to_object(vm, source_file),
vm,
)
.unwrap();
let argtypes = runtime_argtypes.map_or_else(
|| BoxedSlice(argtypes).ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("argtypes", argtypes, vm).unwrap();
dict.set_item("returns", returns.ast_to_object(vm, source_file), vm)
.unwrap();
let _ = range;
node.into()
}
@@ -216,21 +229,13 @@ impl Node for ModFunctionType {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let argtypes: Vec<Option<ast::Expr>> =
get_node_list_field(vm, source_file, &object, "argtypes", "FunctionType")?;
let (runtime_argtypes, argtypes) = runtime_expr_list_from_values(argtypes);
Ok(Self {
argtypes: {
let argtypes: BoxedSlice<_> = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "argtypes", "FunctionType")?,
)?;
argtypes.0
},
returns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "returns", "FunctionType")?,
)?,
range: Default::default(),
argtypes: argtypes.into_boxed_slice(),
returns: get_required_node_field(vm, source_file, &object, "returns", "FunctionType")?,
runtime_argtypes,
})
}
}
+48 -13
View File
@@ -1,5 +1,6 @@
use crate::{PyObjectRef, PyResult, VirtualMachine};
use crate::{PyObjectRef, PyResult, VirtualMachine, builtins::PyList};
use rustpython_compiler_core::SourceFile;
use thin_vec::ThinVec;
pub(crate) trait Node: Sized {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef;
@@ -31,14 +32,52 @@ impl<T: Node> Node for Vec<T> {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
// Recursion guard for each element: prevents stack overflow when a
// sequence element transitively references the sequence itself
// (e.g. `l = ast.List(...); l.elts = [l]`). See issue #4862.
vm.extract_elements_with(&object, |obj| {
vm.with_recursion("while traversing AST node", || {
Node::ast_from_object(vm, source_file, obj)
})
})
let list = object.downcast_ref::<PyList>().ok_or_else(|| {
vm.new_type_error(format!(
"AST list field must be a list, not a {}",
object.class().name()
))
})?;
let len = list.borrow_vec().len();
let mut result = Self::with_capacity(len);
for i in 0..len {
let item = {
let items = list.borrow_vec();
if items.len() != len {
return Err(
vm.new_runtime_error("AST list field changed size during iteration")
);
}
items[i].clone()
};
result.push(vm.with_recursion("while traversing AST node", || {
Node::ast_from_object(vm, source_file, item)
})?);
if list.borrow_vec().len() != len {
return Err(vm.new_runtime_error("AST list field changed size during iteration"));
}
}
Ok(result)
}
}
impl<T: Node> Node for ThinVec<T> {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
vm.ctx
.new_list(
self.into_iter()
.map(|node| node.ast_to_object(vm, source_file))
.collect(),
)
.into()
}
fn ast_from_object(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Vec::<T>::ast_from_object(vm, source_file, object).map(Into::into)
}
}
@@ -52,10 +91,6 @@ impl<T: Node> Node for Box<T> {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
// Recursion guard: every descent through a Box<AstNode> increments the
// VM's recursion depth so cyclic or pathologically deep ASTs raise
// RecursionError instead of overflowing the native stack.
// See issue #4862.
vm.with_recursion("while traversing AST node", || {
T::ast_from_object(vm, source_file, object).map(Self::new)
})
+90 -86
View File
@@ -16,17 +16,18 @@ impl Node for ast::BoolOp {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeBoolOpAnd::static_type()) {
Self::And
} else if cls.is(pyast::NodeBoolOpOr::static_type()) {
Self::Or
} else {
return Err(vm.new_type_error(format!(
"expected some sort of boolop, but got {}",
object.repr(vm)?
)));
})
Ok(
if is_node_instance(vm, &object, pyast::NodeBoolOpAnd::static_type())? {
Self::And
} else if is_node_instance(vm, &object, pyast::NodeBoolOpOr::static_type())? {
Self::Or
} else {
return Err(vm.new_type_error(format!(
"expected some sort of boolop, but got {}",
object.repr(vm)?
)));
},
)
}
}
@@ -56,39 +57,40 @@ impl Node for ast::Operator {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeOperatorAdd::static_type()) {
Self::Add
} else if cls.is(pyast::NodeOperatorSub::static_type()) {
Self::Sub
} else if cls.is(pyast::NodeOperatorMult::static_type()) {
Self::Mult
} else if cls.is(pyast::NodeOperatorMatMult::static_type()) {
Self::MatMult
} else if cls.is(pyast::NodeOperatorDiv::static_type()) {
Self::Div
} else if cls.is(pyast::NodeOperatorMod::static_type()) {
Self::Mod
} else if cls.is(pyast::NodeOperatorPow::static_type()) {
Self::Pow
} else if cls.is(pyast::NodeOperatorLShift::static_type()) {
Self::LShift
} else if cls.is(pyast::NodeOperatorRShift::static_type()) {
Self::RShift
} else if cls.is(pyast::NodeOperatorBitOr::static_type()) {
Self::BitOr
} else if cls.is(pyast::NodeOperatorBitXor::static_type()) {
Self::BitXor
} else if cls.is(pyast::NodeOperatorBitAnd::static_type()) {
Self::BitAnd
} else if cls.is(pyast::NodeOperatorFloorDiv::static_type()) {
Self::FloorDiv
} else {
return Err(vm.new_type_error(format!(
"expected some sort of operator, but got {}",
object.repr(vm)?
)));
})
Ok(
if is_node_instance(vm, &object, pyast::NodeOperatorAdd::static_type())? {
Self::Add
} else if is_node_instance(vm, &object, pyast::NodeOperatorSub::static_type())? {
Self::Sub
} else if is_node_instance(vm, &object, pyast::NodeOperatorMult::static_type())? {
Self::Mult
} else if is_node_instance(vm, &object, pyast::NodeOperatorMatMult::static_type())? {
Self::MatMult
} else if is_node_instance(vm, &object, pyast::NodeOperatorDiv::static_type())? {
Self::Div
} else if is_node_instance(vm, &object, pyast::NodeOperatorMod::static_type())? {
Self::Mod
} else if is_node_instance(vm, &object, pyast::NodeOperatorPow::static_type())? {
Self::Pow
} else if is_node_instance(vm, &object, pyast::NodeOperatorLShift::static_type())? {
Self::LShift
} else if is_node_instance(vm, &object, pyast::NodeOperatorRShift::static_type())? {
Self::RShift
} else if is_node_instance(vm, &object, pyast::NodeOperatorBitOr::static_type())? {
Self::BitOr
} else if is_node_instance(vm, &object, pyast::NodeOperatorBitXor::static_type())? {
Self::BitXor
} else if is_node_instance(vm, &object, pyast::NodeOperatorBitAnd::static_type())? {
Self::BitAnd
} else if is_node_instance(vm, &object, pyast::NodeOperatorFloorDiv::static_type())? {
Self::FloorDiv
} else {
return Err(vm.new_type_error(format!(
"expected some sort of operator, but got {}",
object.repr(vm)?
)));
},
)
}
}
@@ -109,21 +111,22 @@ impl Node for ast::UnaryOp {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeUnaryOpInvert::static_type()) {
Self::Invert
} else if cls.is(pyast::NodeUnaryOpNot::static_type()) {
Self::Not
} else if cls.is(pyast::NodeUnaryOpUAdd::static_type()) {
Self::UAdd
} else if cls.is(pyast::NodeUnaryOpUSub::static_type()) {
Self::USub
} else {
return Err(vm.new_type_error(format!(
"expected some sort of unaryop, but got {}",
object.repr(vm)?
)));
})
Ok(
if is_node_instance(vm, &object, pyast::NodeUnaryOpInvert::static_type())? {
Self::Invert
} else if is_node_instance(vm, &object, pyast::NodeUnaryOpNot::static_type())? {
Self::Not
} else if is_node_instance(vm, &object, pyast::NodeUnaryOpUAdd::static_type())? {
Self::UAdd
} else if is_node_instance(vm, &object, pyast::NodeUnaryOpUSub::static_type())? {
Self::USub
} else {
return Err(vm.new_type_error(format!(
"expected some sort of unaryop, but got {}",
object.repr(vm)?
)));
},
)
}
}
@@ -150,32 +153,33 @@ impl Node for ast::CmpOp {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeCmpOpEq::static_type()) {
Self::Eq
} else if cls.is(pyast::NodeCmpOpNotEq::static_type()) {
Self::NotEq
} else if cls.is(pyast::NodeCmpOpLt::static_type()) {
Self::Lt
} else if cls.is(pyast::NodeCmpOpLtE::static_type()) {
Self::LtE
} else if cls.is(pyast::NodeCmpOpGt::static_type()) {
Self::Gt
} else if cls.is(pyast::NodeCmpOpGtE::static_type()) {
Self::GtE
} else if cls.is(pyast::NodeCmpOpIs::static_type()) {
Self::Is
} else if cls.is(pyast::NodeCmpOpIsNot::static_type()) {
Self::IsNot
} else if cls.is(pyast::NodeCmpOpIn::static_type()) {
Self::In
} else if cls.is(pyast::NodeCmpOpNotIn::static_type()) {
Self::NotIn
} else {
return Err(vm.new_type_error(format!(
"expected some sort of cmpop, but got {}",
object.repr(vm)?
)));
})
Ok(
if is_node_instance(vm, &object, pyast::NodeCmpOpEq::static_type())? {
Self::Eq
} else if is_node_instance(vm, &object, pyast::NodeCmpOpNotEq::static_type())? {
Self::NotEq
} else if is_node_instance(vm, &object, pyast::NodeCmpOpLt::static_type())? {
Self::Lt
} else if is_node_instance(vm, &object, pyast::NodeCmpOpLtE::static_type())? {
Self::LtE
} else if is_node_instance(vm, &object, pyast::NodeCmpOpGt::static_type())? {
Self::Gt
} else if is_node_instance(vm, &object, pyast::NodeCmpOpGtE::static_type())? {
Self::GtE
} else if is_node_instance(vm, &object, pyast::NodeCmpOpIs::static_type())? {
Self::Is
} else if is_node_instance(vm, &object, pyast::NodeCmpOpIsNot::static_type())? {
Self::IsNot
} else if is_node_instance(vm, &object, pyast::NodeCmpOpIn::static_type())? {
Self::In
} else if is_node_instance(vm, &object, pyast::NodeCmpOpNotIn::static_type())? {
Self::NotIn
} else {
return Err(vm.new_type_error(format!(
"expected some sort of cmpop, but got {}",
object.repr(vm)?
)));
},
)
}
}
+14 -15
View File
@@ -11,14 +11,12 @@ impl Node for ast::ConversionFlag {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
// Python's AST uses ASCII codes: 's', 'r', 'a', -1=None
// Note: 255 is -1i8 as u8 (ruff's ConversionFlag::None)
match i32::try_from_object(vm, object)? {
-1 | 255 => Ok(Self::None),
match node_object_to_i32(vm, object)? {
-1 => Ok(Self::None),
x if x == b's' as i32 => Ok(Self::Str),
x if x == b'r' as i32 => Ok(Self::Repr),
x if x == b'a' as i32 => Ok(Self::Ascii),
_ => Err(vm.new_value_error("invalid conversion flag")),
x => Err(vm.new_system_error(format!("Unrecognized conversion character {x}"))),
}
}
}
@@ -34,10 +32,13 @@ impl Node for ast::name::Name {
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
match object.downcast::<PyStr>() {
Ok(name) => Ok(Self::new(name)),
Err(_) => Err(vm.new_value_error("expected str for name")),
if !object.class().is(vm.ctx.types.str_type) {
return Err(vm.new_type_error("AST identifier must be of type str"));
}
object
.downcast::<PyStr>()
.map(Self::new)
.map_err(|_| vm.new_type_error("AST identifier must be of type str"))
}
}
@@ -89,11 +90,7 @@ impl Node for ast::Alias {
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
name: Node::ast_from_object(
vm,
source_file,
get_node_field_required(vm, &object, "name", "alias")?,
)?,
name: get_required_identifier_field(vm, source_file, &object, "name", "alias")?,
asname: get_node_field_opt(vm, &object, "asname")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
@@ -137,10 +134,12 @@ impl Node for ast::WithItem {
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
context_expr: Node::ast_from_object(
context_expr: get_required_node_field(
vm,
source_file,
get_node_field_required(vm, &object, "context_expr", "withitem")?,
&object,
"context_expr",
"withitem",
)?,
optional_vars: get_node_field_opt(vm, &object, "optional_vars")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
+161 -75
View File
@@ -11,10 +11,12 @@ impl Node for ast::Parameters {
vararg,
kwonlyargs,
kwarg,
range,
range: _,
runtime_defaults,
} = self;
let (posonlyargs, args, defaults) =
let (posonlyargs, args, mut defaults) =
extract_positional_parameter_defaults(posonlyargs, args);
defaults.runtime_defaults = runtime_defaults;
let (kwonlyargs, kw_defaults) = extract_keyword_parameter_defaults(kwonlyargs);
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned())
@@ -40,9 +42,12 @@ impl Node for ast::Parameters {
.unwrap();
dict.set_item("kwarg", kwarg.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("defaults", defaults.ast_to_object(vm, source_file), vm)
.unwrap();
let _ = range;
let runtime_defaults = defaults.runtime_defaults.take();
let defaults = runtime_defaults.map_or_else(
|| defaults.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("defaults", defaults, vm).unwrap();
node.into()
}
@@ -51,33 +56,44 @@ impl Node for ast::Parameters {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let kwonlyargs = Node::ast_from_object(
let posonlyargs = PositionalParameters::ast_from_field(
vm,
source_file,
get_node_field(vm, &object, "kwonlyargs", "arguments")?,
&object,
"posonlyargs",
"arguments",
)?;
let kw_defaults = Node::ast_from_object(
let args =
PositionalParameters::ast_from_field(vm, source_file, &object, "args", "arguments")?;
let vararg = get_node_field_opt(vm, &object, "vararg")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?;
let kwonlyargs =
KeywordParameters::ast_from_field(vm, source_file, &object, "kwonlyargs", "arguments")?;
let kw_defaults = ParameterDefaults::ast_from_field(
vm,
source_file,
get_node_field(vm, &object, "kw_defaults", "arguments")?,
&object,
"kw_defaults",
"arguments",
)?;
let kwarg = get_node_field_opt(vm, &object, "kwarg")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?;
let defaults = ParameterDefaults::ast_from_field_preserve_none(
vm,
source_file,
&object,
"defaults",
"arguments",
)?;
let kwonlyargs = merge_keyword_parameter_defaults(vm, kwonlyargs, kw_defaults)?;
let posonlyargs = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "posonlyargs", "arguments")?,
)?;
let args = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "args", "arguments")?,
)?;
let defaults = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "defaults", "arguments")?,
)?;
let ParameterDefaults {
runtime_defaults,
defaults,
_range: _,
} = defaults;
let kwonlyargs = merge_keyword_parameter_defaults(vm, kwonlyargs, kw_defaults)?;
let (posonlyargs, args) =
merge_positional_parameter_defaults(vm, posonlyargs, args, defaults)?;
@@ -85,14 +101,11 @@ impl Node for ast::Parameters {
node_index: Default::default(),
posonlyargs,
args,
vararg: get_node_field_opt(vm, &object, "vararg")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
vararg,
kwonlyargs,
kwarg: get_node_field_opt(vm, &object, "kwarg")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
kwarg,
range: Default::default(),
runtime_defaults,
})
}
@@ -103,62 +116,68 @@ impl Node for ast::Parameters {
// product
impl Node for ast::Parameter {
fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
node_index: _,
name,
annotation,
// type_comment,
range,
runtime_type_comment,
runtime_type_comment_bytes,
} = self;
// ruff covers the ** in range but python expects it to start at the ident
let range = TextRange::new(name.start(), range.end());
let node = NodeAst
.into_ref_with_type(_vm, pyast::NodeArg::static_type().to_owned())
.into_ref_with_type(vm, pyast::NodeArg::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("arg", name.ast_to_object(_vm, source_file), _vm)
dict.set_item("arg", name.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item(
"annotation",
annotation.ast_to_object(_vm, source_file),
_vm,
dict.set_item("annotation", annotation.ast_to_object(vm, source_file), vm)
.unwrap();
let type_comment = super::constant::runtime_string_object(
vm,
runtime_type_comment,
runtime_type_comment_bytes,
)
.unwrap();
// Ruff AST doesn't track type_comment, so always set to None
dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap();
node_add_location(&dict, range, _vm, source_file);
.unwrap_or_else(|| vm.ctx.none());
dict.set_item("type_comment", type_comment, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
fn ast_from_object(
_vm: &VirtualMachine,
vm: &VirtualMachine,
source_file: &SourceFile,
_object: PyObjectRef,
) -> PyResult<Self> {
let name = get_required_identifier_field(vm, source_file, &_object, "arg", "arg")?;
let annotation = get_node_field_opt(vm, &_object, "annotation")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?;
let type_comment = get_ast_string_field_opt(vm, &_object, "type_comment")?;
let (runtime_type_comment, runtime_type_comment_bytes) = type_comment
.map_or((None, None), |type_comment| {
super::constant::runtime_string_from_pyobject(vm, type_comment)
});
let range = range_from_object(vm, source_file, _object, "arg")?;
Ok(Self {
node_index: Default::default(),
name: Node::ast_from_object(
_vm,
source_file,
get_node_field_required(_vm, &_object, "arg", "arg")?,
)?,
annotation: get_node_field_opt(_vm, &_object, "annotation")?
.map(|obj| Node::ast_from_object(_vm, source_file, obj))
.transpose()?,
// type_comment: get_node_field_opt(_vm, &_object, "type_comment")?
// .map(|obj| Node::ast_from_object(_vm, obj))
// .transpose()?,
range: range_from_object(_vm, source_file, _object, "arg")?,
name,
annotation,
range,
runtime_type_comment,
runtime_type_comment_bytes,
})
}
}
// product
impl Node for ast::Keyword {
fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
node_index: _,
arg,
@@ -166,32 +185,28 @@ impl Node for ast::Keyword {
range: _range,
} = self;
let node = NodeAst
.into_ref_with_type(_vm, pyast::NodeKeyword::static_type().to_owned())
.into_ref_with_type(vm, pyast::NodeKeyword::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("arg", arg.ast_to_object(_vm, source_file), _vm)
dict.set_item("arg", arg.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("value", value.ast_to_object(_vm, source_file), _vm)
dict.set_item("value", value.ast_to_object(vm, source_file), vm)
.unwrap();
node_add_location(&dict, _range, _vm, source_file);
node_add_location(&dict, _range, vm, source_file);
node.into()
}
fn ast_from_object(
_vm: &VirtualMachine,
vm: &VirtualMachine,
source_file: &SourceFile,
_object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
arg: get_node_field_opt(_vm, &_object, "arg")?
.map(|obj| Node::ast_from_object(_vm, source_file, obj))
arg: get_node_field_opt(vm, &_object, "arg")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
value: Node::ast_from_object(
_vm,
source_file,
get_node_field_required(_vm, &_object, "value", "keyword")?,
)?,
range: range_from_object(_vm, source_file, _object, "keyword")?,
value: get_required_node_field(vm, source_file, &_object, "value", "keyword")?,
range: range_from_object(vm, source_file, _object, "keyword")?,
})
}
}
@@ -201,6 +216,21 @@ struct PositionalParameters {
pub args: Box<[ast::Parameter]>,
}
impl PositionalParameters {
fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
Ok(Self {
args: get_node_boxed_slice_field(vm, source_file, object, field, typ)?,
_range: TextRange::default(),
})
}
}
impl Node for PositionalParameters {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.args).ast_to_object(vm, source_file)
@@ -214,7 +244,7 @@ impl Node for PositionalParameters {
let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self {
args: args.0,
_range: TextRange::default(), // TODO
_range: TextRange::default(),
})
}
}
@@ -224,6 +254,21 @@ struct KeywordParameters {
pub keywords: Box<[ast::Parameter]>,
}
impl KeywordParameters {
fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
Ok(Self {
keywords: get_node_boxed_slice_field(vm, source_file, object, field, typ)?,
_range: TextRange::default(),
})
}
}
impl Node for KeywordParameters {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.keywords).ast_to_object(vm, source_file)
@@ -237,16 +282,55 @@ impl Node for KeywordParameters {
let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self {
keywords: keywords.0,
_range: TextRange::default(), // TODO
_range: TextRange::default(),
})
}
}
struct ParameterDefaults {
pub _range: TextRange, // TODO: Use this
runtime_defaults: Option<Vec<Option<ast::Expr>>>,
defaults: Box<[Option<Box<ast::Expr>>]>,
}
impl ParameterDefaults {
fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
Ok(Self {
defaults: get_node_boxed_slice_field(vm, source_file, object, field, typ)?,
runtime_defaults: None,
_range: TextRange::default(),
})
}
fn ast_from_field_preserve_none(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
let defaults: Vec<Option<Box<ast::Expr>>> =
get_node_list_field(vm, source_file, object, field, typ)?;
let runtime_defaults = defaults.iter().any(Option::is_none).then(|| {
defaults
.iter()
.map(|default| default.as_deref().cloned())
.collect()
});
Ok(Self {
defaults: defaults.into_boxed_slice(),
runtime_defaults,
_range: TextRange::default(),
})
}
}
impl Node for ParameterDefaults {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.defaults).ast_to_object(vm, source_file)
@@ -260,7 +344,8 @@ impl Node for ParameterDefaults {
let defaults: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self {
defaults: defaults.0,
_range: TextRange::default(), // TODO
runtime_defaults: None,
_range: TextRange::default(),
})
}
}
@@ -287,6 +372,7 @@ fn extract_positional_parameter_defaults(
.map(|item| item.range())
.reduce(|acc, next| acc.cover(next))
.unwrap_or_default(),
runtime_defaults: None,
defaults: defaults.into_boxed_slice(),
};
@@ -325,14 +411,13 @@ fn merge_positional_parameter_defaults(
vm: &VirtualMachine,
posonlyargs: PositionalParameters,
args: PositionalParameters,
defaults: ParameterDefaults,
defaults: Box<[Option<Box<ast::Expr>>]>,
) -> PyResult<(
Vec<ast::ParameterWithDefault>,
Vec<ast::ParameterWithDefault>,
)> {
let posonlyargs = posonlyargs.args;
let args = args.args;
let defaults = defaults.defaults;
let mut posonlyargs: Vec<_> = <Box<[_]> as IntoIterator>::into_iter(posonlyargs)
.map(|parameter| ast::ParameterWithDefault {
@@ -383,6 +468,7 @@ fn extract_keyword_parameter_defaults(
.map(|item| item.range())
.reduce(|acc, next| acc.cover(next))
.unwrap_or_default(),
runtime_defaults: None,
defaults: defaults.into_boxed_slice(),
};
@@ -422,5 +508,5 @@ fn merge_keyword_parameter_defaults(
default,
range: Default::default(),
})
.collect())
.collect::<Vec<_>>())
}
+386 -189
View File
@@ -10,6 +10,7 @@ impl Node for ast::MatchCase {
guard,
body,
range: _,
runtime_body,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeMatchCase::static_type().to_owned())
@@ -19,8 +20,11 @@ impl Node for ast::MatchCase {
.unwrap();
dict.set_item("guard", guard.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap();
let body = runtime_body.map_or_else(
|| body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
node.into()
}
@@ -29,22 +33,18 @@ impl Node for ast::MatchCase {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let body: Vec<Option<ast::Stmt>> =
get_node_list_field(vm, source_file, &object, "body", "match_case")?;
let (runtime_body, body) = runtime_stmt_list_from_values(body);
Ok(Self {
node_index: Default::default(),
pattern: Node::ast_from_object(
vm,
source_file,
get_node_field_required(vm, &object, "pattern", "match_case")?,
)?,
pattern: get_required_node_field(vm, source_file, &object, "pattern", "match_case")?,
guard: get_node_field_opt(vm, &object, "guard")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
body: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "body", "match_case")?,
)?,
body,
range: Default::default(),
runtime_body,
})
}
}
@@ -68,64 +68,152 @@ impl Node for ast::Pattern {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodePatternMatchValue::static_type()) {
Self::MatchValue(ast::PatternMatchValue::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchSingleton::static_type()) {
Self::MatchSingleton(ast::PatternMatchSingleton::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchSequence::static_type()) {
Self::MatchSequence(ast::PatternMatchSequence::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchMapping::static_type()) {
Self::MatchMapping(ast::PatternMatchMapping::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchClass::static_type()) {
Self::MatchClass(ast::PatternMatchClass::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchStar::static_type()) {
Self::MatchStar(ast::PatternMatchStar::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchAs::static_type()) {
Self::MatchAs(ast::PatternMatchAs::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodePatternMatchOr::static_type()) {
Self::MatchOr(ast::PatternMatchOr::ast_from_object(
vm,
source_file,
object,
)?)
if vm.is_none(&object) {
return Err(vm.new_type_error(format!(
"expected some sort of pattern, but got {}",
object.repr(vm)?
)));
}
enum PatternKind {
Value,
Singleton,
Sequence,
Mapping,
Class,
Star,
As,
Or,
}
let kind = if is_node_instance(vm, &object, pyast::NodePatternMatchValue::static_type())? {
PatternKind::Value
} else if is_node_instance(vm, &object, pyast::NodePatternMatchSingleton::static_type())? {
PatternKind::Singleton
} else if is_node_instance(vm, &object, pyast::NodePatternMatchSequence::static_type())? {
PatternKind::Sequence
} else if is_node_instance(vm, &object, pyast::NodePatternMatchMapping::static_type())? {
PatternKind::Mapping
} else if is_node_instance(vm, &object, pyast::NodePatternMatchClass::static_type())? {
PatternKind::Class
} else if is_node_instance(vm, &object, pyast::NodePatternMatchStar::static_type())? {
PatternKind::Star
} else if is_node_instance(vm, &object, pyast::NodePatternMatchAs::static_type())? {
PatternKind::As
} else if is_node_instance(vm, &object, pyast::NodePatternMatchOr::static_type())? {
PatternKind::Or
} else {
return Err(vm.new_type_error(format!(
"expected some sort of pattern, but got {}",
object.repr(vm)?
)));
};
let range = pattern_range_from_object(vm, source_file, object.clone())?;
Ok(match kind {
PatternKind::Value => Self::MatchValue(pattern_match_value_from_object_with_range(
vm,
source_file,
object,
range,
)?),
PatternKind::Singleton => Self::MatchSingleton(
pattern_match_singleton_from_object_with_range(vm, source_file, object, range)?,
),
PatternKind::Sequence => Self::MatchSequence(
pattern_match_sequence_from_object_with_range(vm, source_file, object, range)?,
),
PatternKind::Mapping => Self::MatchMapping(
pattern_match_mapping_from_object_with_range(vm, source_file, object, range)?,
),
PatternKind::Class => Self::MatchClass(pattern_match_class_from_object_with_range(
vm,
source_file,
object,
range,
)?),
PatternKind::Star => Self::MatchStar(pattern_match_star_from_object_with_range(
vm,
source_file,
object,
range,
)?),
PatternKind::As => Self::MatchAs(pattern_match_as_from_object_with_range(
vm,
source_file,
object,
range,
)?),
PatternKind::Or => Self::MatchOr(pattern_match_or_from_object_with_range(
vm,
source_file,
object,
range,
)?),
})
}
}
fn null_pattern_placeholder(range: TextRange) -> ast::Pattern {
ast::Pattern::MatchAs(ast::PatternMatchAs {
node_index: Default::default(),
range,
pattern: None,
name: None,
})
}
fn lower_nullable_patterns(values: &[Option<ast::Pattern>], range: TextRange) -> Vec<ast::Pattern> {
values
.iter()
.cloned()
.map(|value| value.unwrap_or_else(|| null_pattern_placeholder(range)))
.collect()
}
fn null_expr_placeholder(range: TextRange) -> ast::Expr {
ast::Expr::NoneLiteral(ast::ExprNoneLiteral {
node_index: Default::default(),
range,
})
}
fn lower_nullable_exprs(values: &[Option<ast::Expr>], range: TextRange) -> Vec<ast::Expr> {
values
.iter()
.cloned()
.map(|value| value.unwrap_or_else(|| null_expr_placeholder(range)))
.collect()
}
type RuntimePatternList = Option<Vec<Option<ast::Pattern>>>;
type PatternListField = (RuntimePatternList, Vec<ast::Pattern>);
fn pattern_list_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
range: TextRange,
) -> PyResult<PatternListField> {
let values: Vec<Option<ast::Pattern>> =
get_node_list_field(vm, source_file, object, field, typ)?;
let runtime_patterns = values.iter().any(Option::is_none).then(|| values.clone());
Ok((runtime_patterns, lower_nullable_patterns(&values, range)))
}
// constructor
fn pattern_match_value_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchValue> {
Ok(ast::PatternMatchValue {
node_index: Default::default(),
value: get_required_node_field(vm, source_file, &object, "value", "MatchValue")?,
range,
})
}
impl Node for ast::PatternMatchValue {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -148,19 +236,29 @@ impl Node for ast::PatternMatchValue {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "MatchValue")?,
)?,
range: range_from_object(vm, source_file, object, "MatchValue")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchValue")?;
pattern_match_value_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn pattern_match_singleton_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchSingleton> {
Ok(ast::PatternMatchSingleton {
node_index: Default::default(),
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "MatchSingleton")?,
)?,
range,
})
}
impl Node for ast::PatternMatchSingleton {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -186,15 +284,8 @@ impl Node for ast::PatternMatchSingleton {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "MatchSingleton")?,
)?,
range: range_from_object(vm, source_file, object, "MatchSingleton")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchSingleton")?;
pattern_match_singleton_from_object_with_range(vm, source_file, object, range)
}
}
@@ -219,21 +310,35 @@ impl Node for ast::Singleton {
} else if object.is(&vm.ctx.false_value) {
Ok(Self::False)
} else {
Err(vm.new_value_error(format!(
"Expected None, True, or False, got {:?}",
object.class().name()
)))
Err(vm.new_value_error("MatchSingleton can only contain True, False and None"))
}
}
}
// constructor
fn pattern_match_sequence_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchSequence> {
let (runtime_patterns, patterns) =
pattern_list_from_field(vm, source_file, &object, "patterns", "MatchSequence", range)?;
Ok(ast::PatternMatchSequence {
node_index: Default::default(),
patterns: patterns.to_vec(),
range,
runtime_patterns,
})
}
impl Node for ast::PatternMatchSequence {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
node_index: _,
patterns,
range,
runtime_patterns,
} = self;
let node = NodeAst
.into_ref_with_type(
@@ -242,8 +347,11 @@ impl Node for ast::PatternMatchSequence {
)
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm)
.unwrap();
let patterns = runtime_patterns.map_or_else(
|| patterns.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("patterns", patterns, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -253,19 +361,40 @@ impl Node for ast::PatternMatchSequence {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
patterns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchSequence")?,
)?,
range: range_from_object(vm, source_file, object, "MatchSequence")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchSequence")?;
pattern_match_sequence_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn pattern_match_mapping_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchMapping> {
let keys: Vec<Option<ast::Expr>> =
get_node_list_field(vm, source_file, &object, "keys", "MatchMapping")?;
let patterns: Vec<Option<ast::Pattern>> =
get_node_list_field(vm, source_file, &object, "patterns", "MatchMapping")?;
let runtime_keys = keys.iter().any(Option::is_none).then(|| keys.clone());
let runtime_patterns = patterns
.iter()
.any(Option::is_none)
.then(|| patterns.clone());
Ok(ast::PatternMatchMapping {
node_index: Default::default(),
keys: lower_nullable_exprs(&keys, range),
patterns: lower_nullable_patterns(&patterns, range),
rest: get_node_field_opt(vm, &object, "rest")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
runtime_keys,
runtime_patterns,
})
}
impl Node for ast::PatternMatchMapping {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -274,15 +403,23 @@ impl Node for ast::PatternMatchMapping {
patterns,
rest,
range,
runtime_keys,
runtime_patterns,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchMapping::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("keys", keys.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm)
.unwrap();
let keys = runtime_keys.map_or_else(
|| keys.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("keys", keys, vm).unwrap();
let patterns = runtime_patterns.map_or_else(
|| patterns.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("patterns", patterns, vm).unwrap();
dict.set_item("rest", rest.ast_to_object(vm, source_file), vm)
.unwrap();
node_add_location(&dict, range, vm, source_file);
@@ -294,27 +431,57 @@ impl Node for ast::PatternMatchMapping {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
keys: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "keys", "MatchMapping")?,
)?,
patterns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchMapping")?,
)?,
rest: get_node_field_opt(vm, &object, "rest")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: range_from_object(vm, source_file, object, "MatchMapping")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchMapping")?;
pattern_match_mapping_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn pattern_match_class_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchClass> {
let cls = get_required_node_field(vm, source_file, &object, "cls", "MatchClass")?;
let patterns: Vec<Option<ast::Pattern>> =
get_node_list_field(vm, source_file, &object, "patterns", "MatchClass")?;
let kwd_attrs = PatternMatchClassKeywordAttributes::ast_from_field(
vm,
source_file,
&object,
"kwd_attrs",
"MatchClass",
)?;
let kwd_patterns: Vec<Option<ast::Pattern>> =
get_node_list_field(vm, source_file, &object, "kwd_patterns", "MatchClass")?;
let has_runtime_shape = kwd_attrs.0.len() != kwd_patterns.len()
|| patterns.iter().any(Option::is_none)
|| kwd_patterns.iter().any(Option::is_none);
let runtime_patterns = has_runtime_shape.then(|| patterns.clone());
let runtime_kwd_attrs = has_runtime_shape.then(|| kwd_attrs.0.clone());
let runtime_kwd_patterns = has_runtime_shape.then(|| kwd_patterns.clone());
let patterns = PatternMatchClassPatterns(lower_nullable_patterns(&patterns, range));
let kwd_patterns =
PatternMatchClassKeywordPatterns(lower_nullable_patterns(&kwd_patterns, range));
let (patterns, keywords) = merge_pattern_match_class(patterns, kwd_attrs, kwd_patterns);
Ok(ast::PatternMatchClass {
node_index: Default::default(),
cls,
range,
arguments: ast::PatternArguments {
node_index: Default::default(),
range: Default::default(),
patterns,
keywords,
},
runtime_patterns,
runtime_kwd_attrs,
runtime_kwd_patterns,
})
}
impl Node for ast::PatternMatchClass {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -322,24 +489,36 @@ impl Node for ast::PatternMatchClass {
cls,
arguments,
range,
runtime_patterns,
runtime_kwd_attrs,
runtime_kwd_patterns,
} = self;
let (patterns, kwd_attrs, kwd_patterns) = split_pattern_match_class(arguments);
let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchClass::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("cls", cls.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("kwd_attrs", kwd_attrs.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item(
"kwd_patterns",
kwd_patterns.ast_to_object(vm, source_file),
vm,
)
.unwrap();
let (patterns, kwd_attrs, kwd_patterns) =
if let (Some(patterns), Some(kwd_attrs), Some(kwd_patterns)) =
(runtime_patterns, runtime_kwd_attrs, runtime_kwd_patterns)
{
(
patterns.ast_to_object(vm, source_file),
kwd_attrs.ast_to_object(vm, source_file),
kwd_patterns.ast_to_object(vm, source_file),
)
} else {
let (patterns, kwd_attrs, kwd_patterns) = split_pattern_match_class(arguments);
(
patterns.ast_to_object(vm, source_file),
kwd_attrs.ast_to_object(vm, source_file),
kwd_patterns.ast_to_object(vm, source_file),
)
};
dict.set_item("patterns", patterns, vm).unwrap();
dict.set_item("kwd_attrs", kwd_attrs, vm).unwrap();
dict.set_item("kwd_patterns", kwd_patterns, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -349,41 +528,8 @@ impl Node for ast::PatternMatchClass {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let patterns = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchClass")?,
)?;
let kwd_attrs: PatternMatchClassKeywordAttributes = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "kwd_attrs", "MatchClass")?,
)?;
let kwd_patterns: PatternMatchClassKeywordPatterns = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "kwd_patterns", "MatchClass")?,
)?;
if kwd_attrs.0.len() != kwd_patterns.0.len() {
return Err(vm.new_value_error("MatchClass has mismatched kwd_attrs and kwd_patterns"));
}
let (patterns, keywords) = merge_pattern_match_class(patterns, kwd_attrs, kwd_patterns);
Ok(Self {
node_index: Default::default(),
cls: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "cls", "MatchClass")?,
)?,
range: range_from_object(vm, source_file, object, "MatchClass")?,
arguments: ast::PatternArguments {
node_index: Default::default(),
range: Default::default(),
patterns,
keywords,
},
})
let range = range_from_object(vm, source_file, object.clone(), "MatchClass")?;
pattern_match_class_from_object_with_range(vm, source_file, object, range)
}
}
@@ -405,6 +551,24 @@ impl Node for PatternMatchClassPatterns {
struct PatternMatchClassKeywordAttributes(Vec<ast::Identifier>);
impl PatternMatchClassKeywordAttributes {
fn ast_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Self> {
Ok(Self(get_node_list_field(
vm,
source_file,
object,
field,
typ,
)?))
}
}
impl Node for PatternMatchClassKeywordAttributes {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
self.0.ast_to_object(vm, source_file)
@@ -435,6 +599,21 @@ impl Node for PatternMatchClassKeywordPatterns {
}
}
// constructor
fn pattern_match_star_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchStar> {
Ok(ast::PatternMatchStar {
node_index: Default::default(),
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for ast::PatternMatchStar {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -457,17 +636,30 @@ impl Node for ast::PatternMatchStar {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: range_from_object(vm, source_file, object, "MatchStar")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchStar")?;
pattern_match_star_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn pattern_match_as_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchAs> {
Ok(ast::PatternMatchAs {
node_index: Default::default(),
pattern: get_node_field_opt(vm, &object, "pattern")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for ast::PatternMatchAs {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -493,33 +685,45 @@ impl Node for ast::PatternMatchAs {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
pattern: get_node_field_opt(vm, &object, "pattern")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
name: get_node_field_opt(vm, &object, "name")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: range_from_object(vm, source_file, object, "MatchAs")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchAs")?;
pattern_match_as_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn pattern_match_or_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::PatternMatchOr> {
let (runtime_patterns, patterns) =
pattern_list_from_field(vm, source_file, &object, "patterns", "MatchOr", range)?;
Ok(ast::PatternMatchOr {
node_index: Default::default(),
patterns: patterns.to_vec(),
range,
runtime_patterns,
})
}
impl Node for ast::PatternMatchOr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
node_index: _,
patterns,
range,
runtime_patterns,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchOr::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm)
.unwrap();
let patterns = runtime_patterns.map_or_else(
|| patterns.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("patterns", patterns, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -528,15 +732,8 @@ impl Node for ast::PatternMatchOr {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
patterns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchOr")?,
)?,
range: range_from_object(vm, source_file, object, "MatchOr")?,
})
let range = range_from_object(vm, source_file, object.clone(), "MatchOr")?;
pattern_match_or_from_object_with_range(vm, source_file, object, range)
}
}
+46 -58
View File
@@ -3,7 +3,6 @@ use crate::builtins::{PyGenericAlias, PyTuple, PyTupleRef, PyTypeRef, make_union
use crate::common::ascii;
use crate::convert::ToPyObject;
use crate::function::FuncArgs;
use crate::types::Initializer;
macro_rules! impl_node {
(
@@ -61,6 +60,12 @@ macro_rules! impl_node {
macro_rules! impl_base_node {
// Base node without fields/attributes (e.g. NodeMod, NodeExpr)
($name:ident) => {
impl_base_node!($name, attributes: []);
};
($name:ident, attributes: [$($attr:expr),* $(,)?]) => {
impl_base_node!($name, attributes: [$($attr),*], optional_end_location: false);
};
($name:ident, attributes: [$($attr:expr),* $(,)?], optional_end_location: $optional_end_location:expr) => {
#[pyclass(flags(HAS_DICT, BASETYPE))]
impl $name {
#[pymethod]
@@ -83,9 +88,24 @@ macro_rules! impl_base_node {
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
}
class.set_attr(
identifier!(ctx, _attributes),
identifier!(ctx, _fields),
ctx.empty_tuple.clone().into(),
);
class.set_str_attr("__match_args__", ctx.empty_tuple.clone(), ctx);
class.set_attr(
identifier!(ctx, _attributes),
ctx.new_tuple(vec![
$(
ctx.new_str(ascii!($attr)).into()
),*
])
.into(),
);
if $optional_end_location {
let none = ctx.none();
class.set_str_attr("end_lineno", none.clone(), ctx);
class.set_str_attr("end_col_offset", none, ctx);
}
}
}
};
@@ -179,7 +199,11 @@ impl_node!(
#[repr(transparent)]
pub(crate) struct NodeStmt(NodeAst);
impl_base_node!(NodeStmt);
impl_base_node!(
NodeStmt,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
optional_end_location: true
);
impl_node!(
#[pyclass(module = "_ast", name = "FunctionType", base = NodeMod)]
@@ -378,7 +402,11 @@ impl_node!(
#[repr(transparent)]
pub(crate) struct NodeExpr(NodeAst);
impl_base_node!(NodeExpr);
impl_base_node!(
NodeExpr,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
optional_end_location: true
);
impl_node!(
#[pyclass(module = "_ast", name = "Continue", base = NodeStmt)]
@@ -533,12 +561,11 @@ impl_node!(
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
);
// NodeExprConstant needs custom Initializer to default kind to None
#[pyclass(module = "_ast", name = "Constant", base = NodeExpr)]
#[repr(transparent)]
pub(crate) struct NodeExprConstant(NodeExpr);
#[pyclass(flags(HAS_DICT, BASETYPE), with(Initializer))]
#[pyclass(flags(HAS_DICT, BASETYPE))]
impl NodeExprConstant {
#[extend_class]
fn extend_class_with_fields(ctx: &Context, class: &'static Py<PyType>) {
@@ -580,24 +607,6 @@ impl NodeExprConstant {
}
}
impl Initializer for NodeExprConstant {
type Args = FuncArgs;
fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
<NodeAst as Initializer>::slot_init(zelf.clone(), args, vm)?;
// kind defaults to None if not provided
let dict = zelf.as_object().dict().unwrap();
if !dict.contains_key("kind", vm) {
dict.set_item("kind", vm.ctx.none(), vm)?;
}
Ok(())
}
fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
unreachable!("slot_init is defined")
}
}
impl_node!(
#[pyclass(module = "_ast", name = "Attribute", base = NodeExpr)]
pub(crate) struct NodeExprAttribute,
@@ -841,7 +850,11 @@ impl_node!(
#[repr(transparent)]
pub(crate) struct NodeExceptHandler(NodeAst);
impl_base_node!(NodeExceptHandler);
impl_base_node!(
NodeExceptHandler,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"],
optional_end_location: true
);
impl_node!(
#[pyclass(module = "_ast", name = "comprehension", base = NodeAst)]
@@ -893,7 +906,10 @@ impl_node!(
#[repr(transparent)]
pub(crate) struct NodePattern(NodeAst);
impl_base_node!(NodePattern);
impl_base_node!(
NodePattern,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"]
);
impl_node!(
#[pyclass(module = "_ast", name = "match_case", base = NodeAst)]
@@ -967,7 +983,10 @@ impl_node!(
#[repr(transparent)]
pub(crate) struct NodeTypeParam(NodeAst);
impl_base_node!(NodeTypeParam);
impl_base_node!(
NodeTypeParam,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"]
);
impl_node!(
#[pyclass(module = "_ast", name = "TypeIgnore", base = NodeTypeIgnore)]
@@ -1681,7 +1700,6 @@ fn populate_field_types(vm: &VirtualMachine, module: &Py<PyModule>) {
let field_types_attr = vm.ctx.intern_str("_field_types");
let annotations_attr = vm.ctx.intern_str("__annotations__");
let empty_dict: PyObjectRef = vm.ctx.new_dict().into();
for &(class_name, fields) in FIELD_TYPES {
if fields.is_empty() {
@@ -1752,36 +1770,6 @@ fn populate_field_types(vm: &VirtualMachine, module: &Py<PyModule>) {
type_obj.set_attr(annotations_attr, field_types);
}
}
// Base AST classes (e.g., expr, stmt) should still expose __annotations__.
const BASE_AST_TYPES: &[&str] = &[
"mod",
"stmt",
"expr",
"expr_context",
"boolop",
"operator",
"unaryop",
"cmpop",
"excepthandler",
"pattern",
"type_ignore",
"type_param",
];
for &class_name in BASE_AST_TYPES {
let class = module
.get_attr(class_name, vm)
.unwrap_or_else(|_| panic!("AST class '{class_name}' not found in module"));
let Some(type_obj) = class.downcast_ref::<PyType>() else {
continue;
};
if type_obj.get_attr(field_types_attr).is_none() {
type_obj.set_attr(field_types_attr, empty_dict.clone());
}
if type_obj.get_attr(annotations_attr).is_none() {
type_obj.set_attr(annotations_attr, empty_dict.clone());
}
}
}
fn populate_singletons(vm: &VirtualMachine, module: &Py<PyModule>) {
+179 -97
View File
@@ -7,13 +7,10 @@ use super::{
#[pymodule]
pub(crate) mod _ast {
use crate::{
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{
PyDictRef, PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, PyUtf8StrRef,
},
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyDictRef, PySet, PyStr, PyTupleRef, PyType, PyTypeRef, PyUtf8Str},
class::{PyClassImpl, StaticType},
common::wtf8::Wtf8,
function::{FuncArgs, KwArgs, PyMethodDef, PyMethodFlags},
function::{ArgIterable, FuncArgs, KwArgs, PyMethodDef, PyMethodFlags},
stdlib::_ast::repr,
types::{Constructor, Initializer},
warn,
@@ -117,10 +114,12 @@ pub(crate) mod _ast {
let fields = cls.get_attr(vm.ctx.intern_str("_fields"));
if let Some(fields) = fields {
let fields: Vec<PyStrRef> = fields.try_to_value(vm)?;
let fields = fields.sequence_unchecked();
let numfields = fields.length(vm)?;
let mut positional: Vec<PyObjectRef> = Vec::new();
for field in fields {
if dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)?.is_some() {
for i in 0..numfields {
let field = fields.get_item(i as isize, vm)?;
if dict.get_item_opt(&*field, vm)?.is_some() {
positional.push(vm.ctx.none());
} else {
break;
@@ -136,6 +135,85 @@ pub(crate) mod _ast {
.new_tuple(vec![type_obj, vm.ctx.new_tuple(vec![]).into(), dict.into()]))
}
fn ast_replace_update_payload(
payload: &PyDictRef,
keys: Option<&PyObjectRef>,
dict: &PyDictRef,
vm: &VirtualMachine,
) -> PyResult<()> {
let Some(keys) = keys else {
return Ok(());
};
let keys = keys.sequence_unchecked();
let num_keys = keys.length(vm)?;
for i in 0..num_keys {
let key = keys.get_item(i as isize, vm)?;
if let Some(value) = dict.get_item_opt(&*key, vm)? {
payload.set_item(&*key, value, vm)?;
}
}
Ok(())
}
fn ast_replace_set_update(
expecting: &PyRef<PySet>,
iterable: Option<&PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let Some(iterable) = iterable else {
return Ok(());
};
let iterable = iterable.clone().try_into_value::<ArgIterable>(vm)?;
for item in iterable.iter(vm)? {
expecting.add(item?, vm)?;
}
Ok(())
}
fn ast_replace_set_discard(
expecting: &PyRef<PySet>,
key: &PyObject,
vm: &VirtualMachine,
) -> PyResult<bool> {
let contained = expecting
.as_object()
.sequence_unchecked()
.contains(key, vm)?;
if contained {
vm.call_method(expecting.as_object(), "discard", (key.to_owned(),))?;
}
Ok(contained)
}
fn ast_replace_set_difference_update(
expecting: &PyRef<PySet>,
iterable: Option<&PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let Some(iterable) = iterable else {
return Ok(());
};
let iterable = iterable.clone().try_into_value::<ArgIterable>(vm)?;
for item in iterable.iter(vm)? {
let item = item?;
ast_replace_set_discard(expecting, &item, vm)?;
}
Ok(())
}
fn ast_set_attr(
obj: &PyObject,
name: &PyObject,
value: impl Into<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let name = name
.to_owned()
.downcast::<PyStr>()
.map_err(|_| vm.new_type_error("attribute name must be string"))?;
obj.set_attr(&name, value, vm)
}
pub(crate) fn ast_replace(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
if !args.args.is_empty() {
return Err(vm.new_type_error("__replace__() takes no positional arguments"));
@@ -146,22 +224,13 @@ pub(crate) mod _ast {
let attributes = cls.get_attr(vm.ctx.intern_str("_attributes"));
let dict = zelf.as_object().dict();
let mut expecting: std::collections::HashSet<String> = std::collections::HashSet::new();
if let Some(fields) = fields.clone() {
let fields: Vec<PyUtf8StrRef> = fields.try_to_value(vm)?;
for field in fields {
expecting.insert(field.as_str().to_owned());
}
}
if let Some(attributes) = attributes.clone() {
let attributes: Vec<PyUtf8StrRef> = attributes.try_to_value(vm)?;
for attr in attributes {
expecting.insert(attr.as_str().to_owned());
}
}
let expecting = PySet::default().into_ref(&vm.ctx);
ast_replace_set_update(&expecting, fields.as_ref(), vm)?;
ast_replace_set_update(&expecting, attributes.as_ref(), vm)?;
for (key, _value) in &args.kwargs {
if !expecting.remove(key) {
let key_obj: PyObjectRef = vm.ctx.new_str(key.as_str()).into();
if !ast_replace_set_discard(&expecting, &key_obj, vm)? {
return Err(vm.new_type_error(format!(
"{}.__replace__ got an unexpected keyword argument '{}'.",
cls.name(),
@@ -172,16 +241,9 @@ pub(crate) mod _ast {
if let Some(dict) = dict.as_ref() {
for (key, _value) in dict.items_vec() {
if let Ok(key) = key.downcast::<PyUtf8Str>() {
expecting.remove(key.as_str());
}
}
if let Some(attributes) = attributes.clone() {
let attributes: Vec<PyUtf8StrRef> = attributes.try_to_value(vm)?;
for attr in attributes {
expecting.remove(attr.as_str());
}
ast_replace_set_discard(&expecting, &key, vm)?;
}
ast_replace_set_difference_update(&expecting, attributes.as_ref(), vm)?;
}
// Discard optional fields (T | None).
@@ -189,20 +251,18 @@ pub(crate) mod _ast {
&& let Ok(field_types) = field_types.downcast::<crate::builtins::PyDict>()
{
for (key, value) in field_types.items_vec() {
let Ok(key) = key.downcast::<PyUtf8Str>() else {
continue;
};
if value.fast_isinstance(vm.ctx.types.union_type) {
expecting.remove(key.as_str());
ast_replace_set_discard(&expecting, &key, vm)?;
}
}
}
if !expecting.is_empty() {
let mut names: Vec<String> = expecting
.into_iter()
.map(|name| format!("{name:?}"))
.collect();
let remaining = expecting.elements();
if !remaining.is_empty() {
let mut names = Vec::with_capacity(remaining.len());
for name in &remaining {
names.push(name.repr(vm)?.to_string());
}
names.sort();
let missing = names.join(", ");
let count = names.len();
@@ -217,22 +277,8 @@ pub(crate) mod _ast {
let payload = vm.ctx.new_dict();
if let Some(dict) = dict {
if let Some(fields) = fields {
let fields: Vec<PyStrRef> = fields.try_to_value(vm)?;
for field in fields {
if let Some(value) = dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)? {
payload.set_item(field.as_object(), value, vm)?;
}
}
}
if let Some(attributes) = attributes {
let attributes: Vec<PyStrRef> = attributes.try_to_value(vm)?;
for attr in attributes {
if let Some(value) = dict.get_item_opt::<Wtf8>(attr.as_wtf8(), vm)? {
payload.set_item(attr.as_object(), value, vm)?;
}
}
}
ast_replace_update_payload(&payload, fields.as_ref(), &dict, vm)?;
ast_replace_update_payload(&payload, attributes.as_ref(), &dict, vm)?;
}
for (key, value) in args.kwargs {
payload.set_item(vm.ctx.intern_str(key), value, vm)?;
@@ -327,7 +373,7 @@ pub(crate) mod _ast {
}
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
unimplemented!("use slot_new")
unreachable!("NodeAst construction is handled by slot_new")
}
}
@@ -350,52 +396,55 @@ pub(crate) mod _ast {
zelf.class().name()
))
})?;
let fields: Vec<PyUtf8StrRef> = fields.try_to_value(vm)?;
let fields_seq = fields.sequence_unchecked();
let numfields = fields_seq.length(vm)?;
let remaining_fields = PySet::default().into_ref(&vm.ctx);
ast_replace_set_update(&remaining_fields, Some(&fields), vm)?;
let n_args = args.args.len();
if n_args > fields.len() {
if n_args > numfields {
return Err(vm.new_type_error(format!(
"{} constructor takes at most {} positional argument{}",
zelf.class().name(),
fields.len(),
if fields.len() == 1 { "" } else { "s" },
numfields,
if numfields == 1 { "" } else { "s" },
)));
}
// Track which fields were set
let mut set_fields = std::collections::HashSet::new();
let mut attributes: Option<Vec<PyStrRef>> = None;
let mut attributes: Option<PyObjectRef> = None;
for (name, arg) in fields.iter().zip(args.args) {
zelf.set_attr(name, arg, vm)?;
set_fields.insert(name.as_str().to_owned());
for (i, arg) in args.args.into_iter().enumerate() {
let name = fields_seq.get_item(i as isize, vm)?;
ast_set_attr(&zelf, &name, arg, vm)?;
ast_replace_set_discard(&remaining_fields, &name, vm)?;
}
for (key, value) in args.kwargs {
if let Some(pos) = fields.iter().position(|f| f.as_bytes() == key.as_bytes())
&& pos < n_args
{
return Err(vm.new_type_error(format!(
"{} got multiple values for argument '{}'",
zelf.class().name(),
key
)));
}
if fields
.iter()
.all(|field| field.as_bytes() != key.as_bytes())
{
let attrs = if let Some(attrs) = &attributes {
attrs
let key_obj: PyObjectRef = vm.ctx.new_str(key.as_str()).into();
let contains = fields_seq.contains(&key_obj, vm)?;
if contains {
if !ast_replace_set_discard(&remaining_fields, &key_obj, vm)? {
return Err(vm.new_type_error(format!(
"{} got multiple values for argument '{}'",
zelf.class().name(),
key
)));
}
} else {
let attrs = if let Some(attributes) = &attributes {
attributes
} else {
let attrs = zelf
.class()
.get_attr(vm.ctx.intern_str("_attributes"))
.and_then(|attr| attr.try_to_value::<Vec<PyStrRef>>(vm).ok())
.unwrap_or_default();
.ok_or_else(|| {
vm.new_attribute_error(format!(
"type object '{}' has no attribute '_attributes'",
zelf.class().name()
))
})?;
attributes = Some(attrs);
attributes.as_ref().unwrap()
};
if attrs.iter().all(|attr| attr.as_bytes() != key.as_bytes()) {
if !attrs.sequence_unchecked().contains(&key_obj, vm)? {
let message = vm.ctx.new_str(format!(
"{}.__init__ got an unexpected keyword argument '{}'. \
Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.",
@@ -412,7 +461,6 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
}
}
set_fields.insert(key.clone());
zelf.set_attr(vm.ctx.intern_str(key), value, vm)?;
}
@@ -425,17 +473,14 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
let expr_ctx_type: PyObjectRef =
super::super::pyast::NodeExprContext::make_static_type().into();
for field in &fields {
if set_fields.contains(field.as_str()) {
continue;
}
if let Some(ftype) = ft_dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)? {
for field in remaining_fields.elements() {
if let Some(ftype) = ft_dict.get_item_opt(&*field, vm)? {
if ftype.fast_isinstance(vm.ctx.types.union_type) {
// Optional field (T | None) — no default
} else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) {
// List field (list[T]) — default to []
let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into();
zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), empty_list, vm)?;
ast_set_attr(&zelf, &field, empty_list, vm)?;
} else if ftype.is(&expr_ctx_type) {
// expr_context — default to Load()
let load_type =
@@ -445,13 +490,15 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
.unwrap_or_else(|| {
vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict()))
});
zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), load_instance, vm)?;
ast_set_attr(&zelf, &field, load_instance, vm)?;
} else {
// Required field missing: emit DeprecationWarning.
let field_repr = field.repr(vm)?;
let message = vm.ctx.new_str(format!(
"{}.__init__ missing 1 required positional argument: '{}'",
"{}.__init__ missing 1 required positional argument: {}. \
This will become an error in Python 3.15.",
zelf.class().name(),
field.as_wtf8()
field_repr
));
warn::warn(
message.into(),
@@ -461,6 +508,21 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
vm,
)?;
}
} else {
let field_repr = field.repr(vm)?;
let message = vm.ctx.new_str(format!(
"Field {} is missing from {}._field_types. \
This will become an error in Python 3.15.",
field_repr,
zelf.class().name()
));
warn::warn(
message.into(),
Some(vm.ctx.exceptions.deprecation_warning.to_owned()),
1,
None,
vm,
)?;
}
}
}
@@ -510,9 +572,29 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
.map_err(|_| vm.new_type_error("AST is not a type"))?;
let ctx = &vm.ctx;
let empty_tuple = ctx.empty_tuple.clone();
let set_empty_annotations = |typ: &Py<PyType>| {
typ.set_str_attr("__annotations__", ctx.new_dict(), ctx);
};
set_empty_annotations(&ast_type);
ast_type.set_str_attr("_fields", empty_tuple.clone(), ctx);
ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx);
ast_type.set_str_attr("__match_args__", empty_tuple, ctx);
for typ in [
super::super::pyast::NodeMod::static_type(),
super::super::pyast::NodeStmt::static_type(),
super::super::pyast::NodeExpr::static_type(),
super::super::pyast::NodeExprContext::static_type(),
super::super::pyast::NodeBoolOp::static_type(),
super::super::pyast::NodeOperator::static_type(),
super::super::pyast::NodeUnaryOp::static_type(),
super::super::pyast::NodeCmpOp::static_type(),
super::super::pyast::NodeExceptHandler::static_type(),
super::super::pyast::NodePattern::static_type(),
super::super::pyast::NodeTypeIgnore::static_type(),
super::super::pyast::NodeTypeParam::static_type(),
] {
set_empty_annotations(typ);
}
const AST_REDUCE: PyMethodDef = PyMethodDef::new_const(
"__reduce__",
+20 -16
View File
@@ -1,7 +1,8 @@
use crate::{
AsObject, PyObjectRef, PyResult, VirtualMachine,
builtins::{PyList, PyTuple},
builtins::{PyList, PyStr, PyTuple},
class::PyClassImpl,
recursion::ReprGuard,
stdlib::_ast::NodeAst,
};
use rustpython_common::wtf8::Wtf8Buf;
@@ -33,9 +34,7 @@ fn repr_ast_list(vm: &VirtualMachine, items: Vec<PyObjectRef>, depth: usize) ->
rendered.push_wtf8(&parts[0]);
}
if items.len() > 2 {
if !parts[0].is_empty() {
rendered.push_wtf8(", ...".as_ref());
}
rendered.push_wtf8(", ...".as_ref());
if parts.len() > 1 {
rendered.push_wtf8(", ".as_ref());
rendered.push_wtf8(&parts[1]);
@@ -75,9 +74,7 @@ fn repr_ast_tuple(vm: &VirtualMachine, items: Vec<PyObjectRef>, depth: usize) ->
rendered.push_wtf8(&parts[0]);
}
if items.len() > 2 {
if !parts[0].is_empty() {
rendered.push_wtf8(", ...".as_ref());
}
rendered.push_wtf8(", ...".as_ref());
if parts.len() > 1 {
rendered.push_wtf8(", ".as_ref());
rendered.push_wtf8(&parts[1]);
@@ -86,9 +83,6 @@ fn repr_ast_tuple(vm: &VirtualMachine, items: Vec<PyObjectRef>, depth: usize) ->
rendered.push_wtf8(", ".as_ref());
rendered.push_wtf8(&parts[1]);
}
if items.len() == 1 {
rendered.push_wtf8(",".as_ref());
}
rendered.push_wtf8(")".as_ref());
Ok(rendered)
}
@@ -104,18 +98,24 @@ pub(crate) fn repr_ast_node(
s.push_wtf8("(...)".as_ref());
return Ok(s);
}
let Some(_guard) = ReprGuard::enter(vm, obj.as_object()) else {
let mut s = Wtf8Buf::from(&*cls.name());
s.push_wtf8("(...)".as_ref());
return Ok(s);
};
let fields = cls.get_attr(vm.ctx.intern_str("_fields"));
let fields = match fields {
Some(fields) => fields.try_to_value::<Vec<crate::builtins::PyStrRef>>(vm)?,
let fields = match cls.get_attr(vm.ctx.intern_str("_fields")) {
Some(fields) => fields,
None => {
let mut s = Wtf8Buf::from(&*cls.name());
s.push_wtf8("(...)".as_ref());
return Ok(s);
}
};
let fields = fields.sequence_unchecked();
let numfields = fields.length(vm)?;
if fields.is_empty() {
if numfields == 0 {
let mut s = Wtf8Buf::from(&*cls.name());
s.push_wtf8("()".as_ref());
return Ok(s);
@@ -124,8 +124,12 @@ pub(crate) fn repr_ast_node(
let mut rendered = Wtf8Buf::from(&*cls.name());
rendered.push_wtf8("(".as_ref());
for (idx, field) in fields.iter().enumerate() {
let value = obj.get_attr(field, vm)?;
for idx in 0..numfields {
let field = fields.get_item(idx as isize, vm)?;
let field = field
.downcast::<PyStr>()
.map_err(|_| vm.new_type_error("attribute name must be string"))?;
let value = obj.get_attr(&field, vm)?;
let value_repr = if value.fast_isinstance(vm.ctx.types.list_type) {
let list = value
.downcast::<PyList>()
File diff suppressed because it is too large. Load diff
+424 -133
View File
@@ -19,6 +19,7 @@ fn ruff_fstring_element_into_iter(
}
fn ruff_fstring_element_to_joined_str_part(
vm: &VirtualMachine,
element: ast::InterpolatedStringElement,
) -> JoinedStrPart {
match element {
@@ -38,12 +39,21 @@ fn ruff_fstring_element_to_joined_str_part(
conversion,
format_spec,
node_index: _,
}) => JoinedStrPart::FormattedValue(FormattedValue {
value: expression,
conversion,
format_spec: ruff_format_spec_to_joined_str(format_spec),
range,
}),
runtime_str: _,
runtime_interpolation_format_spec: _,
runtime_formatted_value_format_spec,
}) => {
let runtime_format_spec = runtime_formatted_value_format_spec.or_else(|| {
ruff_format_spec_to_joined_str(vm, format_spec)
.map(|joined_str| Box::new(joined_str.into_expr(false)))
});
JoinedStrPart::FormattedValue(FormattedValue {
value: expression,
conversion,
format_spec: runtime_format_spec,
range,
})
}
}
}
@@ -235,6 +245,7 @@ fn warn_invalid_escape_sequences_in_format_spec(
}
fn ruff_format_spec_to_joined_str(
vm: &VirtualMachine,
format_spec: Option<Box<ast::InterpolatedStringFormatSpec>>,
) -> Option<Box<JoinedStr>> {
match format_spec {
@@ -254,10 +265,14 @@ fn ruff_format_spec_to_joined_str(
range
};
let values: Vec<_> = ruff_fstring_element_into_iter(elements)
.map(ruff_fstring_element_to_joined_str_part)
.map(|element| ruff_fstring_element_to_joined_str_part(vm, element))
.collect();
let values = normalize_joined_str_parts(values).into_boxed_slice();
Some(Box::new(JoinedStr { range, values }))
Some(Box::new(JoinedStr {
range,
values,
runtime_values: None,
}))
}
}
}
@@ -290,40 +305,98 @@ fn ruff_fstring_element_to_ruff_fstring_part(
}
}
fn joined_str_to_ruff_format_spec(
joined_str: Option<Box<JoinedStr>>,
fn format_spec_expr_to_ruff_format_spec(
format_spec: Option<Box<ast::Expr>>,
) -> Option<Box<ast::InterpolatedStringFormatSpec>> {
match joined_str {
None => None,
Some(joined_str) => {
let JoinedStr { range, values } = *joined_str;
let elements: Vec<_> = Box::into_iter(values)
.map(joined_str_part_to_ruff_fstring_element)
.collect();
let format_spec = ast::InterpolatedStringFormatSpec {
node_index: Default::default(),
let format_spec = format_spec?;
let ast::Expr::FString(mut fstring) = *format_spec else {
return None;
};
let ast::ExprFString {
range,
ref mut value,
node_index: _,
runtime_joined_str: _,
runtime_values: _,
} = fstring;
let default_part = ast::FStringPart::FString(ast::FString {
node_index: Default::default(),
range: Default::default(),
elements: Default::default(),
flags: ast::FStringFlags::empty(),
});
let mut elements = Vec::new();
for i in 0..value.as_slice().len() {
let part = core::mem::replace(value.iter_mut().nth(i).unwrap(), default_part.clone());
match part {
ast::FStringPart::Literal(ast::StringLiteral {
range,
elements: elements.into(),
};
Some(Box::new(format_spec))
value,
node_index: _,
flags: _,
}) => elements.push(ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement {
node_index: Default::default(),
range,
value,
},
)),
ast::FStringPart::FString(ast::FString {
elements: fstring_elements,
..
}) => {
elements.extend(ruff_fstring_element_into_iter(fstring_elements));
}
}
}
Some(Box::new(ast::InterpolatedStringFormatSpec {
node_index: Default::default(),
range,
elements: elements.into(),
}))
}
#[derive(Debug)]
pub(super) struct JoinedStr {
pub(super) range: TextRange,
pub(super) values: Box<[JoinedStrPart]>,
pub(super) runtime_values: Option<Vec<Option<ast::Expr>>>,
}
impl JoinedStr {
pub(super) fn into_expr(self) -> ast::Expr {
let Self { range, values } = self;
pub(super) fn into_expr(self, from_ast_object: bool) -> ast::Expr {
let Self {
range,
values,
runtime_values: mut raw_runtime_values,
} = self;
let values = if values.iter().any(joined_str_part_requires_runtime_values) {
if raw_runtime_values.is_none() {
raw_runtime_values = Some(
values
.into_vec()
.into_iter()
.map(|part| joined_str_part_to_expr(from_ast_object, part))
.map(Some)
.collect(),
);
}
Vec::new().into_boxed_slice()
} else {
values
};
let (runtime_joined_str, runtime_values) =
raw_runtime_values.take().map_or((None, None), |values| {
if values.iter().any(Option::is_none) {
(None, Some(values))
} else {
(Some(values.into_iter().flatten().collect()), None)
}
});
ast::Expr::FString(ast::ExprFString {
node_index: Default::default(),
range: Default::default(),
range,
value: match values.len() {
// ruff represents an empty fstring like this:
0 => ast::FStringValue::single(ast::FString {
node_index: Default::default(),
range,
@@ -332,7 +405,8 @@ impl JoinedStr {
}),
1 => ast::FStringValue::single(
Box::<[_]>::into_iter(values)
.map(joined_str_part_to_ruff_fstring_element)
.map(|part| joined_str_part_to_ruff_fstring_element(from_ast_object, part))
.map(Option::unwrap)
.map(|element| ast::FString {
node_index: Default::default(),
range,
@@ -344,54 +418,108 @@ impl JoinedStr {
),
_ => ast::FStringValue::concatenated(
Box::<[_]>::into_iter(values)
.map(joined_str_part_to_ruff_fstring_element)
.map(|part| joined_str_part_to_ruff_fstring_element(from_ast_object, part))
.map(Option::unwrap)
.map(ruff_fstring_element_to_ruff_fstring_part)
.collect(),
),
},
runtime_joined_str,
runtime_values,
})
}
}
fn joined_str_part_to_ruff_fstring_element(part: JoinedStrPart) -> ast::InterpolatedStringElement {
fn joined_str_part_requires_runtime_values(part: &JoinedStrPart) -> bool {
matches!(
part,
JoinedStrPart::Constant(Constant {
value,
..
}) if !matches!(value, ConstantLiteral::Str { .. })
)
}
fn joined_str_part_to_expr(from_ast_object: bool, part: JoinedStrPart) -> ast::Expr {
match part {
JoinedStrPart::FormattedValue(value) => formatted_value_to_expr(from_ast_object, value),
JoinedStrPart::Constant(value) => value.into_expr(),
}
}
fn joined_str_part_to_ruff_fstring_element(
from_ast_object: bool,
part: JoinedStrPart,
) -> Option<ast::InterpolatedStringElement> {
match part {
JoinedStrPart::FormattedValue(value) => {
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement {
node_index: Default::default(),
range: value.range,
expression: value.value.clone(),
debug_text: None, // TODO: What is this?
conversion: value.conversion,
format_spec: joined_str_to_ruff_format_spec(value.format_spec),
})
let format_spec = value.format_spec.clone();
let runtime_formatted_value_format_spec = (from_ast_object && format_spec.is_some())
.then_some(format_spec.clone())
.flatten();
Some(ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
node_index: Default::default(),
range: value.range,
expression: value.value.clone(),
debug_text: None,
conversion: value.conversion,
format_spec: format_spec_expr_to_ruff_format_spec(format_spec),
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec,
},
))
}
JoinedStrPart::Constant(value) => {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
node_index: Default::default(),
range: value.range,
value: match value.value {
ConstantLiteral::Str { value, .. } => value,
_ => todo!(),
let Constant { range, value, .. } = value;
let ConstantLiteral::Str { value, .. } = value else {
return None;
};
Some(ast::InterpolatedStringElement::Literal(
ast::InterpolatedStringLiteralElement {
node_index: Default::default(),
range,
value,
},
})
))
}
}
}
// constructor
pub(super) fn joined_str_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<JoinedStr> {
let values: Vec<Option<ast::Expr>> =
get_node_list_field(vm, source_file, &object, "values", "JoinedStr")?;
Ok(JoinedStr {
values: Vec::new().into_boxed_slice(),
runtime_values: Some(values),
range,
})
}
impl Node for JoinedStr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { values, range } = self;
let Self {
values,
runtime_values,
range,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprJoinedStr::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item(
"values",
BoxedSlice(values).ast_to_object(vm, source_file),
vm,
)
.unwrap();
let values = if let Some(runtime_values) = runtime_values {
BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file)
} else {
BoxedSlice(values).ast_to_object(vm, source_file)
};
dict.set_item("values", values, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -400,15 +528,8 @@ impl Node for JoinedStr {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let values: BoxedSlice<_> = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "values", "JoinedStr")?,
)?;
Ok(Self {
values: values.0,
range: range_from_object(vm, source_file, object, "JoinedStr")?,
})
let range = range_from_object(vm, source_file, object.clone(), "JoinedStr")?;
joined_str_from_object_with_range(vm, source_file, object, range)
}
}
@@ -431,8 +552,7 @@ impl Node for JoinedStrPart {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
if cls.is(pyast::NodeExprFormattedValue::static_type()) {
if is_node_instance(vm, &object, pyast::NodeExprFormattedValue::static_type())? {
Ok(Self::FormattedValue(Node::ast_from_object(
vm,
source_file,
@@ -452,11 +572,31 @@ impl Node for JoinedStrPart {
pub(super) struct FormattedValue {
value: Box<ast::Expr>,
conversion: ast::ConversionFlag,
format_spec: Option<Box<JoinedStr>>,
format_spec: Option<Box<ast::Expr>>,
range: TextRange,
}
// constructor
pub(super) fn formatted_value_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<FormattedValue> {
Ok(FormattedValue {
value: get_required_node_field(vm, source_file, &object, "value", "FormattedValue")?,
conversion: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "conversion", "FormattedValue")?,
)?,
format_spec: get_node_field_opt(vm, &object, "format_spec")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for FormattedValue {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -487,25 +627,24 @@ impl Node for FormattedValue {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "FormattedValue")?,
)?,
conversion: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "conversion", "FormattedValue")?,
)?,
format_spec: get_node_field_opt(vm, &object, "format_spec")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: range_from_object(vm, source_file, object, "FormattedValue")?,
})
let range = range_from_object(vm, source_file, object.clone(), "FormattedValue")?;
formatted_value_from_object_with_range(vm, source_file, object, range)
}
}
pub(super) fn formatted_value_to_expr(
from_ast_object: bool,
formatted: FormattedValue,
) -> ast::Expr {
let range = formatted.range;
JoinedStr {
range,
values: vec![JoinedStrPart::FormattedValue(formatted)].into_boxed_slice(),
runtime_values: None,
}
.into_expr(from_ast_object)
}
pub(super) fn fstring_to_object(
vm: &VirtualMachine,
source_file: &SourceFile,
@@ -515,7 +654,27 @@ pub(super) fn fstring_to_object(
range,
mut value,
node_index: _,
runtime_joined_str,
runtime_values,
} = expression;
if let Some(joined_str) = runtime_joined_str {
return JoinedStr {
range,
values: Vec::new().into_boxed_slice(),
runtime_values: Some(joined_str.into_iter().map(Some).collect()),
}
.ast_to_object(vm, source_file);
}
if let Some(values) = runtime_values {
return JoinedStr {
range,
values: Vec::new().into_boxed_slice(),
runtime_values: Some(values),
}
.ast_to_object(vm, source_file);
}
let default_part = ast::FStringPart::FString(ast::FString {
node_index: Default::default(),
range: Default::default(),
@@ -545,7 +704,7 @@ pub(super) fn fstring_to_object(
node_index: _,
}) => {
for element in ruff_fstring_element_into_iter(elements) {
values.push(ruff_fstring_element_to_joined_str_part(element));
values.push(ruff_fstring_element_to_joined_str_part(vm, element));
}
}
}
@@ -555,12 +714,13 @@ pub(super) fn fstring_to_object(
if let JoinedStrPart::FormattedValue(value) = part
&& let Some(format_spec) = &value.format_spec
{
warn_invalid_escape_sequences_in_format_spec(vm, source_file, format_spec.range);
warn_invalid_escape_sequences_in_format_spec(vm, source_file, format_spec.range());
}
}
let c = JoinedStr {
range,
values: values.into_boxed_slice(),
runtime_values: None,
};
c.ast_to_object(vm, source_file)
}
@@ -568,8 +728,9 @@ pub(super) fn fstring_to_object(
// ===== TString (Template String) Support =====
fn ruff_tstring_element_to_template_str_part(
element: ast::InterpolatedStringElement,
vm: &VirtualMachine,
source_file: &SourceFile,
element: ast::InterpolatedStringElement,
) -> TemplateStrPart {
match element {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
@@ -588,6 +749,9 @@ fn ruff_tstring_element_to_template_str_part(
conversion,
format_spec,
node_index: _,
runtime_str,
runtime_interpolation_format_spec,
runtime_formatted_value_format_spec: _,
}) => {
let expr_range =
extend_expr_range_with_wrapping_parens(source_file, range, expression.range())
@@ -604,11 +768,23 @@ fn ruff_tstring_element_to_template_str_part(
} else {
tstring_interpolation_expr_str(source_file, range, expr_range)
};
let runtime_interpolation = super::constant::runtime_interpolation_object(
vm,
runtime_str,
runtime_interpolation_format_spec,
);
TemplateStrPart::Interpolation(TStringInterpolation {
value: expression,
str: expr_str,
str: runtime_interpolation
.as_ref()
.map_or_else(|| vm.ctx.new_str(expr_str).into(), |(str, _)| str.clone()),
conversion,
format_spec: ruff_format_spec_to_joined_str(format_spec),
format_spec: runtime_interpolation
.and_then(|(_, format_spec)| format_spec)
.or_else(|| {
ruff_format_spec_to_joined_str(vm, format_spec)
.map(|joined_str| Box::new(joined_str.into_expr(false)))
}),
range,
})
}
@@ -695,34 +871,51 @@ fn strip_interpolation_expr(expr_source: &str) -> String {
pub(super) struct TemplateStr {
pub(super) range: TextRange,
pub(super) values: Box<[TemplateStrPart]>,
pub(super) runtime_values: Option<Vec<Option<ast::Expr>>>,
}
pub(super) fn template_str_to_expr(
vm: &VirtualMachine,
source_file: &SourceFile,
template: TemplateStr,
) -> PyResult<ast::Expr> {
let TemplateStr { range, values } = template;
let elements = template_parts_to_elements(vm, values)?;
let TemplateStr {
range,
values,
runtime_values: raw_runtime_values,
} = template;
let elements = template_parts_to_elements(vm, source_file, values)?;
let tstring = ast::TString {
range,
node_index: Default::default(),
elements,
flags: ast::TStringFlags::empty(),
};
let (runtime_template_str, runtime_values) =
raw_runtime_values.map_or((None, None), |values| {
if values.iter().any(Option::is_none) {
(None, Some(values))
} else {
(Some(values.into_iter().flatten().collect()), None)
}
});
Ok(ast::Expr::TString(ast::ExprTString {
node_index: Default::default(),
range,
value: ast::TStringValue::single(tstring),
runtime_template_str,
runtime_values,
}))
}
pub(super) fn interpolation_to_expr(
vm: &VirtualMachine,
source_file: &SourceFile,
interpolation: TStringInterpolation,
) -> PyResult<ast::Expr> {
let range = interpolation.range;
let part = TemplateStrPart::Interpolation(interpolation);
let elements = template_parts_to_elements(vm, vec![part].into_boxed_slice())?;
let range = TextRange::default();
let elements = template_parts_to_elements(vm, source_file, vec![part].into_boxed_slice())?;
let tstring = ast::TString {
range,
node_index: Default::default(),
@@ -733,22 +926,26 @@ pub(super) fn interpolation_to_expr(
node_index: Default::default(),
range,
value: ast::TStringValue::single(tstring),
runtime_template_str: None,
runtime_values: None,
}))
}
fn template_parts_to_elements(
vm: &VirtualMachine,
source_file: &SourceFile,
values: Box<[TemplateStrPart]>,
) -> PyResult<ast::InterpolatedStringElements> {
let mut elements = Vec::with_capacity(values.len());
for value in values.into_vec() {
elements.push(template_part_to_element(vm, value)?);
elements.push(template_part_to_element(vm, source_file, value)?);
}
Ok(ast::InterpolatedStringElements::from(elements))
}
fn template_part_to_element(
vm: &VirtualMachine,
source_file: &SourceFile,
part: TemplateStrPart,
) -> PyResult<ast::InterpolatedStringElement> {
match part {
@@ -767,12 +964,18 @@ fn template_part_to_element(
TemplateStrPart::Interpolation(interpolation) => {
let TStringInterpolation {
value,
str,
conversion,
format_spec,
range,
..
} = interpolation;
let format_spec = joined_str_to_ruff_format_spec(format_spec);
let str_constant =
super::constant::constant_object_to_constant_data(vm, source_file, str)?;
let runtime_str = Some(super::constant::constant_data_to_ast_constant_value(
str_constant,
));
let runtime_interpolation_format_spec = format_spec.clone();
let format_spec = format_spec_expr_to_ruff_format_spec(format_spec);
Ok(ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
range,
@@ -781,6 +984,9 @@ fn template_part_to_element(
debug_text: None,
conversion,
format_spec,
runtime_str,
runtime_interpolation_format_spec,
runtime_formatted_value_format_spec: None,
},
))
}
@@ -788,19 +994,38 @@ fn template_part_to_element(
}
// constructor
pub(super) fn template_str_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<TemplateStr> {
let values: Vec<Option<ast::Expr>> =
get_node_list_field(vm, source_file, &object, "values", "TemplateStr")?;
Ok(TemplateStr {
values: Vec::new().into_boxed_slice(),
runtime_values: Some(values),
range,
})
}
impl Node for TemplateStr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { values, range } = self;
let Self {
values,
runtime_values,
range,
} = self;
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprTemplateStr::static_type().to_owned())
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item(
"values",
BoxedSlice(values).ast_to_object(vm, source_file),
vm,
)
.unwrap();
let values = if let Some(runtime_values) = runtime_values {
BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file)
} else {
BoxedSlice(values).ast_to_object(vm, source_file)
};
dict.set_item("values", values, vm).unwrap();
node_add_location(&dict, range, vm, source_file);
node.into()
}
@@ -809,15 +1034,8 @@ impl Node for TemplateStr {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let values: BoxedSlice<_> = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "values", "TemplateStr")?,
)?;
Ok(Self {
values: values.0,
range: range_from_object(vm, source_file, object, "TemplateStr")?,
})
let range = range_from_object(vm, source_file, object.clone(), "TemplateStr")?;
template_str_from_object_with_range(vm, source_file, object, range)
}
}
@@ -840,8 +1058,7 @@ impl Node for TemplateStrPart {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
if cls.is(pyast::NodeExprInterpolation::static_type()) {
if is_node_instance(vm, &object, pyast::NodeExprInterpolation::static_type())? {
Ok(Self::Interpolation(Node::ast_from_object(
vm,
source_file,
@@ -860,13 +1077,38 @@ impl Node for TemplateStrPart {
#[derive(Debug)]
pub(super) struct TStringInterpolation {
value: Box<ast::Expr>,
str: String,
str: PyObjectRef,
conversion: ast::ConversionFlag,
format_spec: Option<Box<JoinedStr>>,
format_spec: Option<Box<ast::Expr>>,
range: TextRange,
}
// constructor
pub(super) fn tstring_interpolation_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<TStringInterpolation> {
let value = get_required_node_field(vm, source_file, &object, "value", "Interpolation")?;
let str = get_node_field(vm, &object, "str", "Interpolation")?;
let conversion = Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "conversion", "Interpolation")?,
)?;
let format_spec: Option<Box<ast::Expr>> = get_node_field_opt(vm, &object, "format_spec")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?;
Ok(TStringInterpolation {
value,
str,
conversion,
format_spec,
range,
})
}
impl Node for TStringInterpolation {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -882,8 +1124,7 @@ impl Node for TStringInterpolation {
let dict = node.as_object().dict().unwrap();
dict.set_item("value", value.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item("str", vm.ctx.new_str(str).into(), vm)
.unwrap();
dict.set_item("str", str, vm).unwrap();
dict.set_item("conversion", conversion.ast_to_object(vm, source_file), vm)
.unwrap();
dict.set_item(
@@ -900,25 +1141,8 @@ impl Node for TStringInterpolation {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let str_obj = get_node_field(vm, &object, "str", "Interpolation")?;
let str_val: String = str_obj.try_into_value(vm)?;
Ok(Self {
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "Interpolation")?,
)?,
str: str_val,
conversion: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "conversion", "Interpolation")?,
)?,
format_spec: get_node_field_opt(vm, &object, "format_spec")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: range_from_object(vm, source_file, object, "Interpolation")?,
})
let range = range_from_object(vm, source_file, object.clone(), "Interpolation")?;
tstring_interpolation_from_object_with_range(vm, source_file, object, range)
}
}
@@ -931,7 +1155,42 @@ pub(super) fn tstring_to_object(
range,
mut value,
node_index: _,
runtime_template_str,
runtime_values,
} = expression;
if let Some(template_str) = runtime_template_str {
return TemplateStr {
range,
values: Vec::new().into_boxed_slice(),
runtime_values: Some(template_str.into_iter().map(Some).collect()),
}
.ast_to_object(vm, source_file);
}
if let Some(values) = runtime_values {
return TemplateStr {
range,
values: Vec::new().into_boxed_slice(),
runtime_values: Some(values),
}
.ast_to_object(vm, source_file);
}
if let [tstring] = value.as_slice()
&& let Some(ast::InterpolatedStringElement::Interpolation(interp)) =
tstring.elements.iter().next()
&& tstring.elements.get(1).is_none()
&& let Some((str, format_spec)) = super::constant::runtime_interpolation_object(
vm,
interp.runtime_str.clone(),
interp.runtime_interpolation_format_spec.clone(),
)
&& let Some(interpolation) =
standalone_tstring_interpolation_to_object(vm, source_file, &value, str, format_spec)
{
return interpolation;
}
let default_tstring = ast::TString {
node_index: Default::default(),
range: Default::default(),
@@ -943,8 +1202,9 @@ pub(super) fn tstring_to_object(
let tstring = core::mem::replace(value.iter_mut().nth(i).unwrap(), default_tstring.clone());
for element in ruff_fstring_element_into_iter(tstring.elements) {
values.push(ruff_tstring_element_to_template_str_part(
element,
vm,
source_file,
element,
));
}
}
@@ -952,6 +1212,37 @@ pub(super) fn tstring_to_object(
let c = TemplateStr {
range,
values: values.into_boxed_slice(),
runtime_values: None,
};
c.ast_to_object(vm, source_file)
}
fn standalone_tstring_interpolation_to_object(
vm: &VirtualMachine,
source_file: &SourceFile,
value: &ast::TStringValue,
str: PyObjectRef,
format_spec: Option<Box<ast::Expr>>,
) -> Option<PyObjectRef> {
let [tstring] = value.as_slice() else {
return None;
};
let mut elements = tstring.elements.iter();
let ast::InterpolatedStringElement::Interpolation(interp) = elements.next()? else {
return None;
};
if elements.next().is_some() {
return None;
}
let interpolation = TStringInterpolation {
value: interp.expression.clone(),
str,
conversion: interp.conversion,
format_spec: format_spec.or_else(|| {
ruff_format_spec_to_joined_str(vm, interp.format_spec.clone())
.map(|joined_str| Box::new(joined_str.into_expr(false)))
}),
range: interp.range,
};
Some(interpolation.ast_to_object(vm, source_file))
}
+15 -18
View File
@@ -2,6 +2,7 @@ use super::*;
use rustpython_compiler_core::SourceFile;
pub(super) enum TypeIgnore {
None,
TypeIgnore(TypeIgnoreTypeIgnore),
}
@@ -9,6 +10,7 @@ pub(super) enum TypeIgnore {
impl Node for TypeIgnore {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
match self {
Self::None => vm.ctx.none(),
Self::TypeIgnore(cons) => cons.ast_to_object(vm, source_file),
}
}
@@ -17,8 +19,9 @@ impl Node for TypeIgnore {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeTypeIgnoreTypeIgnore::static_type()) {
Ok(if vm.is_none(&object) {
Self::None
} else if is_node_instance(vm, &object, pyast::NodeTypeIgnoreTypeIgnore::static_type())? {
Self::TypeIgnore(TypeIgnoreTypeIgnore::ast_from_object(
vm,
source_file,
@@ -34,15 +37,14 @@ impl Node for TypeIgnore {
}
pub(super) struct TypeIgnoreTypeIgnore {
range: TextRange,
lineno: PyRefExact<PyInt>,
tag: PyRefExact<PyStr>,
lineno: i32,
tag: PyObjectRef,
}
// constructor
impl Node for TypeIgnoreTypeIgnore {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { lineno, tag, range } = self;
fn ast_to_object(self, vm: &VirtualMachine, _source_file: &SourceFile) -> PyObjectRef {
let Self { lineno, tag } = self;
let node = NodeAst
.into_ref_with_type(
vm,
@@ -50,25 +52,20 @@ impl Node for TypeIgnoreTypeIgnore {
)
.unwrap();
let dict = node.as_object().dict().unwrap();
dict.set_item("lineno", lineno.to_pyobject(vm), vm).unwrap();
dict.set_item("tag", tag.to_pyobject(vm), vm).unwrap();
node_add_location(&dict, range, vm, source_file);
dict.set_item("lineno", vm.ctx.new_int(lineno).into(), vm)
.unwrap();
dict.set_item("tag", tag, vm).unwrap();
node.into()
}
fn ast_from_object(
vm: &VirtualMachine,
source_file: &SourceFile,
_source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
lineno: get_node_field(vm, &object, "lineno", "TypeIgnore")?
.downcast_exact(vm)
.unwrap(),
tag: get_node_field(vm, &object, "tag", "TypeIgnore")?
.downcast_exact(vm)
.unwrap(),
range: range_from_object(vm, source_file, object, "TypeIgnore")?,
lineno: get_int_field(vm, &object, "lineno", "TypeIgnore")?,
tag: node_object_to_ast_string(vm, get_node_field(vm, &object, "tag", "TypeIgnore")?)?,
})
}
}
+135 -75
View File
@@ -3,7 +3,10 @@ use rustpython_compiler_core::SourceFile;
impl Node for ast::TypeParams {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
self.type_params.ast_to_object(vm, source_file)
self.runtime_type_params.map_or_else(
|| self.type_params.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
)
}
fn ast_from_object(
@@ -11,22 +14,51 @@ impl Node for ast::TypeParams {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let type_params: Vec<ast::TypeParam> = Node::ast_from_object(vm, source_file, object)?;
let range = Option::zip(type_params.first(), type_params.last())
.map(|(first, last)| first.range().cover(last.range()))
.unwrap_or_default();
Ok(Self {
node_index: Default::default(),
type_params,
range,
})
Ok(type_params_from_values(
vm,
Node::ast_from_object(vm, source_file, object)?,
))
}
fn is_none(&self) -> bool {
self.type_params.is_empty()
self.type_params.is_empty() && self.runtime_type_params.is_none()
}
}
pub(super) fn type_params_from_field(
vm: &VirtualMachine,
source_file: &SourceFile,
object: &PyObject,
field: &'static str,
typ: &str,
) -> PyResult<Option<Box<ast::TypeParams>>> {
let type_params: Vec<Option<ast::TypeParam>> =
get_node_list_field(vm, source_file, object, field, typ)?;
let type_params = type_params_from_values(vm, type_params);
Ok((!type_params.is_none()).then_some(Box::new(type_params)))
}
fn type_params_from_values(
_vm: &VirtualMachine,
values: Vec<Option<ast::TypeParam>>,
) -> ast::TypeParams {
let runtime_type_params = values.iter().any(Option::is_none).then(|| values.clone());
let type_params = lower_nullable_type_params(&values);
let range = Option::zip(type_params.first(), type_params.last())
.map(|(first, last)| first.range().cover(last.range()))
.unwrap_or_default();
ast::TypeParams {
node_index: Default::default(),
type_params,
range,
runtime_type_params,
}
}
fn lower_nullable_type_params(values: &[Option<ast::TypeParam>]) -> Vec<ast::TypeParam> {
values.iter().filter_map(Clone::clone).collect()
}
// sum
impl Node for ast::TypeParam {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
@@ -42,35 +74,70 @@ impl Node for ast::TypeParam {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
let cls = object.class();
Ok(if cls.is(pyast::NodeTypeParamTypeVar::static_type()) {
Self::TypeVar(ast::TypeParamTypeVar::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodeTypeParamParamSpec::static_type()) {
Self::ParamSpec(ast::TypeParamParamSpec::ast_from_object(
vm,
source_file,
object,
)?)
} else if cls.is(pyast::NodeTypeParamTypeVarTuple::static_type()) {
Self::TypeVarTuple(ast::TypeParamTypeVarTuple::ast_from_object(
vm,
source_file,
object,
)?)
if vm.is_none(&object) {
return Err(vm.new_type_error(format!(
"expected some sort of type_param, but got {}",
object.repr(vm)?
)));
}
enum TypeParamKind {
TypeVar,
ParamSpec,
TypeVarTuple,
}
let kind = if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVar::static_type())? {
TypeParamKind::TypeVar
} else if is_node_instance(vm, &object, pyast::NodeTypeParamParamSpec::static_type())? {
TypeParamKind::ParamSpec
} else if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVarTuple::static_type())? {
TypeParamKind::TypeVarTuple
} else {
return Err(vm.new_type_error(format!(
"expected some sort of type_param, but got {}",
object.repr(vm)?
)));
};
let range = type_param_range_from_object(vm, source_file, object.clone())?;
Ok(match kind {
TypeParamKind::TypeVar => Self::TypeVar(type_var_from_object_with_range(
vm,
source_file,
object,
range,
)?),
TypeParamKind::ParamSpec => Self::ParamSpec(param_spec_from_object_with_range(
vm,
source_file,
object,
range,
)?),
TypeParamKind::TypeVarTuple => Self::TypeVarTuple(
type_var_tuple_from_object_with_range(vm, source_file, object, range)?,
),
})
}
}
// constructor
fn type_var_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::TypeParamTypeVar> {
Ok(ast::TypeParamTypeVar {
node_index: Default::default(),
name: get_required_identifier_field(vm, source_file, &object, "name", "TypeVar")?,
bound: get_node_field_opt(vm, &object, "bound")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
default: get_node_field_opt(vm, &object, "default_value")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for ast::TypeParamTypeVar {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -99,27 +166,28 @@ impl Node for ast::TypeParamTypeVar {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
name: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "name", "TypeVar")?,
)?,
bound: get_node_field_opt(vm, &object, "bound")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
default: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "default_value", "TypeVar")?,
)?,
range: range_from_object(vm, source_file, object, "TypeVar")?,
})
let range = type_param_range_from_object(vm, source_file, object.clone())?;
type_var_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn param_spec_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::TypeParamParamSpec> {
Ok(ast::TypeParamParamSpec {
node_index: Default::default(),
name: get_required_identifier_field(vm, source_file, &object, "name", "ParamSpec")?,
default: get_node_field_opt(vm, &object, "default_value")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for ast::TypeParamParamSpec {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -145,24 +213,28 @@ impl Node for ast::TypeParamParamSpec {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
name: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "name", "ParamSpec")?,
)?,
default: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "default_value", "ParamSpec")?,
)?,
range: range_from_object(vm, source_file, object, "ParamSpec")?,
})
let range = type_param_range_from_object(vm, source_file, object.clone())?;
param_spec_from_object_with_range(vm, source_file, object, range)
}
}
// constructor
fn type_var_tuple_from_object_with_range(
vm: &VirtualMachine,
source_file: &SourceFile,
object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::TypeParamTypeVarTuple> {
Ok(ast::TypeParamTypeVarTuple {
node_index: Default::default(),
name: get_required_identifier_field(vm, source_file, &object, "name", "TypeVarTuple")?,
default: get_node_field_opt(vm, &object, "default_value")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range,
})
}
impl Node for ast::TypeParamTypeVarTuple {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self {
@@ -191,19 +263,7 @@ impl Node for ast::TypeParamTypeVarTuple {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
Ok(Self {
node_index: Default::default(),
name: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "name", "TypeVarTuple")?,
)?,
default: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "default_value", "TypeVarTuple")?,
)?,
range: range_from_object(vm, source_file, object, "TypeVarTuple")?,
})
let range = type_param_range_from_object(vm, source_file, object.clone())?;
type_var_tuple_from_object_with_range(vm, source_file, object, range)
}
}
+378 -67
View File
@@ -1,8 +1,10 @@
// spell-checker: ignore assignlist ifexp
use super::module::Mod;
use crate::{PyResult, VirtualMachine};
use crate::{PyResult, VirtualMachine, compiler::CompileError};
use ruff_python_ast as ast;
use rustpython_codegen::error::{CodegenError, CodegenErrorType};
use rustpython_compiler_core::bytecode::ConstantData;
fn expr_context_name(ctx: ast::ExprContext) -> &'static str {
match ctx {
@@ -13,6 +15,17 @@ fn expr_context_name(ctx: ast::ExprContext) -> &'static str {
}
}
fn invalid_syntax_error(vm: &VirtualMachine) -> crate::builtins::PyBaseExceptionRef {
vm.new_syntax_error(
&CompileError::Codegen(CodegenError {
location: None,
error: CodegenErrorType::SyntaxError("invalid syntax".to_owned()),
source_path: "<unknown>".to_owned(),
}),
None,
)
}
fn validate_name(vm: &VirtualMachine, name: &ast::name::Name) -> PyResult<()> {
match name.as_str() {
"None" | "True" | "False" => Err(vm.new_value_error(format!(
@@ -30,6 +43,7 @@ fn validate_comprehension(vm: &VirtualMachine, gens: &[ast::Comprehension]) -> P
for comp in gens {
validate_expr(vm, &comp.target, ast::ExprContext::Store)?;
validate_expr(vm, &comp.iter, ast::ExprContext::Load)?;
validate_runtime_expr_list_slots(vm, comp.runtime_ifs.as_ref(), ast::ExprContext::Load)?;
validate_exprs(vm, &comp.ifs, ast::ExprContext::Load, false)?;
}
Ok(())
@@ -42,30 +56,49 @@ fn validate_keywords(vm: &VirtualMachine, keywords: &[ast::Keyword]) -> PyResult
Ok(())
}
fn validate_parameter_annotation(vm: &VirtualMachine, parameter: &ast::Parameter) -> PyResult<()> {
if let Some(annotation) = &parameter.annotation {
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
Ok(())
}
fn validate_parameters(vm: &VirtualMachine, params: &ast::Parameters) -> PyResult<()> {
for param in params
.posonlyargs
.iter()
.chain(&params.args)
.chain(&params.kwonlyargs)
{
if let Some(annotation) = &param.parameter.annotation {
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
if let Some(default) = &param.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
for param in params.posonlyargs.iter().chain(&params.args) {
validate_parameter_annotation(vm, &param.parameter)?;
}
if let Some(vararg) = &params.vararg
&& let Some(annotation) = &vararg.annotation
{
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
for param in &params.kwonlyargs {
validate_parameter_annotation(vm, &param.parameter)?;
}
if let Some(kwarg) = &params.kwarg
&& let Some(annotation) = &kwarg.annotation
{
validate_expr(vm, annotation, ast::ExprContext::Load)?;
}
if let Some(defaults) = params.runtime_defaults.as_ref() {
for default in defaults {
let Some(default) = default else {
return Err(vm.new_value_error("None disallowed in expression list"));
};
validate_expr(vm, default, ast::ExprContext::Load)?;
}
} else {
for param in params.posonlyargs.iter().chain(&params.args) {
if let Some(default) = &param.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
}
for param in &params.kwonlyargs {
if let Some(default) = &param.default {
validate_expr(vm, default, ast::ExprContext::Load)?;
}
}
Ok(())
}
@@ -99,8 +132,14 @@ fn validate_assignlist(
validate_exprs(vm, targets, ctx, false)
}
fn validate_body(vm: &VirtualMachine, body: &[ast::Stmt], owner: &'static str) -> PyResult<()> {
fn validate_body(
vm: &VirtualMachine,
body: &[ast::Stmt],
metadata: Option<&Vec<Option<ast::Stmt>>>,
owner: &'static str,
) -> PyResult<()> {
validate_nonempty_seq(vm, body.len(), "body", owner)?;
validate_runtime_stmt_list_slots(vm, metadata)?;
validate_stmts(vm, body)
}
@@ -111,34 +150,83 @@ fn validate_interpolated_elements<'a>(
for element in elements {
if let ast::InterpolatedStringElementRef::Interpolation(interpolation) = element {
validate_expr(vm, &interpolation.expression, ast::ExprContext::Load)?;
if let Some(format_spec) = &interpolation.format_spec {
for spec_element in &format_spec.elements {
if let ast::InterpolatedStringElement::Interpolation(spec_interp) = spec_element
{
validate_expr(vm, &spec_interp.expression, ast::ExprContext::Load)?;
}
}
if let Some(format_spec) = interpolation.runtime_formatted_value_format_spec.as_deref()
{
validate_expr(vm, format_spec, ast::ExprContext::Load)?;
} else if let Some(format_spec) =
interpolation.runtime_interpolation_format_spec.as_deref()
{
validate_expr(vm, format_spec, ast::ExprContext::Load)?;
} else if let Some(format_spec) = &interpolation.format_spec {
validate_interpolated_elements(
vm,
format_spec
.elements
.iter()
.map(ast::InterpolatedStringElementRef::from),
)?;
}
}
}
Ok(())
}
fn ensure_literal_number(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool {
let ast::Expr::NumberLiteral(number) = expr else {
return false;
};
match number.value {
ast::Number::Int(_) | ast::Number::Float(_) => allow_real,
ast::Number::Complex { .. } => allow_imaginary,
}
}
fn ensure_literal_negative(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool {
let ast::Expr::UnaryOp(unary) = expr else {
return false;
};
if unary.op != ast::UnaryOp::USub {
return false;
}
ensure_literal_number(&unary.operand, allow_real, allow_imaginary)
}
fn ensure_literal_complex(expr: &ast::Expr) -> bool {
let ast::Expr::BinOp(bin) = expr else {
return false;
};
if !matches!(bin.op, ast::Operator::Add | ast::Operator::Sub) {
return false;
}
let real_left = ensure_literal_number(&bin.left, true, false)
|| ensure_literal_negative(&bin.left, true, false);
real_left && ensure_literal_number(&bin.right, false, true)
}
fn ast_constant_value(expr: &ast::Expr) -> Option<ConstantData> {
expr.as_constant_expr()
.map(|expr| super::constant::ast_constant_value_to_constant_data(expr.value.clone()))
}
fn validate_pattern_match_value(vm: &VirtualMachine, expr: &ast::Expr) -> PyResult<()> {
validate_expr(vm, expr, ast::ExprContext::Load)?;
if let Some(constant) = ast_constant_value(expr) {
return match &constant {
ConstantData::Integer { .. }
| ConstantData::Float { .. }
| ConstantData::Bytes { .. }
| ConstantData::Complex { .. }
| ConstantData::Str { .. } => Ok(()),
_ => Err(vm.new_value_error("unexpected constant inside of a literal pattern")),
};
}
match expr {
ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => {
Ok(())
}
ast::Expr::Attribute(_) => Ok(()),
ast::Expr::UnaryOp(op) => match &*op.operand {
ast::Expr::NumberLiteral(_) => Ok(()),
_ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")),
},
ast::Expr::BinOp(bin) => match (&*bin.left, &*bin.right) {
(ast::Expr::NumberLiteral(_), ast::Expr::NumberLiteral(_)) => Ok(()),
_ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")),
},
ast::Expr::UnaryOp(_) if ensure_literal_negative(expr, true, true) => Ok(()),
ast::Expr::BinOp(_) if ensure_literal_complex(expr) => Ok(()),
ast::Expr::FString(_) | ast::Expr::TString(_) => Ok(()),
ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
@@ -162,7 +250,10 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool)
ast::Pattern::MatchSingleton(singleton) => match singleton.value {
ast::Singleton::None | ast::Singleton::True | ast::Singleton::False => Ok(()),
},
ast::Pattern::MatchSequence(seq) => validate_patterns(vm, &seq.patterns, true),
ast::Pattern::MatchSequence(seq) => {
validate_runtime_pattern_list_slots(vm, seq.runtime_patterns.as_ref())?;
validate_patterns(vm, &seq.patterns, true)
}
ast::Pattern::MatchMapping(mapping) => {
if mapping.keys.len() != mapping.patterns.len() {
return Err(vm.new_value_error(
@@ -172,15 +263,34 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool)
if let Some(rest) = &mapping.rest {
validate_capture(vm, rest)?;
}
validate_runtime_expr_option_list_slots(vm, mapping.runtime_keys.as_ref())?;
for key in &mapping.keys {
if let ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) = key {
if matches!(
key,
ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::Constant(ast::ExprConstant {
value: ast::ConstantValue::Boolean(_) | ast::ConstantValue::None,
..
})
) {
continue;
}
validate_pattern_match_value(vm, key)?;
}
validate_runtime_pattern_list_slots(vm, mapping.runtime_patterns.as_ref())?;
validate_patterns(vm, &mapping.patterns, false)
}
ast::Pattern::MatchClass(match_class) => {
if let (Some(kwd_attrs), Some(kwd_patterns)) = (
match_class.runtime_kwd_attrs.as_ref(),
match_class.runtime_kwd_patterns.as_ref(),
) && kwd_attrs.len() != kwd_patterns.len()
{
return Err(vm.new_value_error(
"MatchClass doesn't have the same number of keyword attributes as patterns",
));
}
validate_expr(vm, &match_class.cls, ast::ExprContext::Load)?;
let mut cls = match_class.cls.as_ref();
loop {
@@ -199,7 +309,13 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool)
for keyword in &match_class.arguments.keywords {
validate_name(vm, keyword.attr.id())?;
}
if let Some(patterns) = &match_class.runtime_patterns {
validate_runtime_nullable_patterns(vm, patterns)?;
}
validate_patterns(vm, &match_class.arguments.patterns, false)?;
if let Some(kwd_patterns) = &match_class.runtime_kwd_patterns {
validate_runtime_nullable_patterns(vm, kwd_patterns)?;
}
for keyword in &match_class.arguments.keywords {
validate_pattern(vm, &keyword.pattern, false)?;
}
@@ -234,11 +350,80 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool)
if match_or.patterns.len() < 2 {
return Err(vm.new_value_error("MatchOr requires at least 2 patterns"));
}
validate_runtime_pattern_list_slots(vm, match_or.runtime_patterns.as_ref())?;
validate_patterns(vm, &match_or.patterns, false)
}
}
}
fn validate_runtime_pattern_list_slots(
vm: &VirtualMachine,
values: Option<&Vec<Option<ast::Pattern>>>,
) -> PyResult<()> {
if values.is_some_and(|values| values.iter().any(Option::is_none)) {
return Err(vm.new_value_error("unexpected pattern"));
}
Ok(())
}
fn validate_runtime_expr_option_list_slots(
vm: &VirtualMachine,
values: Option<&Vec<Option<ast::Expr>>>,
) -> PyResult<()> {
if values.is_some_and(|values| values.iter().any(Option::is_none)) {
return Err(vm.new_value_error("None disallowed in expression list"));
}
Ok(())
}
fn validate_runtime_expr_list_slots(
vm: &VirtualMachine,
values: Option<&Vec<Option<ast::Expr>>>,
ctx: ast::ExprContext,
) -> PyResult<()> {
if let Some(values) = values {
for value in values {
let Some(value) = value else {
return Err(vm.new_value_error("None disallowed in expression list"));
};
validate_expr(vm, value, ctx)?;
}
}
Ok(())
}
fn validate_runtime_stmt_list_slots(
vm: &VirtualMachine,
values: Option<&Vec<Option<ast::Stmt>>>,
) -> PyResult<()> {
if let Some(values) = values
&& values.iter().any(Option::is_none)
{
return Err(vm.new_value_error("None disallowed in statement list"));
}
Ok(())
}
fn validate_runtime_except_handler_list_slots(
vm: &VirtualMachine,
values: Option<&Vec<Option<ast::ExceptHandler>>>,
) -> PyResult<()> {
if values.is_some_and(|values| values.iter().any(Option::is_none)) {
return Err(vm.new_value_error("unexpected excepthandler"));
}
Ok(())
}
fn validate_runtime_nullable_patterns(
vm: &VirtualMachine,
patterns: &[Option<ast::Pattern>],
) -> PyResult<()> {
if patterns.iter().any(Option::is_none) {
return Err(vm.new_value_error("unexpected pattern"));
}
Ok(())
}
fn validate_patterns(
vm: &VirtualMachine,
patterns: &[ast::Pattern],
@@ -282,6 +467,12 @@ fn validate_type_params(
type_params: Option<&ast::TypeParams>,
) -> PyResult<()> {
if let Some(type_params) = type_params {
if let Some(values) = type_params.runtime_type_params.as_ref() {
for tp in values.iter().flatten() {
validate_typeparam(vm, tp)?;
}
return Ok(());
}
for tp in &type_params.type_params {
validate_typeparam(vm, tp)?;
}
@@ -337,6 +528,11 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
if op.values.len() < 2 {
return Err(vm.new_value_error("BoolOp with less than 2 values"));
}
validate_runtime_expr_list_slots(
vm,
op.runtime_values.as_ref(),
ast::ExprContext::Load,
)?;
validate_exprs(vm, &op.values, ast::ExprContext::Load, false)
}
ast::Expr::Named(named) => {
@@ -362,6 +558,11 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
validate_expr(vm, &ifexp.orelse, ast::ExprContext::Load)
}
ast::Expr::Dict(dict) => {
validate_runtime_expr_list_slots(
vm,
dict.runtime_values.as_ref(),
ast::ExprContext::Load,
)?;
for item in &dict.items {
if let Some(key) = &item.key {
validate_expr(vm, key, ast::ExprContext::Load)?;
@@ -370,7 +571,14 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
}
Ok(())
}
ast::Expr::Set(set) => validate_exprs(vm, &set.elts, ast::ExprContext::Load, false),
ast::Expr::Set(set) => {
validate_runtime_expr_list_slots(
vm,
set.runtime_elts.as_ref(),
ast::ExprContext::Load,
)?;
validate_exprs(vm, &set.elts, ast::ExprContext::Load, false)
}
ast::Expr::ListComp(list) => {
validate_comprehension(vm, &list.generators)?;
validate_expr(vm, &list.elt, ast::ExprContext::Load)
@@ -409,34 +617,73 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
"Compare has a different number of comparators and operands",
));
}
validate_runtime_expr_list_slots(
vm,
compare.runtime_comparators.as_ref(),
ast::ExprContext::Load,
)?;
validate_exprs(vm, &compare.comparators, ast::ExprContext::Load, false)?;
validate_expr(vm, &compare.left, ast::ExprContext::Load)
}
ast::Expr::Call(call) => {
validate_expr(vm, &call.func, ast::ExprContext::Load)?;
validate_runtime_expr_list_slots(
vm,
call.arguments.runtime_args.as_ref(),
ast::ExprContext::Load,
)?;
validate_exprs(vm, &call.arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &call.arguments.keywords)
}
ast::Expr::FString(fstring) => validate_interpolated_elements(
vm,
fstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
),
ast::Expr::TString(tstring) => validate_interpolated_elements(
vm,
tstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
),
ast::Expr::FString(fstring) => {
validate_runtime_expr_list_slots(
vm,
fstring.runtime_values.as_ref(),
ast::ExprContext::Load,
)?;
if let Some(joined_str) = fstring.runtime_joined_str.as_ref() {
validate_exprs(vm, joined_str, ast::ExprContext::Load, false)
} else {
validate_interpolated_elements(
vm,
fstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
)
}
}
ast::Expr::TString(tstring) => {
validate_runtime_expr_list_slots(
vm,
tstring.runtime_values.as_ref(),
ast::ExprContext::Load,
)?;
if let Some(template_str) = tstring.runtime_template_str.as_ref() {
validate_exprs(vm, template_str, ast::ExprContext::Load, false)
} else {
validate_interpolated_elements(
vm,
tstring
.value
.elements()
.map(ast::InterpolatedStringElementRef::from),
)
}
}
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::Constant(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_) => Ok(()),
| ast::Expr::EllipsisLiteral(_) => {
if let Some(invalid_type) = super::constant::invalid_constant_type(expr) {
Err(vm.new_type_error(format!("got an invalid type in Constant: {invalid_type}")))
} else {
Ok(())
}
}
ast::Expr::Attribute(attr) => validate_expr(vm, &attr.value, ast::ExprContext::Load),
ast::Expr::Subscript(sub) => {
validate_expr(vm, &sub.slice, ast::ExprContext::Load)?;
@@ -444,8 +691,14 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
}
ast::Expr::Starred(star) => validate_expr(vm, &star.value, ctx),
ast::Expr::Name(_) => Ok(()),
ast::Expr::List(list) => validate_exprs(vm, &list.elts, ctx, false),
ast::Expr::Tuple(tuple) => validate_exprs(vm, &tuple.elts, ctx, false),
ast::Expr::List(list) => {
validate_runtime_expr_list_slots(vm, list.runtime_elts.as_ref(), ctx)?;
validate_exprs(vm, &list.elts, ctx, false)
}
ast::Expr::Tuple(tuple) => {
validate_runtime_expr_list_slots(vm, tuple.runtime_elts.as_ref(), ctx)?;
validate_exprs(vm, &tuple.elts, ctx, false)
}
ast::Expr::Slice(slice) => {
if let Some(lower) = &slice.lower {
validate_expr(vm, lower, ast::ExprContext::Load)?;
@@ -458,7 +711,7 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
}
Ok(())
}
ast::Expr::IpyEscapeCommand(_) => Ok(()),
ast::Expr::IpyEscapeCommand(_) => Err(invalid_syntax_error(vm)),
}
}
@@ -477,9 +730,14 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
} else {
"FunctionDef"
};
validate_body(vm, &func.body, owner)?;
validate_body(vm, &func.body, func.runtime_body.as_ref(), owner)?;
validate_type_params(vm, func.type_params.as_deref())?;
validate_parameters(vm, &func.parameters)?;
validate_runtime_expr_list_slots(
vm,
func.runtime_decorator_list.as_ref(),
ast::ExprContext::Load,
)?;
validate_decorators(vm, &func.decorator_list)?;
if let Some(returns) = &func.returns {
validate_expr(vm, returns, ast::ExprContext::Load)?;
@@ -487,12 +745,27 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(())
}
ast::Stmt::ClassDef(class_def) => {
validate_body(vm, &class_def.body, "ClassDef")?;
validate_body(
vm,
&class_def.body,
class_def.runtime_body.as_ref(),
"ClassDef",
)?;
validate_type_params(vm, class_def.type_params.as_deref())?;
if let Some(arguments) = &class_def.arguments {
validate_runtime_expr_list_slots(
vm,
arguments.runtime_bases.as_ref(),
ast::ExprContext::Load,
)?;
validate_exprs(vm, &arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &arguments.keywords)?;
}
validate_runtime_expr_list_slots(
vm,
class_def.runtime_decorator_list.as_ref(),
ast::ExprContext::Load,
)?;
validate_decorators(vm, &class_def.decorator_list)
}
ast::Stmt::Return(ret) => {
@@ -501,8 +774,20 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
}
Ok(())
}
ast::Stmt::Delete(del) => validate_assignlist(vm, &del.targets, ast::ExprContext::Del),
ast::Stmt::Delete(del) => {
validate_runtime_expr_list_slots(
vm,
del.runtime_targets.as_ref(),
ast::ExprContext::Del,
)?;
validate_assignlist(vm, &del.targets, ast::ExprContext::Del)
}
ast::Stmt::Assign(assign) => {
validate_runtime_expr_list_slots(
vm,
assign.runtime_targets.as_ref(),
ast::ExprContext::Store,
)?;
validate_assignlist(vm, &assign.targets, ast::ExprContext::Store)?;
validate_expr(vm, &assign.value, ast::ExprContext::Load)
}
@@ -532,22 +817,30 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
let owner = if for_stmt.is_async { "AsyncFor" } else { "For" };
validate_expr(vm, &for_stmt.target, ast::ExprContext::Store)?;
validate_expr(vm, &for_stmt.iter, ast::ExprContext::Load)?;
validate_body(vm, &for_stmt.body, owner)?;
validate_body(vm, &for_stmt.body, for_stmt.runtime_body.as_ref(), owner)?;
validate_runtime_stmt_list_slots(vm, for_stmt.runtime_orelse.as_ref())?;
validate_stmts(vm, &for_stmt.orelse)
}
ast::Stmt::While(while_stmt) => {
validate_expr(vm, &while_stmt.test, ast::ExprContext::Load)?;
validate_body(vm, &while_stmt.body, "While")?;
validate_body(
vm,
&while_stmt.body,
while_stmt.runtime_body.as_ref(),
"While",
)?;
validate_runtime_stmt_list_slots(vm, while_stmt.runtime_orelse.as_ref())?;
validate_stmts(vm, &while_stmt.orelse)
}
ast::Stmt::If(if_stmt) => {
validate_expr(vm, &if_stmt.test, ast::ExprContext::Load)?;
validate_body(vm, &if_stmt.body, "If")?;
validate_body(vm, &if_stmt.body, if_stmt.runtime_body.as_ref(), "If")?;
for clause in &if_stmt.elif_else_clauses {
if let Some(test) = &clause.test {
validate_expr(vm, test, ast::ExprContext::Load)?;
}
validate_body(vm, &clause.body, "If")?;
validate_body(vm, &clause.body, clause.runtime_body.as_ref(), "If")?;
validate_runtime_stmt_list_slots(vm, clause.runtime_orelse.as_ref())?;
}
Ok(())
}
@@ -564,7 +857,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
validate_expr(vm, optional_vars, ast::ExprContext::Store)?;
}
}
validate_body(vm, &with_stmt.body, owner)
validate_body(vm, &with_stmt.body, with_stmt.runtime_body.as_ref(), owner)
}
ast::Stmt::Match(match_stmt) => {
validate_expr(vm, &match_stmt.subject, ast::ExprContext::Load)?;
@@ -574,7 +867,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
if let Some(guard) = &case.guard {
validate_expr(vm, guard, ast::ExprContext::Load)?;
}
validate_body(vm, &case.body, "match_case")?;
validate_body(vm, &case.body, case.runtime_body.as_ref(), "match_case")?;
}
Ok(())
}
@@ -591,7 +884,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
}
ast::Stmt::Try(try_stmt) => {
let owner = if try_stmt.is_star { "TryStar" } else { "Try" };
validate_body(vm, &try_stmt.body, owner)?;
validate_body(vm, &try_stmt.body, try_stmt.runtime_body.as_ref(), owner)?;
if try_stmt.handlers.is_empty() && try_stmt.finalbody.is_empty() {
return Err(vm.new_value_error(format!(
"{owner} has neither except handlers nor finalbody"
@@ -602,14 +895,22 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
vm.new_value_error(format!("{owner} has orelse but no except handlers"))
);
}
validate_runtime_except_handler_list_slots(vm, try_stmt.runtime_handlers.as_ref())?;
for handler in &try_stmt.handlers {
let ast::ExceptHandler::ExceptHandler(handler) = handler;
if let Some(type_expr) = &handler.type_ {
validate_expr(vm, type_expr, ast::ExprContext::Load)?;
}
validate_body(vm, &handler.body, "ExceptHandler")?;
validate_body(
vm,
&handler.body,
handler.runtime_body.as_ref(),
"ExceptHandler",
)?;
}
validate_runtime_stmt_list_slots(vm, try_stmt.runtime_finalbody.as_ref())?;
validate_stmts(vm, &try_stmt.finalbody)?;
validate_runtime_stmt_list_slots(vm, try_stmt.runtime_orelse.as_ref())?;
validate_stmts(vm, &try_stmt.orelse)
}
ast::Stmt::Assert(assert_stmt) => {
@@ -624,6 +925,11 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(())
}
ast::Stmt::ImportFrom(import) => {
if let Some(level) = import.runtime_level
&& level < 0
{
return Err(vm.new_value_error("Negative ImportFrom level"));
}
validate_nonempty_seq(vm, import.names.len(), "names", "ImportFrom")?;
Ok(())
}
@@ -636,10 +942,8 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(())
}
ast::Stmt::Expr(expr) => validate_expr(vm, &expr.value, ast::ExprContext::Load),
ast::Stmt::Pass(_)
| ast::Stmt::Break(_)
| ast::Stmt::Continue(_)
| ast::Stmt::IpyEscapeCommand(_) => Ok(()),
ast::Stmt::Pass(_) | ast::Stmt::Break(_) | ast::Stmt::Continue(_) => Ok(()),
ast::Stmt::IpyEscapeCommand(_) => Err(invalid_syntax_error(vm)),
}
}
@@ -652,10 +956,17 @@ fn validate_stmts(vm: &VirtualMachine, stmts: &[ast::Stmt]) -> PyResult<()> {
pub(super) fn validate_mod(vm: &VirtualMachine, module: &Mod) -> PyResult<()> {
match module {
Mod::Module(module) => validate_stmts(vm, &module.body),
Mod::Interactive(module) => validate_stmts(vm, &module.body),
Mod::Module(module) => {
validate_runtime_stmt_list_slots(vm, module.module.runtime_body.as_ref())?;
validate_stmts(vm, &module.module.body)
}
Mod::Interactive(module) => {
validate_runtime_stmt_list_slots(vm, module.runtime_body.as_ref())?;
validate_stmts(vm, &module.body)
}
Mod::Expression(expr) => validate_expr(vm, &expr.body, ast::ExprContext::Load),
Mod::FunctionType(func_type) => {
validate_runtime_expr_option_list_slots(vm, func_type.runtime_argtypes.as_ref())?;
validate_exprs(vm, &func_type.argtypes, ast::ExprContext::Load, false)?;
validate_expr(vm, &func_type.returns, ast::ExprContext::Load)
}
+2
View File
@@ -153,7 +153,9 @@ mod _symtable {
CompilerScope::Class => TYPE_CLASS,
CompilerScope::Module => TYPE_MODULE,
CompilerScope::Annotation => TYPE_ANNOTATION,
CompilerScope::TypeAlias => TYPE_TYPE_ALIAS,
CompilerScope::TypeParams => TYPE_TYPE_PARAMETERS,
CompilerScope::TypeVariable => TYPE_TYPE_VARIABLE,
}
}
+371 -340
View File
@@ -13,11 +13,12 @@ mod builtins {
PyByteArray, PyBytes, PyDictRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType,
PyUtf8StrRef,
enumerate::PyReverseSequenceIterator,
function::{PyCellRef, PyFunction},
function::{PyCell, PyCellRef, PyFunction},
int::PyIntRef,
iter::PyCallableIterator,
list::{PyList, SortOptions},
},
bytecode,
common::hash::PyHash,
function::{
ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping,
@@ -29,9 +30,13 @@ mod builtins {
readline::{Readline, ReadlineResult},
stdlib::sys,
types::PyComparisonOp,
vm::compile_mode::{
CompilerFlags, PY_EVAL_INPUT, PY_FILE_INPUT, PY_FUNC_TYPE_INPUT, PY_SINGLE_INPUT,
compile_future_feature_mask, compile_future_features_from_flags,
},
};
use itertools::Itertools;
use num_traits::{Signed, ToPrimitive, Zero};
use num_traits::{Signed, ToPrimitive};
use rustpython_common::wtf8::CodePoint;
#[cfg(not(feature = "rustpython-compiler"))]
@@ -103,8 +108,8 @@ mod builtins {
filename: PyObjectRef,
mode: PyUtf8StrRef,
// CPython parity: flags / optimize accept any object with __index__,
// not just exact int. Matches the behavior of `int(x)` arg conversion
// used by Python/Python-ast.c::compile.
// not just exact int. Matches the argument conversion used by
// builtin_compile_impl.
#[pyarg(any, optional)]
flags: OptionalArg<ArgPrimitiveIndex<i32>>,
// CPython parity: dont_inherit goes through PyObject_IsTrue, so
@@ -114,174 +119,67 @@ mod builtins {
dont_inherit: OptionalArg<ArgIntoBool>,
#[pyarg(any, optional)]
optimize: OptionalArg<ArgPrimitiveIndex<i32>>,
#[pyarg(any, optional)]
#[pyarg(named, optional)]
_feature_version: OptionalArg<i32>,
}
/// Detect PEP 263 encoding cookie from source bytes.
/// Checks first two lines for `# coding[:=] <encoding>` pattern.
/// Returns the encoding name if found, or None for default (UTF-8).
#[cfg(feature = "parser")]
fn detect_source_encoding(source: &[u8]) -> Option<String> {
fn find_encoding_in_line(line: &[u8]) -> Option<String> {
// PEP 263: '#' must be preceded only by whitespace/formfeed
let hash_pos = line.iter().position(|&b| b == b'#')?;
if !line[..hash_pos]
.iter()
.all(|&b| matches!(b, b' ' | b'\t' | b'\x0c' | b'\r'))
{
return None;
}
let after_hash = &line[hash_pos..];
// Find "coding" after the #
let coding_pos = after_hash.windows(6).position(|w| w == b"coding")?;
let after_coding = &after_hash[coding_pos + 6..];
// Next char must be ':' or '='
let rest = if matches!(after_coding.first(), Some(b':' | b'=')) {
&after_coding[1..]
} else {
return None;
};
// Skip whitespace
let rest = rest
.iter()
.copied()
.skip_while(|&b| matches!(b, b' ' | b'\t'))
.collect::<Vec<_>>();
// Read encoding name: [-\w.]+
let name = rest
.iter()
.take_while(|&&b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
.map(|&b| b as char)
.collect::<String>();
if name.is_empty() {
None
} else {
Some(normalize_source_encoding(&name))
}
fn merge_compile_future_features(
flags: i32,
dont_inherit: bool,
vm: &VirtualMachine,
) -> bytecode::CodeFlags {
let mut future_features = compile_future_features_from_flags(flags);
if !dont_inherit && let Some(frame) = vm.current_frame() {
future_features |= bytecode::CodeFlags::from_bits_truncate(
frame.code.flags.bits() & compile_future_feature_mask().bits(),
);
}
// Split into lines (first two only)
let mut lines = source.splitn(3, |&b| b == b'\n');
if let Some(first) = lines.next() {
// Strip BOM if present
let first = first.strip_prefix(b"\xef\xbb\xbf").unwrap_or(first);
if let Some(enc) = find_encoding_in_line(first) {
return Some(enc);
}
// Only check second line if first line is blank or a comment
let trimmed = first
.iter()
.find(|&&b| !matches!(b, b' ' | b'\t' | b'\x0c' | b'\r'))
.copied();
if trimmed.is_some_and(|b| b != b'#') {
return None;
}
}
lines.next().and_then(find_encoding_in_line)
future_features
}
/// Match CPython's Parser/tokenizer/helpers.c:get_normal_name().
#[cfg(feature = "parser")]
fn normalize_source_encoding(name: &str) -> String {
let mut normalized = String::with_capacity(name.len().min(12));
for ch in name.chars().take(12) {
if ch == '_' {
normalized.push('-');
} else {
normalized.push(ch.to_ascii_lowercase());
}
}
fn audit_compile_source(vm: &VirtualMachine, source: &[u8], filename: &str) -> PyResult<()> {
vm.sys_module.get_attr("audit", vm)?.call(
(
vm.ctx.new_str("compile"),
vm.ctx.new_bytes(source.to_vec()),
vm.ctx.new_str(filename),
),
vm,
)?;
Ok(())
}
if normalized == "utf-8" || normalized.starts_with("utf-8-") {
"utf-8".to_owned()
} else if normalized == "latin-1"
|| normalized == "iso-8859-1"
|| normalized == "iso-latin-1"
|| normalized.starts_with("latin-1-")
|| normalized.starts_with("iso-8859-1-")
|| normalized.starts_with("iso-latin-1-")
fn trim_eval_source_bytes(mut source: &[u8]) -> &[u8] {
while let Some((&first, rest)) = source.split_first()
&& matches!(first, b' ' | b'\t')
{
"iso-8859-1".to_owned()
} else {
name.to_owned()
source = rest;
}
source
}
/// Decode source bytes to a string, handling PEP 263 encoding declarations
/// and BOM. Raises SyntaxError for invalid UTF-8 without an encoding
/// declaration.
#[cfg(feature = "parser")]
fn is_utf8_encoding(name: &str) -> bool {
name == "utf-8"
}
#[cfg(feature = "parser")]
fn decode_source_bytes(source: &[u8], filename: &str, vm: &VirtualMachine) -> PyResult<String> {
let has_bom = source.starts_with(b"\xef\xbb\xbf");
let encoding = detect_source_encoding(source);
let is_utf8 = encoding.as_deref().is_none_or(is_utf8_encoding);
// Validate BOM + encoding combination
if has_bom && !is_utf8 {
let enc = encoding.as_deref().unwrap_or("utf-8");
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
format!("encoding problem: {enc} with BOM").into(),
));
fn decode_eval_exec_source_bytes(
vm: &VirtualMachine,
source: &[u8],
filename: &str,
) -> PyResult<String> {
#[cfg(feature = "parser")]
{
vm.decode_source_bytes(source, filename, false)
}
if is_utf8 {
let src = if has_bom { &source[3..] } else { source };
match core::str::from_utf8(src) {
Ok(s) => Ok(s.to_owned()),
Err(e) => {
let bad_byte = src[e.valid_up_to()];
let line = src[..e.valid_up_to()]
.iter()
.filter(|&&b| b == b'\n')
.count()
+ 1;
Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
format!(
"Non-UTF-8 code starting with '\\x{bad_byte:02x}' \
on line {line}, but no encoding declared; \
see https://peps.python.org/pep-0263/ for details \
({filename}, line {line})"
)
.into(),
))
}
}
} else {
// Use codec registry for non-UTF-8 encodings
let enc = encoding.as_deref().unwrap();
let bytes_obj = vm.ctx.new_bytes(source.to_vec());
let decoded = vm
.state
.codec_registry
.decode_text(bytes_obj.into(), enc, None, vm)
.map_err(|exc| {
if exc.fast_isinstance(vm.ctx.exceptions.lookup_error) {
vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
format!("unknown encoding for '{filename}': {enc}").into(),
)
} else {
exc
}
})?;
Ok(decoded.to_string_lossy().into_owned())
#[cfg(not(feature = "parser"))]
{
_ = filename;
core::str::from_utf8(source)
.map(str::to_owned)
.map_err(|err| {
let msg = format!(
"(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte",
source[err.valid_up_to()],
err.valid_up_to()
);
vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into())
})
}
}
@@ -303,30 +201,60 @@ mod builtins {
use crate::{class::PyClassImpl, stdlib::_ast};
let feature_version = feature_version_from_arg(args._feature_version, vm)?;
let feature_version = args._feature_version.into_option().unwrap_or(-1);
let mode_str = args.mode.as_str();
let flags: i32 = args.flags.map_or(0, |v| v.value);
let cf = CompilerFlags::from_bits_retain(flags);
if (flags & !CompilerFlags::ALLOWED_FLAGS.bits()) != 0 {
return Err(vm.new_value_error("compile(): unrecognised flags"));
}
let optimize: i32 = args.optimize.map_or(-1, |v| v.value);
let optimize: u8 = match optimize {
-1 => vm.state.config.settings.optimize,
-1 => vm.state.config.settings.optimize.min(2),
0..=2 => optimize as u8,
_ => return Err(vm.new_value_error("compile(): invalid optimize value")),
};
let dont_inherit = args.dont_inherit.map_or(false, ArgIntoBool::into_bool);
let is_ast_only = cf.contains(CompilerFlags::ONLY_AST);
let future_features = merge_compile_future_features(flags, dont_inherit, vm);
if args
.source
.fast_isinstance(&_ast::NodeAst::make_static_type())
{
let flags: i32 = args.flags.map_or(0, |v| v.value);
let is_ast_only = !(flags & _ast::PY_CF_ONLY_AST).is_zero();
// func_type mode requires PyCF_ONLY_AST
if mode_str == "func_type" && !is_ast_only {
let start = if mode_str == "exec" {
PY_FILE_INPUT
} else if mode_str == "eval" {
PY_EVAL_INPUT
} else if mode_str == "single" {
PY_SINGLE_INPUT
} else if mode_str == "func_type" {
if !is_ast_only {
return Err(vm.new_value_error(
"compile() mode 'func_type' requires flag PyCF_ONLY_AST",
));
}
PY_FUNC_TYPE_INPUT
} else {
let msg = if is_ast_only {
"compile() mode must be 'exec', 'eval', 'single' or 'func_type'"
} else {
"compile() mode must be 'exec', 'eval' or 'single'"
};
return Err(vm.new_value_error(msg));
};
let ast_type = _ast::NodeAst::make_static_type().as_object().to_owned();
if args.source.is_instance(&ast_type, vm)? {
let explicit_future_annotations =
future_features.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
vm.sys_module.get_attr("audit", vm)?.call(
(
vm.ctx.new_str("compile"),
args.source.clone(),
vm.ctx.none(),
),
vm,
)?;
// compile(ast_node, ..., PyCF_ONLY_AST) returns the AST after validation
if is_ast_only {
@@ -336,15 +264,29 @@ mod builtins {
"compile() mode must be 'exec', 'eval', 'single' or 'func_type'",
)
})?;
if !args.source.fast_isinstance(&expected_type) {
if !args.source.is_instance(expected_type.as_object(), vm)? {
return Err(vm.new_type_error(format!(
"expected {} node, got {}",
expected_name,
args.source.class().name()
)));
}
_ast::validate_ast_object(vm, args.source.clone())?;
return Ok(args.source);
#[cfg(not(feature = "rustpython-codegen"))]
{
_ast::validate_ast_object(vm, args.source.clone())?;
return Ok(args.source);
}
#[cfg(feature = "rustpython-codegen")]
{
return _ast::preprocess_ast_object(
vm,
args.source,
&filename.to_string_lossy(),
optimize,
cf.contains(CompilerFlags::OPTIMIZED_AST),
explicit_future_annotations,
);
}
}
#[cfg(not(feature = "rustpython-codegen"))]
@@ -353,133 +295,104 @@ mod builtins {
}
#[cfg(feature = "rustpython-codegen")]
{
let (expected_type, expected_name) = _ast::mode_type_and_name(mode_str)
.ok_or_else(|| {
vm.new_value_error("compile() mode must be 'exec', 'eval' or 'single'")
})?;
if !args.source.is_instance(expected_type.as_object(), vm)? {
return Err(vm.new_type_error(format!(
"expected {} node, got {}",
expected_name,
args.source.class().name()
)));
}
let mode = mode_str
.parse::<crate::compiler::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?;
return _ast::compile(
vm,
args.source,
&filename.to_string_lossy(),
mode,
Some(optimize),
);
let mut opts = vm.compile_opts();
opts.optimize = optimize;
opts.allow_top_level_await = cf.contains(CompilerFlags::ALLOW_TOP_LEVEL_AWAIT);
opts.future_features = future_features;
return _ast::compile(vm, args.source, &filename.to_string_lossy(), mode, opts);
}
}
#[cfg(not(feature = "parser"))]
return Err(vm.new_type_error(
"can't compile() source code when the `parser` feature of rustpython is disabled",
));
{
Err(vm.new_type_error(
"can't compile() source code when the `parser` feature of rustpython is disabled",
))
}
#[cfg(feature = "parser")]
{
use crate::convert::ToPyException;
use ruff_python_parser as parser;
let source = ArgStrOrBytesLike::try_from_object(vm, args.source)?;
let source = source.borrow_bytes();
let source = decode_source_bytes(&source, &filename.to_string_lossy(), vm)?;
let source = source.as_str();
let flags: i32 = args.flags.map_or(0, |v| v.value);
if !(flags & !_ast::PY_COMPILE_FLAGS_MASK).is_zero() {
return Err(vm.new_value_error("compile(): unrecognised flags"));
}
let allow_incomplete = !(flags & _ast::PY_CF_ALLOW_INCOMPLETE_INPUT).is_zero();
let type_comments = !(flags & _ast::PY_CF_TYPE_COMMENTS).is_zero();
let optimize_level = optimize;
if (flags & _ast::PY_CF_ONLY_AST).is_zero() {
#[cfg(not(feature = "compiler"))]
{
Err(vm.new_value_error(CODEGEN_NOT_SUPPORTED))
}
#[cfg(feature = "compiler")]
{
if let Some(feature_version) = feature_version {
let mode = mode_str
.parse::<parser::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?;
let _ = _ast::parse(
vm,
source,
mode,
optimize_level,
Some(feature_version),
type_comments,
)
.map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm))?;
}
let mode = mode_str
.parse::<crate::compiler::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?;
let mut opts = vm.compile_opts();
opts.optimize = optimize;
let code = vm
.compile_with_opts(source, mode, &filename.to_string_lossy(), opts)
.map_err(|err| {
(err, Some(source), allow_incomplete).to_pyexception(vm)
})?;
Ok(code.into())
}
} else {
if mode_str == "func_type" {
return _ast::parse_func_type(vm, source, optimize_level, feature_version)
.map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm));
}
let mode = mode_str
.parse::<parser::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?;
let parsed = _ast::parse(
vm,
let mut compile_flags = flags | future_features.bits() as i32;
#[cfg(feature = "rustpython-compiler")]
let compile_source = |source: &[u8], compile_flags: i32| {
vm.compile_string_object_with_flags(
source,
mode,
optimize_level,
&filename.to_string_lossy(),
start,
compile_flags,
feature_version,
type_comments,
optimize as i32,
)
.map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm))?;
if mode_str == "single" {
return _ast::wrap_interactive(vm, parsed);
};
match &source {
ArgStrOrBytesLike::Str(source) => {
if source.as_bytes().contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
audit_compile_source(
vm,
source.as_bytes(),
filename.to_string_lossy().as_ref(),
)?;
compile_flags |= CompilerFlags::IGNORE_COOKIE.bits();
#[cfg(feature = "rustpython-compiler")]
{
compile_source(source.as_bytes(), compile_flags)
}
#[cfg(not(feature = "rustpython-compiler"))]
{
Err(vm.new_value_error(CODEGEN_NOT_SUPPORTED))
}
}
ArgStrOrBytesLike::Buf(source) => {
let source_bytes = source.borrow_buf();
let source_bytes: &[u8] = &source_bytes;
if source_bytes.contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
audit_compile_source(
vm,
source_bytes,
filename.to_string_lossy().as_ref(),
)?;
#[cfg(feature = "rustpython-compiler")]
{
compile_source(source_bytes, compile_flags)
}
#[cfg(not(feature = "rustpython-compiler"))]
{
Err(vm.new_value_error(CODEGEN_NOT_SUPPORTED))
}
}
Ok(parsed)
}
}
}
}
#[cfg(feature = "ast")]
fn feature_version_from_arg(
feature_version: OptionalArg<i32>,
vm: &VirtualMachine,
) -> PyResult<Option<ruff_python_ast::PythonVersion>> {
let Some(minor) = feature_version.into_option() else {
return Ok(None);
};
if minor < 0 {
return Ok(None);
}
u8::try_from(minor)
.map(|v| Some(ruff_python_ast::PythonVersion { major: 3, minor: v }))
.map_err(|_| vm.new_value_error("compile() _feature_version out of range"))
}
#[pyfunction]
fn delattr(obj: PyObjectRef, attr: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
let attr = attr.try_to_ref::<PyStr>(vm).map_err(|_| {
let attr = attr.try_to_ref::<PyStr>(vm).map_err(|_e| {
vm.new_type_error(format!(
"attribute name must be string, not '{}'",
attr.class().name()
@@ -507,42 +420,39 @@ mod builtins {
}
impl ScopeArgs {
fn validate_globals_dict(
globals: &PyObject,
vm: &VirtualMachine,
func_name: &'static str,
) -> PyResult<()> {
if globals.fast_isinstance(vm.ctx.types.dict_type) {
return Ok(());
}
let msg = match func_name {
"eval" => {
let is_mapping = globals.mapping_unchecked().check();
if is_mapping {
"globals must be a real dict; try eval(expr, {}, mapping)".into()
} else {
"globals must be a dict".into()
}
}
"exec" => format!(
"exec() globals must be a dict, not {}",
globals.class().name()
),
_ => "globals must be a dict".into(),
};
Err(vm.new_type_error(msg))
}
fn make_scope(
self,
vm: &VirtualMachine,
func_name: &'static str,
) -> PyResult<crate::scope::Scope> {
fn validate_globals_dict(
globals: &PyObject,
vm: &VirtualMachine,
func_name: &'static str,
) -> PyResult<()> {
if !globals.fast_isinstance(vm.ctx.types.dict_type) {
return Err(match func_name {
"eval" => {
let is_mapping = globals.mapping_unchecked().check();
vm.new_type_error(if is_mapping {
"globals must be a real dict; try eval(expr, {}, mapping)"
} else {
"globals must be a dict"
})
}
"exec" => vm.new_type_error(format!(
"exec() globals must be a dict, not {}",
globals.class().name()
)),
_ => vm.new_type_error("globals must be a dict"),
});
}
Ok(())
}
let (globals, locals) = match self.globals {
Some(globals) => {
Self::validate_globals_dict(&globals, vm, func_name)?;
validate_globals_dict(&globals, vm, func_name)?;
let globals = PyDictRef::try_from_object(vm, globals)?;
if !globals.contains_key(identifier!(vm, __builtins__), vm) {
@@ -570,6 +480,61 @@ mod builtins {
}
}
#[derive(FromArgs)]
struct ExecArgs {
#[pyarg(positional)]
source: Either<ArgStrOrBytesLike, PyRef<crate::builtins::PyCode>>,
#[pyarg(any, default)]
globals: Option<PyObjectRef>,
#[pyarg(any, default)]
locals: Option<ArgMapping>,
#[pyarg(named, optional)]
closure: OptionalOption<PyObjectRef>,
}
fn exec_closure(
code_obj: &PyRef<crate::builtins::PyCode>,
closure: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<Option<PyRef<PyTuple<PyCellRef>>>> {
let num_free = code_obj.freevars.len();
let Some(closure) = closure else {
if num_free == 0 {
return Ok(None);
}
return Err(vm.new_type_error(format!(
"code object requires a closure of exactly length {num_free}"
)));
};
if num_free == 0 {
return Err(vm.new_type_error("cannot use a closure with this code object"));
}
let closure_tuple = closure
.downcast_exact::<PyTuple>(vm)
.map_err(|_| {
vm.new_type_error(format!(
"code object requires a closure of exactly length {num_free}"
))
})?
.into_pyref();
if closure_tuple.len() != num_free {
return Err(vm.new_type_error(format!(
"code object requires a closure of exactly length {num_free}"
)));
}
closure_tuple
.try_into_typed::<PyCell>(vm)
.map(Some)
.map_err(|_| {
vm.new_type_error(format!(
"code object requires a closure of exactly length {num_free}"
))
})
}
#[pyfunction]
fn eval(
source: Either<ArgStrOrBytesLike, PyRef<crate::builtins::PyCode>>,
@@ -581,38 +546,93 @@ mod builtins {
// source as string
let code = match source {
Either::A(either) => {
let source: &[u8] = &either.borrow_bytes();
if source.contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
let source = core::str::from_utf8(source).map_err(|err| {
let msg = format!(
"(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte",
source[err.valid_up_to()],
err.valid_up_to()
);
vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into())
})?;
Ok(Either::A(vm.ctx.new_utf8_str(source.trim_start())))
let source = match &either {
ArgStrOrBytesLike::Str(source) => {
if source.as_bytes().contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
let source = source.expect_str().trim_start_matches([' ', '\t']);
audit_compile_source(vm, source.as_bytes(), "<string>")?;
source.to_owned()
}
ArgStrOrBytesLike::Buf(source) => {
let source: &[u8] = &source.borrow_buf();
if source.contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
let source = trim_eval_source_bytes(source);
audit_compile_source(vm, source, "<string>")?;
decode_eval_exec_source_bytes(vm, source, "eval")?
}
};
Ok(Either::A(vm.ctx.new_utf8_str(source)))
}
Either::B(code) => Ok(Either::B(code)),
}?;
run_code(vm, code, scope, crate::compiler::Mode::Eval, "eval")
run_code(vm, code, scope, crate::compiler::Mode::Eval, "eval", None)
}
#[pyfunction]
fn exec(
source: Either<PyUtf8StrRef, PyRef<crate::builtins::PyCode>>,
scope: ScopeArgs,
vm: &VirtualMachine,
) -> PyResult {
let scope = scope.make_scope(vm, "exec")?;
run_code(vm, source, scope, crate::compiler::Mode::Exec, "exec")
fn exec(args: ExecArgs, vm: &VirtualMachine) -> PyResult {
let ExecArgs {
source,
globals,
locals,
closure,
} = args;
let scope = ScopeArgs { globals, locals }.make_scope(vm, "exec")?;
let closure = closure.flatten();
let (source, closure) = match source {
Either::A(either) => {
if closure.is_some() {
return Err(
vm.new_type_error("closure can only be used when source is a code object")
);
}
let source = match &either {
ArgStrOrBytesLike::Str(source) => {
if source.as_bytes().contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
audit_compile_source(vm, source.as_bytes(), "<string>")?;
source.expect_str().to_owned()
}
ArgStrOrBytesLike::Buf(source) => {
let source: &[u8] = &source.borrow_buf();
if source.contains(&0) {
return Err(vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code string cannot contain null bytes".into(),
));
}
audit_compile_source(vm, source, "<string>")?;
decode_eval_exec_source_bytes(vm, source, "exec")?
}
};
(Either::A(vm.ctx.new_utf8_str(source)), None)
}
Either::B(code) => {
let closure = exec_closure(&code, closure, vm)?;
(Either::B(code), closure)
}
};
run_code(
vm,
source,
scope,
crate::compiler::Mode::Exec,
"exec",
closure,
)
}
fn run_code(
@@ -621,28 +641,39 @@ mod builtins {
scope: crate::scope::Scope,
#[allow(unused_variables)] mode: crate::compiler::Mode,
func: &str,
closure: Option<PyRef<PyTuple<PyCellRef>>>,
) -> PyResult {
// Determine code object:
let code_obj = match source {
#[cfg(feature = "rustpython-compiler")]
Either::A(string) => {
let source = string.as_str();
vm.compile(source, mode, "<string>")
.map_err(|err| vm.new_syntax_error(&err, Some(source)))?
let mut opts = vm.compile_opts();
if let Some(frame) = vm.current_frame() {
opts.future_features = bytecode::CodeFlags::from_bits_truncate(
frame.code.flags.bits() & compile_future_feature_mask().bits(),
);
}
vm.compile_with_opts(source, mode, "<string>", opts)
.map_err(|err| err.into_pyexception(vm, Some(source)))?
}
#[cfg(not(feature = "rustpython-compiler"))]
Either::A(_) => return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED)),
Either::B(code_obj) => code_obj,
};
if !code_obj.freevars.is_empty() {
vm.sys_module
.get_attr("audit", vm)?
.call((vm.ctx.new_str("exec"), code_obj.clone()), vm)?;
if closure.is_none() && !code_obj.freevars.is_empty() {
return Err(vm.new_type_error(format!(
"code object passed to {func}() may not contain free variables"
)));
}
// Run the code:
vm.run_code_obj(code_obj, scope)
vm.run_code_obj_with_closure(code_obj, scope, closure)
}
#[pyfunction]
@@ -1002,8 +1033,8 @@ mod builtins {
modulus,
} = args;
let modulus = modulus
.as_ref()
.map_or_else(|| vm.ctx.none.as_object(), |m| m);
.as_deref()
.unwrap_or_else(|| vm.ctx.none.as_object());
vm._pow(&x, &y, modulus)
}
+50 -5
View File
@@ -751,7 +751,7 @@ pub mod sys {
.read_to_string(&mut source)
.map_err(|e| vm.new_os_error(format!("Error reading from stdin: {e}")))?;
vm.compile(&source, crate::compiler::Mode::Single, "<stdin>")
.map_err(|e| vm.new_os_error(format!("Error running stdin: {e}")))?;
.map_err(|e| e.into_pyexception(vm, Some(&source)))?;
Ok(())
}
@@ -1228,7 +1228,7 @@ pub mod sys {
vm.state.int_max_str_digits.store(maxdigits);
Ok(())
} else {
let error = format!("maxdigits must be 0 or larger than {threshold:?}");
let error = format!("maxdigits must be 0 or larger than {threshold}");
Err(vm.new_value_error(error))
}
}
@@ -1744,12 +1744,54 @@ pub mod sys {
}
for hook in hooks {
hook.call((event.clone(), args.clone()), vm)?;
call_audit_hook(&hook, event.clone().into(), args, vm)?;
}
Ok(())
}
fn audit_hook_can_trace(hook: &PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
match hook.get_attr("__cantrace__", vm) {
Ok(can_trace) => can_trace.try_to_bool(vm),
Err(exc)
if exc
.class()
.fast_issubclass(vm.ctx.exceptions.attribute_error) =>
{
Ok(false)
}
Err(exc) => Err(exc),
}
}
fn call_audit_hook(
hook: &PyObjectRef,
event: PyObjectRef,
args: &PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
// Tracing is suppressed while dispatching Python audit hooks,
// except for hooks that explicitly opt in with __cantrace__.
vm.enter_tracing();
let can_trace = audit_hook_can_trace(hook, vm);
let result = match can_trace {
Ok(can_trace) => {
if can_trace {
vm.leave_tracing();
}
let result = hook.call((event, args.clone()), vm).map(|_| ());
if can_trace {
vm.enter_tracing();
}
result
}
Err(exc) => Err(exc),
};
vm.leave_tracing();
result
}
#[pyfunction]
fn audit(event: PyStrRef, args: PosArgs, vm: &VirtualMachine) -> PyResult<()> {
if vm.audit_hooks.borrow().is_empty() {
@@ -1773,10 +1815,13 @@ pub mod sys {
let event: PyObjectRef = vm.ctx.new_str("sys.addaudithook").into();
for existing_hook in hooks {
let Err(exc) = existing_hook.call((event.clone(), args.clone()), vm) else {
let Err(exc) = call_audit_hook(&existing_hook, event.clone(), &args, vm) else {
continue;
};
if exc.class().fast_issubclass(vm.ctx.exceptions.runtime_error) {
if exc
.class()
.fast_issubclass(vm.ctx.exceptions.exception_type)
{
return Ok(());
}
return Err(exc);
+3 -1
View File
@@ -747,7 +747,7 @@ fn fire(
cb_extra: &[PyObjectRef],
) -> PyResult<()> {
// Prevent recursive event firing
if FIRING.with(|f| f.get()) {
if vm.tracing_is_suppressed() || FIRING.with(|f| f.get()) {
return Ok(());
}
@@ -795,6 +795,7 @@ fn fire(
let args = FuncArgs::from(args_vec);
FIRING.with(|f| f.set(true));
vm.enter_tracing();
let result = (|| {
for (tool, cb) in callbacks {
let result = cb.call(args.clone(), vm)?;
@@ -817,6 +818,7 @@ fn fire(
}
Ok(())
})();
vm.leave_tracing();
FIRING.with(|f| f.set(false));
result
}
File diff suppressed because it is too large. Load diff
+83
View File
@@ -0,0 +1,83 @@
use crate::bytecode;
pub(crate) const PY_SINGLE_INPUT: i32 = 256;
pub(crate) const PY_FILE_INPUT: i32 = 257;
pub(crate) const PY_EVAL_INPUT: i32 = 258;
pub(crate) const PY_FUNC_TYPE_INPUT: i32 = 345;
bitflags::bitflags! {
/// `PyCF_*` compiler flags together with the `__future__` `CO_FUTURE_*`
/// bits, mirroring `PyCompilerFlags.cf_flags`.
///
/// Caveat emptor: these flags are undocumented on purpose and depending on
/// their effect outside the standard library is **unsupported**.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct CompilerFlags: i32 {
const SOURCE_IS_UTF8 = 0x0100;
const DONT_IMPLY_DEDENT = 0x0200;
const ONLY_AST = 0x0400;
const IGNORE_COOKIE = 0x0800;
const TYPE_COMMENTS = 0x1000;
const ALLOW_TOP_LEVEL_AWAIT = 0x2000;
const ALLOW_INCOMPLETE_INPUT = 0x4000;
const OPTIMIZED_AST = 0x8000 | Self::ONLY_AST.bits();
// __future__ flags - sync with Lib/__future__.py and Include/cpython/compile.h.
const NESTED = 0x0010;
const FUTURE_DIVISION = 0x20000;
const FUTURE_ABSOLUTE_IMPORT = 0x40000;
const FUTURE_WITH_STATEMENT = 0x80000;
const FUTURE_PRINT_FUNCTION = 0x100000;
const FUTURE_UNICODE_LITERALS = 0x200000;
const FUTURE_BARRY_AS_BDFL = 0x400000;
const FUTURE_GENERATOR_STOP = 0x800000;
const FUTURE_ANNOTATIONS = 0x1000000;
}
}
impl CompilerFlags {
const FUTURE_MASK: Self = Self::FUTURE_DIVISION
.union(Self::FUTURE_ABSOLUTE_IMPORT)
.union(Self::FUTURE_WITH_STATEMENT)
.union(Self::FUTURE_PRINT_FUNCTION)
.union(Self::FUTURE_UNICODE_LITERALS)
.union(Self::FUTURE_BARRY_AS_BDFL)
.union(Self::FUTURE_GENERATOR_STOP)
.union(Self::FUTURE_ANNOTATIONS);
const MASK_OBSOLETE: Self = Self::NESTED;
const COMPILE_MASK: Self = Self::ONLY_AST
.union(Self::ALLOW_TOP_LEVEL_AWAIT)
.union(Self::TYPE_COMMENTS)
.union(Self::DONT_IMPLY_DEDENT)
.union(Self::ALLOW_INCOMPLETE_INPUT)
.union(Self::OPTIMIZED_AST);
pub(crate) const ALLOWED_FLAGS: Self = Self::FUTURE_MASK
.union(Self::MASK_OBSOLETE)
.union(Self::COMPILE_MASK);
}
// Python-visible `ast.PyCF_*` attribute values. The flags cross the
// `compile()` boundary as a plain `int`, so the exposed surface stays `i32`.
pub(crate) const PY_CF_SOURCE_IS_UTF8: i32 = CompilerFlags::SOURCE_IS_UTF8.bits();
pub(crate) const PY_CF_DONT_IMPLY_DEDENT: i32 = CompilerFlags::DONT_IMPLY_DEDENT.bits();
pub(crate) const PY_CF_ONLY_AST: i32 = CompilerFlags::ONLY_AST.bits();
pub(crate) const PY_CF_IGNORE_COOKIE: i32 = CompilerFlags::IGNORE_COOKIE.bits();
pub(crate) const PY_CF_TYPE_COMMENTS: i32 = CompilerFlags::TYPE_COMMENTS.bits();
pub(crate) const PY_CF_ALLOW_TOP_LEVEL_AWAIT: i32 = CompilerFlags::ALLOW_TOP_LEVEL_AWAIT.bits();
pub(crate) const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = CompilerFlags::ALLOW_INCOMPLETE_INPUT.bits();
pub(crate) const PY_CF_OPTIMIZED_AST: i32 = CompilerFlags::OPTIMIZED_AST.bits();
pub(crate) fn compile_future_feature_mask() -> bytecode::CodeFlags {
// RustPython accepts barry_as_FLUFL but leaves its parser mode disabled.
bytecode::CodeFlags::FUTURE_DIVISION
| bytecode::CodeFlags::FUTURE_ABSOLUTE_IMPORT
| bytecode::CodeFlags::FUTURE_WITH_STATEMENT
| bytecode::CodeFlags::FUTURE_PRINT_FUNCTION
| bytecode::CodeFlags::FUTURE_UNICODE_LITERALS
| bytecode::CodeFlags::FUTURE_GENERATOR_STOP
| bytecode::CodeFlags::FUTURE_ANNOTATIONS
}
pub(crate) fn compile_future_features_from_flags(flags: i32) -> bytecode::CodeFlags {
bytecode::CodeFlags::from_bits_truncate(flags as u32 & compile_future_feature_mask().bits())
}
+4 -2
View File
@@ -300,8 +300,10 @@ impl Default for InterpreterBuilder {
/// let scope = vm.new_scope_with_builtins();
/// let source = r#"print("Hello World!")"#;
/// let code_obj = vm.compile(
/// source, Mode::Exec, "<embedded>"
/// ).map_err(|err| vm.new_syntax_error(&err, Some(source))).unwrap();
/// source,
/// Mode::Exec,
/// "<embedded>",
/// ).map_err(|err| err.into_pyexception(vm, Some(source))).unwrap();
/// vm.run_code_obj(code_obj, scope).unwrap();
/// });
/// ```
+47 -26
View File
@@ -5,6 +5,9 @@
#[cfg(feature = "rustpython-compiler")]
mod compile;
pub(crate) mod compile_mode;
#[cfg(feature = "rustpython-compiler")]
pub use compile::VmCompileError;
mod context;
mod interpreter;
mod method;
@@ -82,6 +85,7 @@ pub struct VirtualMachine {
pub profile_func: RefCell<PyObjectRef>,
pub trace_func: RefCell<PyObjectRef>,
pub use_tracing: Cell<bool>,
tracing_depth: Cell<usize>,
pub recursion_limit: Cell<usize>,
pub(crate) signal_handlers: OnceCell<SignalHandlers>,
pub(crate) signal_rx: Option<signal::UserSignalReceiver>,
@@ -739,6 +743,7 @@ impl VirtualMachine {
profile_func,
trace_func,
use_tracing: Cell::new(false),
tracing_depth: Cell::new(0),
recursion_limit: Cell::new(if cfg!(debug_assertions) { 256 } else { 1000 }),
signal_handlers,
signal_rx: None,
@@ -1095,30 +1100,22 @@ impl VirtualMachine {
}
pub fn run_code_obj(&self, code: PyRef<PyCode>, scope: Scope) -> PyResult {
use crate::builtins::{PyFunction, PyModule};
self.run_code_obj_with_closure(code, scope, None)
}
// Create a function object for module code, similar to CPython's PyEval_EvalCode
let func = PyFunction::new(code.clone(), scope.globals.clone(), self)?;
let func_obj = func.into_ref(&self.ctx).into();
pub(crate) fn run_code_obj_with_closure(
&self,
code: PyRef<PyCode>,
scope: Scope,
closure: Option<PyRef<crate::builtins::PyTuple<crate::builtins::function::PyCellRef>>>,
) -> PyResult {
use crate::builtins::PyFunction;
// Extract builtins from globals["__builtins__"], like PyEval_EvalCode
let builtins = match scope
.globals
.get_item_opt(identifier!(self, __builtins__), self)?
{
Some(b) => {
if let Some(module) = b.downcast_ref::<PyModule>() {
module.dict().into()
} else {
b
}
}
None => self.builtins.dict().into(),
};
let frame =
Frame::new(code, scope, builtins, &[], Some(func_obj), false, self).into_ref(&self.ctx);
self.run_frame(frame)
// Create a function object for module code, similar to PyEval_EvalCode
let mut func = PyFunction::new(code, scope.globals.clone(), self)?;
func.closure = closure;
let func = func.into_ref(&self.ctx);
func.invoke_with_locals(FuncArgs::default(), scope.locals, self)
}
#[cold]
@@ -1434,9 +1431,11 @@ impl VirtualMachine {
}
/// Stack margin bytes (like _PyOS_STACK_MARGIN_BYTES).
/// 2048 * sizeof(void*) = 16KB for 64-bit.
/// The margin is doubled for debug/sanitized builds because frame
/// evaluation consumes more native stack in those configurations.
#[cfg_attr(any(miri, target_env = "musl"), allow(dead_code))]
const STACK_MARGIN_BYTES: usize = 2048 * core::mem::size_of::<usize>();
const STACK_MARGIN_BYTES: usize =
(if cfg!(debug_assertions) { 4096 } else { 2048 }) * core::mem::size_of::<usize>();
/// Get the stack boundaries using platform-specific APIs.
/// Returns (base, top) where base is the lowest address and top is the highest.
@@ -1709,11 +1708,33 @@ impl VirtualMachine {
#[cfg(feature = "rustpython-codegen")]
pub fn compile_opts(&self) -> crate::compiler::CompileOpts {
crate::compiler::CompileOpts {
optimize: self.state.config.settings.optimize,
optimize: self.state.config.settings.optimize.min(2),
debug_ranges: self.state.config.settings.code_debug_ranges,
int_max_str_digits: self.state.int_max_str_digits.load(),
allow_top_level_await: false,
future_features: crate::bytecode::CodeFlags::empty(),
dont_imply_dedent: false,
recursion_limit: self.recursion_limit.get(),
}
}
#[inline]
pub(crate) fn enter_tracing(&self) {
self.tracing_depth.set(self.tracing_depth.get() + 1);
}
#[inline]
pub(crate) fn leave_tracing(&self) {
let depth = self.tracing_depth.get();
debug_assert!(depth > 0);
self.tracing_depth.set(depth.saturating_sub(1));
}
#[inline]
pub(crate) fn tracing_is_suppressed(&self) -> bool {
self.tracing_depth.get() != 0
}
// To be called right before raising the recursion depth.
fn check_recursive_call(&self, _where: &str) -> PyResult<()> {
if self.recursion_depth.get() >= self.recursion_limit.get() {
@@ -2286,7 +2307,7 @@ mod tests {
let source = "from dir_module.dir_module_inner import value2";
let code_obj = vm
.compile(source, vm::compiler::Mode::Exec, "<embedded>")
.map_err(|err| vm.new_syntax_error(&err, Some(source)))
.map_err(|err| err.into_pyexception(vm, Some(source)))
.unwrap();
if let Err(e) = vm.run_code_obj(code_obj, scope) {
+13 -5
View File
@@ -22,7 +22,7 @@ impl VirtualMachine {
pub fn run_string(&self, scope: Scope, source: &str, source_path: &str) -> PyResult {
let code_obj = self
.compile(source, compiler::Mode::Exec, source_path)
.map_err(|err| self.new_syntax_error(&err, Some(source)))?;
.map_err(|err| err.into_pyexception(self, Some(source)))?;
// linecache._register_code(code, source, filename)
let _ = self.register_code_in_linecache(&code_obj, source);
self.run_code_obj(code_obj, scope)
@@ -47,7 +47,7 @@ impl VirtualMachine {
pub fn run_block_expr(&self, scope: Scope, source: &str) -> PyResult {
let code_obj = self
.compile(source, compiler::Mode::BlockExpr, "<embedded>")
.map_err(|err| self.new_syntax_error(&err, Some(source)))?;
.map_err(|err| err.into_pyexception(self, Some(source)))?;
self.run_code_obj(code_obj, scope)
}
}
@@ -105,11 +105,19 @@ mod file_run {
if path != "<stdin>" {
set_main_loader(module_dict, path, "SourceFileLoader", self)?;
}
match crate::host_env::fs::read_to_string(path) {
Ok(source) => {
match crate::host_env::fs::read(path) {
Ok(source_bytes) => {
if source_bytes.contains(&0) {
return Err(self.new_exception_msg(
self.ctx.exceptions.syntax_error.to_owned(),
"source code cannot contain null bytes".into(),
));
}
let source = String::from_utf8(source_bytes)
.map_err(|err| self.new_os_error(err.to_string()))?;
let code_obj = self
.compile(&source, compiler::Mode::Exec, path)
.map_err(|err| self.new_syntax_error(&err, Some(&source)))?;
.map_err(|err| err.into_pyexception(self, Some(&source)))?;
self.run_code_obj(code_obj, scope)?;
}
Err(err) => {
+1
View File
@@ -719,6 +719,7 @@ impl VirtualMachine {
profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())),
trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())),
use_tracing: Cell::new(use_tracing),
tracing_depth: Cell::new(0),
recursion_limit: self.recursion_limit.clone(),
signal_handlers: core::cell::OnceCell::new(),
signal_rx: None,
+82 -24
View File
@@ -54,15 +54,6 @@ impl SyntaxErrorInfo {
Self { msg, narrow_caret }
}
fn with_msg(&mut self, msg: &str) {
self.msg = msg.into();
}
#[cfg(feature = "parser")]
const fn with_narrow_caret(&mut self, narrow_caret: bool) {
self.narrow_caret = narrow_caret;
}
#[cfg(feature = "parser")]
#[must_use]
const fn handle_expected_token(expected: TokenKind, found: TokenKind) -> &'static str {
@@ -114,7 +105,7 @@ impl SyntaxErrorInfo {
}
ParseErrorType::InvalidStarredExpressionUsage => {
self.with_narrow_caret(true);
self.narrow_caret = true;
"invalid syntax".into()
}
@@ -134,7 +125,7 @@ impl SyntaxErrorInfo {
ParseErrorType::EmptyTypeParams => "Type parameter list cannot be empty".into(),
ParseErrorType::InvalidStarPatternUsage => {
self.with_narrow_caret(true);
self.narrow_caret = true;
"cannot use starred expression here".into()
}
@@ -179,12 +170,22 @@ impl SyntaxErrorInfo {
| ParseErrorType::SimpleAndCompoundStatementOnSameLine
| ParseErrorType::ExpectedExpression => "invalid syntax".into(),
ParseErrorType::OtherError(s) if s.starts_with("Expected an identifier") => {
"invalid syntax".into()
}
ParseErrorType::OtherError(s)
if s.starts_with("Expected an identifier, but found a keyword") =>
if s.eq_ignore_ascii_case(
"Expected a type parameter or the end of the type parameter list",
) =>
{
"invalid syntax".into()
}
ParseErrorType::OtherError(s) if s.eq_ignore_ascii_case("Expected a statement") => {
"invalid syntax".into()
}
ParseErrorType::OtherError(s)
if s.eq_ignore_ascii_case(
"bytes literal cannot be mixed with non-bytes literals",
@@ -262,7 +263,7 @@ impl SyntaxErrorInfo {
_ => return,
};
self.with_msg(&msg);
self.msg = msg;
}
}
@@ -576,6 +577,16 @@ impl VirtualMachine {
source: Option<&str>,
allow_incomplete: bool,
) -> PyBaseExceptionRef {
if matches!(
error,
crate::compiler::CompileError::Codegen(crate::compiler::codegen::error::CodegenError {
error: crate::compiler::codegen::error::CodegenErrorType::RecursionError,
..
})
) {
return self.new_recursion_error(error.to_string());
}
let incomplete_or_syntax = |allow| -> &'static Py<crate::builtins::PyType> {
if allow {
self.ctx.exceptions.incomplete_input_error
@@ -687,7 +698,9 @@ impl VirtualMachine {
raw_location,
..
}) => {
if s.starts_with("Expected an indented block after") {
if s.starts_with("Expected an indented block after")
|| s.starts_with("expected an indented block after")
{
if allow_incomplete {
// Check that all chars in the error are whitespace, if so, the source is
// incomplete. Otherwise, we've found code that might violates
@@ -717,6 +730,12 @@ impl VirtualMachine {
} else {
self.ctx.exceptions.indentation_error
}
} else if allow_incomplete
&& source.is_some_and(|source| {
raw_location.end().to_usize() >= source.len() && !source.ends_with('\n')
})
{
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
@@ -738,7 +757,29 @@ impl VirtualMachine {
let statement = source.and_then(|src| get_statement(src, error.location()));
let mut msg = error.to_string();
if let Some(msg) = msg.get_mut(..1) {
if !msg.starts_with("Exceeds the limit ")
&& !msg.starts_with("Did you mean ")
&& !msg.starts_with("Invalid star expression")
&& !msg.starts_with("Function parameters cannot be parenthesized")
&& !msg.starts_with("Lambda expression parameters cannot be parenthesized")
&& !msg.starts_with("Cannot have two type comments on def")
&& !msg.starts_with("Variable annotation syntax is")
&& !msg.starts_with("The '@' operator is")
&& !msg.starts_with("Async functions are")
&& !msg.starts_with("Async comprehensions are")
&& !msg.starts_with("Async for loops are")
&& !msg.starts_with("Async with statements are")
&& !msg.starts_with("Exception groups are")
&& !msg.starts_with("Positional-only parameters are")
&& !msg.starts_with("Pattern matching is")
&& !msg.starts_with("Type statement is")
&& !msg.starts_with("Type parameter lists are")
&& !msg.starts_with("Type parameter defaults are")
&& !msg.starts_with("Assignment expressions are")
&& !msg.starts_with("Await expressions are")
&& !msg.starts_with("Underscores in numeric literals are")
&& let Some(msg) = msg.get_mut(..1)
{
msg.make_ascii_lowercase();
}
@@ -753,17 +794,26 @@ impl VirtualMachine {
};
if syntax_error_type.is(self.ctx.exceptions.tab_error) {
syntax_error_info.with_msg("inconsistent use of tabs and spaces in indentation");
syntax_error_info.msg = "inconsistent use of tabs and spaces in indentation".to_owned();
}
if syntax_error_type.is(self.ctx.exceptions.incomplete_input_error) {
syntax_error_info.msg = "incomplete input".to_owned();
}
let SyntaxErrorInfo { msg, narrow_caret } = syntax_error_info;
let check_version_suite_error = msg.starts_with("Async functions are")
|| msg.starts_with("Async for loops are")
|| msg.starts_with("Async with statements are")
|| msg.starts_with("Exception groups are")
|| msg.starts_with("except expressions without parentheses are")
|| msg.starts_with("Pattern matching is");
let line_end_binary_operator_error = msg.starts_with("The '@' operator is");
let syntax_error = self.new_exception_msg(syntax_error_type, msg.into());
let (lineno, offset) = error.python_location();
let lineno = self.ctx.new_int(lineno);
let offset = self.ctx.new_int(offset);
let (lineno_raw, offset_raw) = error.python_location();
let lineno = self.ctx.new_int(lineno_raw);
let offset = self.ctx.new_int(offset_raw);
set_attrs!(
syntax_error.as_object(), self, unwrap,
"lineno" => lineno,
@@ -772,15 +822,23 @@ impl VirtualMachine {
// Set end_lineno and end_offset if available
if let Some((end_lineno, end_offset)) = error.python_end_location() {
let (end_lineno, end_offset) = if narrow_caret {
let (end_lineno, end_offset) = if check_version_suite_error
&& statement
.as_deref()
.and_then(|line| line.chars().next())
.is_some_and(|ch| ch.is_ascii_whitespace())
{
(end_lineno, -1)
} else if line_end_binary_operator_error && end_offset == offset_raw {
(end_lineno, (end_offset + 1) as isize)
} else if narrow_caret {
let (l, o) = error.python_location();
(l, o + 1)
(l, (o + 1) as isize)
} else {
(end_lineno, end_offset)
(end_lineno, end_offset as isize)
};
let end_lineno = self.ctx.new_int(end_lineno);
let end_offset = self.ctx.new_int(end_offset);
set_attrs!(
syntax_error.as_object(), self, unwrap,
"end_lineno" => end_lineno,
+1
View File
@@ -20,6 +20,7 @@ no-start-func = []
rustpython-common = { workspace = true }
rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, default-features = false, optional = true }
ruff_text_size = { workspace = true }
# make sure no threading! otherwise wasm build will fail
rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde", "wasmbind"] }
+5 -1
View File
@@ -70,7 +70,11 @@ pub mod eval {
if let Some(js_vars) = js_vars {
vm.add_to_scope("js_vars".into(), js_vars.into())?;
}
vm.run(source, mode, None)
if matches!(mode, Mode::Single) {
vm.exec_single(source, None)
} else {
vm.run(source, mode, None)
}
}
/// Evaluate Python code
+64 -7
View File
@@ -6,9 +6,14 @@ use crate::{
use alloc::rc::{Rc, Weak};
use core::cell::RefCell;
use js_sys::{Object, TypeError};
use ruff_text_size::Ranged;
use rustpython_vm::{
Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine, builtins::PyWeak,
compiler::Mode, function::ArgMapping, scope::Scope,
Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine,
builtins::PyWeak,
compiler::{self, Mode},
function::ArgMapping,
scope::Scope,
vm::VmCompileError,
};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
@@ -21,6 +26,25 @@ pub(crate) struct StoredVirtualMachine {
held_objects: RefCell<Vec<PyObjectRef>>,
}
fn compile_err_to_js(vm: &VirtualMachine, err: VmCompileError) -> JsValue {
match err {
VmCompileError::Compile(err) => convert::syntax_err(err).into(),
err => convert::py_err_to_js_err(vm, &err.into_pyexception(vm, None)),
}
}
fn statement_chunks(source: &str) -> Option<Vec<&str>> {
let module = compiler::parser::parse_module(source).ok()?.into_syntax();
module
.body
.iter()
.map(|stmt| {
let range = stmt.range();
source.get(range.start().to_usize()..range.end().to_usize())
})
.collect()
}
#[pymodule]
mod _window {
use super::{js_module, wasm_builtins};
@@ -263,8 +287,8 @@ impl WASMVirtualMachine {
) -> Result<(), JsValue> {
self.with_vm(|vm, _| {
let code = vm
.compile(source, Mode::Exec, &name)
.map_err(convert::syntax_err)?;
.compile(source, Mode::Exec, name.as_str())
.map_err(|err| compile_err_to_js(vm, err))?;
let attrs = vm.ctx.new_dict();
attrs
.set_item("__name__", vm.new_pyobj(name.as_str()), vm)
@@ -327,13 +351,46 @@ impl WASMVirtualMachine {
) -> Result<JsValue, JsValue> {
self.with_vm(|vm, StoredVirtualMachine { scope, .. }| {
let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned());
let code = vm.compile(source, mode, &source_path);
let code = code.map_err(convert::syntax_err)?;
let code = vm.compile(source, mode, source_path.as_str());
let code = code.map_err(|err| compile_err_to_js(vm, err))?;
let result = vm.run_code_obj(code, scope.clone());
convert::pyresult_to_js_result(vm, result)
})?
}
pub(crate) fn run_single(
&self,
source: &str,
source_path: Option<String>,
) -> Result<JsValue, JsValue> {
self.with_vm(|vm, StoredVirtualMachine { scope, .. }| {
let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned());
let Some(chunks) = statement_chunks(source) else {
let code = vm.compile(source, Mode::Single, source_path.as_str());
let code = code.map_err(|err| compile_err_to_js(vm, err))?;
let result = vm.run_code_obj(code, scope.clone());
return convert::pyresult_to_js_result(vm, result);
};
if chunks.is_empty() {
return Ok(convert::py_to_js(vm, vm.ctx.none()));
}
let displayhook = vm
.sys_module
.get_attr("displayhook", vm)
.map_err(|_| TypeError::new("lost sys.displayhook"))?;
let mut result = vm.ctx.none();
for chunk in chunks {
let code = vm.compile(chunk, Mode::BlockExpr, source_path.as_str());
let code = code.map_err(|err| compile_err_to_js(vm, err))?;
result = vm.run_code_obj(code, scope.clone()).into_js(vm)?;
displayhook.call((result.clone(),), vm).into_js(vm)?;
}
Ok(convert::py_to_js(vm, result))
})?
}
pub fn exec(&self, source: &str, source_path: Option<String>) -> Result<JsValue, JsValue> {
self.run(source, Mode::Exec, source_path)
}
@@ -348,6 +405,6 @@ impl WASMVirtualMachine {
source: &str,
source_path: Option<String>,
) -> Result<JsValue, JsValue> {
self.run(source, Mode::Single, source_path)
self.run_single(source, source_path)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ fn main() -> vm::PyResult<()> {
let source = r#"print("Hello World!")"#;
let code_obj = vm
.compile(source, vm::compiler::Mode::Exec, "<embedded>")
.map_err(|err| vm.new_syntax_error(&err, Some(source)))?;
.map_err(|err| err.into_pyexception(vm, Some(source)))?;
vm.run_code_obj(code_obj, scope)?;
+1 -1
View File
@@ -66,7 +66,7 @@ def fib(n):
// (note that this is only the case when compiler::Mode::Single is passed to vm.compile)
match vm
.compile(&input, vm::compiler::Mode::Single, "<embedded>")
.map_err(|err| vm.new_syntax_error(&err, Some(&input)))
.map_err(|err| err.into_pyexception(vm, Some(&input)))
.and_then(|code_obj| vm.run_code_obj(code_obj, scope.clone()))
{
Ok(output) => {
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more