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", "deoptimize",
"emscripten", "emscripten",
"excs", "excs",
"flufl",
"fnfe", "fnfe",
"fsdefault",
"ifexp", "ifexp",
"implicits",
"interps", "interps",
"jitted", "jitted",
"jitting", "jitting",
+32 -7
View File
@@ -39,11 +39,36 @@ runs:
- name: Install Linux dependencies - name: Install Linux dependencies
shell: bash shell: bash
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: > env:
sudo apt-get update 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 packages=()
${{ fromJSON(inputs.gcc-multilib) && 'gcc-multilib' || '' }} if [[ "$GCC_MULTILIB" == "true" ]]; then
${{ fromJSON(inputs.musl-tools) && 'musl-tools' || '' }} packages+=(gcc-multilib)
${{ fromJSON(inputs.clang) && 'clang' || '' }} fi
${{ fromJSON(inputs.gcc-aarch64-linux-gnu) && 'gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross' || '' }} 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]] [[package]]
name = "rustpython-ruff_python_ast" name = "rustpython-ruff_python_ast"
version = "0.15.8" version = "0.15.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
checksum = "f021ff72cabf5e2cd6d8ec8813d376a8445a228dc610ab56c27bd9054cda70d4"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bitflags 2.13.0", "bitflags 2.13.0",
@@ -3650,9 +3649,8 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-ruff_python_parser" name = "rustpython-ruff_python_parser"
version = "0.15.8" version = "0.15.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
checksum = "01e6ee78bd9671fb5766664b2695fe1f2a92a961f4d9101646c570d8acdb1e0b"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"bstr", "bstr",
@@ -3671,9 +3669,8 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-ruff_python_trivia" name = "rustpython-ruff_python_trivia"
version = "0.15.8" version = "0.15.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
checksum = "79e7cfd1056f3a02ff0d2d0e4474286ca963260782f878b7b81c1dd87432e682"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"rustpython-ruff_source_file", "rustpython-ruff_source_file",
@@ -3683,9 +3680,8 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-ruff_source_file" name = "rustpython-ruff_source_file"
version = "0.15.8" version = "0.15.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
checksum = "948107aad62ddb12a11fc7bf68a49e52a0b0a3737d415a2505e54f5a9edac737"
dependencies = [ dependencies = [
"memchr", "memchr",
"rustpython-ruff_text_size", "rustpython-ruff_text_size",
@@ -3693,9 +3689,8 @@ dependencies = [
[[package]] [[package]]
name = "rustpython-ruff_text_size" name = "rustpython-ruff_text_size"
version = "0.15.8" version = "0.15.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af"
checksum = "8291ee0f5a779e54ccd4e0151a0c426f8b49a123f99b5b6545db17ccdd4277aa"
dependencies = [ dependencies = [
"get-size2", "get-size2",
] ]
@@ -3855,6 +3850,7 @@ dependencies = [
"static_assertions", "static_assertions",
"strum", "strum",
"strum_macros", "strum_macros",
"thin-vec",
"thiserror", "thiserror",
"timsort", "timsort",
"wasm-bindgen", "wasm-bindgen",
@@ -3881,6 +3877,7 @@ dependencies = [
"js-sys", "js-sys",
"rustpython-common", "rustpython-common",
"rustpython-pylib", "rustpython-pylib",
"rustpython-ruff_text_size",
"rustpython-stdlib", "rustpython-stdlib",
"rustpython-vm", "rustpython-vm",
"serde-wasm-bindgen", "serde-wasm-bindgen",
@@ -4363,6 +4360,12 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "thin-vec"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" 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-wtf8 = { path = "crates/wtf8", version = "0.5.0" }
rustpython-doc = { path = "crates/doc", version = "0.5.0" } rustpython-doc = { path = "crates/doc", version = "0.5.0" }
# Use RustPython-packaged Ruff crates from the published fork while keeping # Use the RustPython Ruff fork for RustPython public `_ast` metadata.
# existing crate names in the codebase. ruff_python_parser = { package = "rustpython-ruff_python_parser", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.15.8" } ruff_python_ast = { package = "rustpython-ruff_python_ast", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.15.8" } ruff_text_size = { package = "rustpython-ruff_text_size", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.15.8" } ruff_source_file = { package = "rustpython-ruff_source_file", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
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" }
der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] } der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] }
phf = { version = "0.13.1", default-features = false, features = ["macros"]} 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 } textwrap = { version = "0.16.2", default-features = false }
termios = "0.3.3" termios = "0.3.3"
thiserror = "2.0" thiserror = "2.0"
thin-vec = "0.2.14"
timsort = "0.1.2" timsort = "0.1.2"
tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" } tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" }
icu_casemap = "2" icu_casemap = "2"
-10
View File
@@ -150,7 +150,6 @@ class AST_Tests(unittest.TestCase):
self.assertRaises(TypeError, ast.parse, ast.Constant(42), self.assertRaises(TypeError, ast.parse, ast.Constant(42),
optimize=optval) optimize=optval)
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_optimization_levels__debug__(self): def test_optimization_levels__debug__(self):
cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)] cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)]
for (optval, expected) in cases: for (optval, expected) in cases:
@@ -586,7 +585,6 @@ class AST_Tests(unittest.TestCase):
compile(m, "<test>", "exec") compile(m, "<test>", "exec")
self.assertIn("but got expr()", str(cm.exception)) self.assertIn("but got expr()", str(cm.exception))
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: expected str for name
def test_invalid_identifier(self): def test_invalid_identifier(self):
m = ast.Module([ast.Expr(ast.Name(42, ast.Load()))], []) m = ast.Module([ast.Expr(ast.Name(42, ast.Load()))], [])
ast.fix_missing_locations(m) ast.fix_missing_locations(m)
@@ -1365,7 +1363,6 @@ class CopyTests(unittest.TestCase):
self.assertIs(repl.ctx, context) self.assertIs(repl.ctx, context)
self.assertRaises(AttributeError, getattr, repl, 'extra') 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): def test_replace_reject_missing_field(self):
# case: warn if deleted field is not replaced # case: warn if deleted field is not replaced
node = ast.parse('x').body[0].value 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=[])", 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): def test_copy_location(self):
src = ast.parse('1 + 1', mode='eval') src = ast.parse('1 + 1', mode='eval')
src.body.right = ast.copy_location(ast.Constant(2), src.body.right) 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)])" "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): def test_increment_lineno(self):
src = ast.parse('1 + 1', mode='eval') src = ast.parse('1 + 1', mode='eval')
self.assertEqual(ast.increment_lineno(src, n=3), src) 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): def test_bad_integer(self):
# issue13436: Bad error message with invalid numeric values # issue13436: Bad error message with invalid numeric values
body = [ast.ImportFrom(module='time', body = [ast.ImportFrom(module='time',
@@ -3259,7 +3253,6 @@ class ASTConstructorTests(unittest.TestCase):
r"MyAttrs.__init__ got an unexpected keyword argument 'c'."): r"MyAttrs.__init__ got an unexpected keyword argument 'c'."):
obj = MyAttrs(c=3) obj = MyAttrs(c=3)
@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_fields_and_types_no_default(self): def test_fields_and_types_no_default(self):
class FieldsAndTypesNoDefault(ast.AST): class FieldsAndTypesNoDefault(ast.AST):
_fields = ('a',) _fields = ('a',)
@@ -3273,7 +3266,6 @@ class ASTConstructorTests(unittest.TestCase):
obj = FieldsAndTypesNoDefault(a=1) obj = FieldsAndTypesNoDefault(a=1)
self.assertEqual(obj.a, 1) self.assertEqual(obj.a, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered
def test_incomplete_field_types(self): def test_incomplete_field_types(self):
class MoreFieldsThanTypes(ast.AST): class MoreFieldsThanTypes(ast.AST):
_fields = ('a', 'b') _fields = ('a', 'b')
@@ -3293,7 +3285,6 @@ class ASTConstructorTests(unittest.TestCase):
self.assertEqual(obj.a, 1) self.assertEqual(obj.a, 1)
self.assertEqual(obj.b, 2) self.assertEqual(obj.b, 2)
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'str' but 'bytes' found.
def test_malformed_fields_with_bytes(self): def test_malformed_fields_with_bytes(self):
class BadFields(ast.AST): class BadFields(ast.AST):
_fields = (b'\xff'*64,) _fields = (b'\xff'*64,)
@@ -3713,7 +3704,6 @@ class ASTOptimizationTests(unittest.TestCase):
f"{ast.dump(optimized_tree)}", f"{ast.dump(optimized_tree)}",
) )
@unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags
def test_folding_format(self): def test_folding_format(self):
code = "'%s' % (a,)" code = "'%s' % (a,)"
-1
View File
@@ -77,7 +77,6 @@ class AuditTest(unittest.TestCase):
def test_open(self): def test_open(self):
self.do_test("test_open", os_helper.TESTFN) self.do_test("test_open", os_helper.TESTFN)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_cantrace(self): def test_cantrace(self):
self.do_test("test_cantrace") self.do_test("test_cantrace")
-3
View File
@@ -486,7 +486,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
msg=f"source={source} mode={mode}") msg=f"source={source} mode={mode}")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_top_level_await(self): def test_compile_top_level_await(self):
"""Test whether code with top level await can be compiled. """Test whether code with top level await can be compiled.
@@ -627,7 +626,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
exec(co, glob) exec(co, glob)
self.assertEqual(type(glob['ticker']()), AsyncGeneratorType) 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): def test_compile_ast(self):
args = ("a*__debug__", "f.py", "exec") args = ("a*__debug__", "f.py", "exec")
raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0] raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0]
@@ -1020,7 +1018,6 @@ class BuiltinTest(ComplexesAreIdenticalMixin, unittest.TestCase):
finally: finally:
sys.stdout = savestdout sys.stdout = savestdout
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument closure
def test_exec_closure(self): def test_exec_closure(self):
def function_without_closures(): def function_without_closures():
return 3 * 5 return 3 * 5
-1
View File
@@ -645,7 +645,6 @@ class CmdLineTest(unittest.TestCase):
self.assertNotIn("\f", text) self.assertNotIn("\f", text)
self.assertIn("\n 1 + 1 = 2\n ^^^^^\n", text) self.assertIn("\n 1 + 1 = 2\n ^^^^^\n", text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntaxerror_multi_line_fstring(self): def test_syntaxerror_multi_line_fstring(self):
script = 'foo = f"""{}\nfoo"""\n' script = 'foo = f"""{}\nfoo"""\n'
with os_helper.temp_dir() as script_dir: 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, self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename,
compile("a = 1\n", "def", 'single').co_filename) compile("a = 1\n", "def", 'single').co_filename)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 2
def test_warning(self): def test_warning(self):
# Test that the warning is only returned once. # Test that the warning is only returned once.
with warnings_helper.check_warnings( with warnings_helper.check_warnings(
-12
View File
@@ -209,7 +209,6 @@ class TestSpecifics(unittest.TestCase):
self.assertEqual(eval("0o777"), 511) self.assertEqual(eval("0o777"), 511)
self.assertEqual(eval("-0o0000010"), -8) self.assertEqual(eval("-0o0000010"), -8)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_int_literals_too_long(self): def test_int_literals_too_long(self):
n = 3000 n = 3000
source = f"a = 1\nb = 2\nc = {'3'*n}\nd = 4" 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', 'single')
self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'exec') self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile
def test_import(self): def test_import(self):
succeed = [ succeed = [
'import sys', 'import sys',
@@ -348,7 +346,6 @@ class TestSpecifics(unittest.TestCase):
l = lambda: "this is the only const" l = lambda: "this is the only const"
self.assertEqual(l.__code__.co_consts, ("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): def test_encoding(self):
code = b'# -*- coding: badencoding -*-\npass\n' code = b'# -*- coding: badencoding -*-\npass\n'
self.assertRaises(SyntaxError, compile, code, 'tmp', 'exec') self.assertRaises(SyntaxError, compile, code, 'tmp', 'exec')
@@ -465,7 +462,6 @@ class TestSpecifics(unittest.TestCase):
# See gh-113054 # See gh-113054
compile('if (5 if 5 else T): 0', '<eval>', 'exec') compile('if (5 if 5 else T): 0', '<eval>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_condition_expression_with_redundant_comparisons_compiles(self): def test_condition_expression_with_redundant_comparisons_compiles(self):
# See gh-113054, gh-114083 # See gh-113054, gh-114083
exprs = [ exprs = [
@@ -580,7 +576,6 @@ class TestSpecifics(unittest.TestCase):
compile(ast.fix_missing_locations(tree), "<file>", "exec") 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): def test_compile_ast(self):
fname = __file__ fname = __file__
if fname.lower().endswith('pyc'): if fname.lower().endswith('pyc'):
@@ -696,7 +691,6 @@ class TestSpecifics(unittest.TestCase):
self.compile_single("class T:\n pass") self.compile_single("class T:\n pass")
self.compile_single("c = '''\na=1\nb=2\nc=3\n'''") 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): def test_bad_single_statement(self):
self.assertInvalidSingle('1\n2') self.assertInvalidSingle('1\n2')
self.assertInvalidSingle('def f(): pass') self.assertInvalidSingle('def f(): pass')
@@ -708,7 +702,6 @@ class TestSpecifics(unittest.TestCase):
self.assertInvalidSingle('x = 5 # comment\nx = 6\n') self.assertInvalidSingle('x = 5 # comment\nx = 6\n')
self.assertInvalidSingle("c = '''\nd=1\n'''\na = 1\n\nb = 2\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): def test_particularly_evil_undecodable(self):
# Issue 24022 # Issue 24022
src = b'0000\x00\n00000000000\n\x00\n\x9e\n' 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] res = script_helper.run_python_until_end(fn)[0]
self.assertIn(b"source code cannot contain null bytes", res.err) 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): def test_yet_more_evil_still_undecodable(self):
# Issue #25388 # Issue #25388
src = b"#\x00\n#\xfd\n" src = b"#\x00\n#\xfd\n"
@@ -756,7 +748,6 @@ class TestSpecifics(unittest.TestCase):
# check_limit("a", " if a else a") # check_limit("a", " if a else a")
# check_limit("if a: pass", "\nelif a: pass", mode="exec") # 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): def test_null_terminated(self):
# The source code is null-terminated internally, but bytes-like # The source code is null-terminated internally, but bytes-like
# objects are accepted, which could be not terminated. # objects are accepted, which could be not terminated.
@@ -1673,7 +1664,6 @@ class TestSpecifics(unittest.TestCase):
self.assertRaises(NameError, ns['foo']) self.assertRaises(NameError, ns['foo'])
@unittest.expectedFailure # TODO: RUSTPYTHON; + [3, 5, 3, 5]
def test_compile_warnings(self): def test_compile_warnings(self):
# Each invocation of compile() emits compiler warnings, even if they # Each invocation of compile() emits compiler warnings, even if they
# have the same message and line number. # 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) self.assertEqual([wm.lineno for wm in caught], [3, 5] * 2)
@unittest.expectedFailure # TODO: RUSTPYTHON; + [5, 9]
def test_compile_warning_in_finally(self): def test_compile_warning_in_finally(self):
# Ensure that warnings inside finally blocks are # Ensure that warnings inside finally blocks are
# only emitted once despite the block being # only emitted once despite the block being
@@ -1742,7 +1731,6 @@ class TestSpecifics(unittest.TestCase):
self.assertEqual(wm.category, SyntaxWarning) self.assertEqual(wm.category, SyntaxWarning)
self.assertIn("\"is\" with 'int' literal", str(wm.message)) self.assertIn("\"is\" with 'int' literal", str(wm.message))
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.subTests('src', [ @support.subTests('src', [
textwrap.dedent(""" textwrap.dedent("""
def f(): def f():
-1
View File
@@ -231,7 +231,6 @@ class ExceptionTests(unittest.TestCase):
line = line.removeprefix('\ufeff') line = line.removeprefix('\ufeff')
self.assertIn(line, cm.exception.text) self.assertIn(line, cm.exception.text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_error_offset_continuation_characters(self): def test_error_offset_continuation_characters(self):
check = self.check check = self.check
check('"\\\n"(1 for c in I,\\\n\\', 2, 2) check('"\\\n"(1 for c in I,\\\n\\', 2, 2)
-5
View File
@@ -701,7 +701,6 @@ x = (
["f'{ {{}} }'", # dict in a set ["f'{ {{}} }'", # dict in a set
]) ])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_compile_time_concat(self): def test_compile_time_concat(self):
x = 'def' x = 'def'
self.assertEqual('abc' f'## {x}ghi', 'abc## defghi') self.assertEqual('abc' f'## {x}ghi', 'abc## defghi')
@@ -816,7 +815,6 @@ x = (
s = "f'{1}' 'x' 'y'" * 1024 s = "f'{1}' 'x' 'y'" * 1024
self.assertEqual(eval(s), '1xy' * 1024) self.assertEqual(eval(s), '1xy' * 1024)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_format_specifier_expressions(self): def test_format_specifier_expressions(self):
width = 10 width = 10
precision = 4 precision = 4
@@ -947,7 +945,6 @@ x = (
["f'{3)+(4}'", ["f'{3)+(4}'",
]) ])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_newlines_before_syntax_error(self): def test_newlines_before_syntax_error(self):
self.assertAllRaise(SyntaxError, self.assertAllRaise(SyntaxError,
"f-string: expecting a valid expression after '{'", "f-string: expecting a valid expression after '{'",
@@ -1031,7 +1028,6 @@ x = (
r"'\N{GREEK CAPITAL LETTER DELTA'", r"'\N{GREEK CAPITAL LETTER DELTA'",
]) ])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_backslashes_in_expression_part(self): def test_backslashes_in_expression_part(self):
self.assertEqual(f"{( self.assertEqual(f"{(
1 + 1 +
@@ -1732,7 +1728,6 @@ x = (
with self.assertRaisesRegex(ValueError, error_msg): with self.assertRaisesRegex(ValueError, error_msg):
f'{1:_,}' 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): def test_syntax_error_for_starred_expressions(self):
with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"): with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"):
compile("f'{*a}'", "?", "exec") 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 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): def test_unknown_future_flag(self):
code = """ code = """
from __future__ import nested_scopes from __future__ import nested_scopes
@@ -135,14 +134,12 @@ class FutureTest(unittest.TestCase):
""" """
self.assertSyntaxError(code, offset=54) self.assertSyntaxError(code, offset=54)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 24
def test_future_import_star(self): def test_future_import_star(self):
code = """ code = """
from __future__ import * from __future__ import *
""" """
self.assertSyntaxError(code, message='future feature * is not defined', offset=24) self.assertSyntaxError(code, message='future feature * is not defined', offset=24)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_import_braces(self): def test_future_import_braces(self):
code = """ code = """
from __future__ import braces from __future__ import braces
@@ -188,7 +185,6 @@ class FutureTest(unittest.TestCase):
out = kill_python(p) out = kill_python(p)
self.assertNotIn(b'SyntaxError: invalid syntax', out) self.assertNotIn(b'SyntaxError: invalid syntax', out)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_dotted_import(self): def test_future_dotted_import(self):
with self.assertRaises(ImportError): with self.assertRaises(ImportError):
exec("from .__future__ import spam") exec("from .__future__ import spam")
@@ -480,7 +476,6 @@ class AnnotationsFutureTestCase(unittest.TestCase):
self.assertEqual(foo.__code__.co_cellvars, ()) self.assertEqual(foo.__code__.co_cellvars, ())
self.assertEqual(foo().__code__.co_freevars, ()) self.assertEqual(foo().__code__.co_freevars, ())
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
def test_annotations_forbidden(self): def test_annotations_forbidden(self):
with self.assertRaises(SyntaxError): with self.assertRaises(SyntaxError):
self._exec_future("test: (yield)") 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 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): Traceback (most recent call last):
... ...
SyntaxError: 'generator expression' is an illegal expression for augmented assignment 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 ### Syntax error cases as covered in Python/symtable.c
###################################################### ######################################################
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_param(self): def test_name_param(self):
prog_text = """\ prog_text = """\
def fn(name_param): def fn(name_param):
@@ -36,7 +35,6 @@ def fn(name_param):
""" """
check_syntax_error(self, prog_text, lineno=2, offset=5) check_syntax_error(self, prog_text, lineno=2, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_after_assign(self): def test_name_after_assign(self):
prog_text = """\ prog_text = """\
def fn(): def fn():
@@ -45,7 +43,6 @@ def fn():
""" """
check_syntax_error(self, prog_text, lineno=3, offset=5) check_syntax_error(self, prog_text, lineno=3, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_after_use(self): def test_name_after_use(self):
prog_text = """\ prog_text = """\
def fn(): def fn():
@@ -54,7 +51,6 @@ def fn():
""" """
check_syntax_error(self, prog_text, lineno=3, offset=5) check_syntax_error(self, prog_text, lineno=3, offset=5)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5
def test_name_annot(self): def test_name_annot(self):
prog_text_3 = """\ prog_text_3 = """\
def fn(): def fn():
-11
View File
@@ -114,7 +114,6 @@ class TokenTests(unittest.TestCase):
# Sanity check: no literal begins with an underscore # Sanity check: no literal begins with an underscore
self.assertRaises(NameError, eval, "_0") self.assertRaises(NameError, eval, "_0")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_bad_numerical_literals(self): def test_bad_numerical_literals(self):
check = self.check_syntax_error check = self.check_syntax_error
check("0b12", "invalid digit '2' in binary literal") check("0b12", "invalid digit '2' in binary literal")
@@ -137,7 +136,6 @@ class TokenTests(unittest.TestCase):
check("1e2_", "invalid decimal literal") check("1e2_", "invalid decimal literal")
check("1e+", "invalid decimal literal") check("1e+", "invalid decimal literal")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_end_of_numerical_literals(self): def test_end_of_numerical_literals(self):
def check(test, error=False): def check(test, error=False):
with self.subTest(expr=test): with self.subTest(expr=test):
@@ -251,7 +249,6 @@ the \'lazy\' dog.\n\
compile(s, "<test>", "exec") compile(s, "<test>", "exec")
self.assertIn("was never closed", str(cm.exception)) self.assertIn("was never closed", str(cm.exception))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised
@skip_wasi_stack_overflow() @skip_wasi_stack_overflow()
def test_max_level(self): def test_max_level(self):
# Macro defined in Parser/lexer/state.h # Macro defined in Parser/lexer/state.h
@@ -298,7 +295,6 @@ class GrammarTests(unittest.TestCase):
my_lst[one()-1]: int = 5 my_lst[one()-1]: int = 5
self.assertEqual(my_lst, [5]) self.assertEqual(my_lst, [5])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_var_annot_syntax_errors(self): def test_var_annot_syntax_errors(self):
# parser pass # parser pass
check_syntax_error(self, "def f: int") check_syntax_error(self, "def f: int")
@@ -751,7 +747,6 @@ class GrammarTests(unittest.TestCase):
# Check the heuristic for print & exec covers significant cases # Check the heuristic for print & exec covers significant cases
# As well as placing some limits on false positives # As well as placing some limits on false positives
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_former_statements_refer_to_builtins(self): def test_former_statements_refer_to_builtins(self):
keywords = "print", "exec" keywords = "print", "exec"
# Cases where we want the custom error # Cases where we want the custom error
@@ -1165,7 +1160,6 @@ class GrammarTests(unittest.TestCase):
""", """,
True) True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_yield(self): def test_yield(self):
# Allowed as standalone statement # Allowed as standalone statement
def g(): yield 1 def g(): yield 1
@@ -1205,7 +1199,6 @@ class GrammarTests(unittest.TestCase):
# Check annotation refleak on SyntaxError # Check annotation refleak on SyntaxError
check_syntax_error(self, "def g(a:(yield)): pass") check_syntax_error(self, "def g(a:(yield)): pass")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_yield_in_comprehensions(self): def test_yield_in_comprehensions(self):
# Check yield in comprehensions # Check yield in comprehensions
def g(): [x for x in [(yield 1)]] def g(): [x for x in [(yield 1)]]
@@ -1302,7 +1295,6 @@ class GrammarTests(unittest.TestCase):
else: else:
self.fail("AssertionError not raised by 'assert False'") self.fail("AssertionError not raised by 'assert False'")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_assert_syntax_warnings(self): def test_assert_syntax_warnings(self):
# Ensure that we warn users if they provide a non-zero length tuple as # Ensure that we warn users if they provide a non-zero length tuple as
# the assertion test. # the assertion test.
@@ -1317,7 +1309,6 @@ class GrammarTests(unittest.TestCase):
compile('assert x, "msg"', '<testcase>', 'exec') compile('assert x, "msg"', '<testcase>', 'exec')
compile('assert False, "msg"', '<testcase>', 'exec') compile('assert False, "msg"', '<testcase>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_assert_warning_promotes_to_syntax_error(self): def test_assert_warning_promotes_to_syntax_error(self):
# If SyntaxWarning is configured to be an error, it actually raises a # If SyntaxWarning is configured to be an error, it actually raises a
# SyntaxError. # SyntaxError.
@@ -1496,7 +1487,6 @@ class GrammarTests(unittest.TestCase):
if 1 not in (): pass if 1 not in (): pass
if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in x is x is not x: 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 test_comparison_is_literal(self):
def check(test, msg): def check(test, msg):
self.check_syntax_warning(test, msg) self.check_syntax_warning(test, msg)
@@ -1526,7 +1516,6 @@ class GrammarTests(unittest.TestCase):
compile('True is x', '<testcase>', 'exec') compile('True is x', '<testcase>', 'exec')
compile('... is x', '<testcase>', 'exec') compile('... is x', '<testcase>', 'exec')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_warn_missed_comma(self): def test_warn_missed_comma(self):
def check(test): def check(test):
self.check_syntax_warning(test, msg) self.check_syntax_warning(test, msg)
-1
View File
@@ -219,7 +219,6 @@ class ListComprehensionTest(unittest.TestCase):
""" """
self._check_in_scopes(code, raises=NameError) 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): def test_references___conditional_annotations___nested(self):
code = """ code = """
class i: [lambda: __conditional_annotations__ for x in y] class i: [lambda: __conditional_annotations__ for x in y]
-5
View File
@@ -4,35 +4,30 @@ GLOBAL_VAR = None
class NamedExpressionInvalidTest(unittest.TestCase): class NamedExpressionInvalidTest(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_01(self): def test_named_expression_invalid_01(self):
code = """x := 0""" code = """x := 0"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"): with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {}) exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_02(self): def test_named_expression_invalid_02(self):
code = """x = y := 0""" code = """x = y := 0"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"): with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {}) exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_03(self): def test_named_expression_invalid_03(self):
code = """y := f(x)""" code = """y := f(x)"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"): with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {}) exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_04(self): def test_named_expression_invalid_04(self):
code = """y0 = y1 := f(x)""" code = """y0 = y1 := f(x)"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"): with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {}) exec(code, {}, {})
@unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message
def test_named_expression_invalid_06(self): def test_named_expression_invalid_06(self):
code = """((a, b) := (1, 2))""" 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(S3()), "seq")
self.assertEqual(self.check_mapping_then_sequence(S4()), "seq") self.assertEqual(self.check_mapping_then_sequence(S4()), "seq")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_late_registration_mapping(self): def test_late_registration_mapping(self):
class Parent: class Parent:
pass 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(ChildPost()), "map")
self.assertEqual(self.check_mapping_then_sequence(GrandchildPost()), "map") self.assertEqual(self.check_mapping_then_sequence(GrandchildPost()), "map")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_late_registration_sequence(self): def test_late_registration_sequence(self):
class Parent: class Parent:
pass pass
@@ -2246,7 +2244,6 @@ class TestPatma(unittest.TestCase):
self.assertEqual(f(None), {}) self.assertEqual(f(None), {})
self.assertEqual(f((1, 2)), {}) self.assertEqual(f((1, 2)), {})
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patma_210(self): def test_patma_210(self):
def f(w): def f(w):
match w: match w:
@@ -2955,7 +2952,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_invalid_syntax_3(self): def test_invalid_syntax_3(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3075,7 +3071,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patterns_may_only_match_literals_and_attribute_lookups_0(self): def test_patterns_may_only_match_literals_and_attribute_lookups_0(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3083,7 +3078,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_patterns_may_only_match_literals_and_attribute_lookups_1(self): def test_patterns_may_only_match_literals_and_attribute_lookups_1(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3126,7 +3120,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_real_number_wrong_ops(self): def test_real_number_wrong_ops(self):
for op in ["*", "/", "@", "**", "%", "//"]: for op in ["*", "/", "@", "**", "%", "//"]:
with self.subTest(op=op): with self.subTest(op=op):
@@ -3202,7 +3195,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case0(self): def test_mapping_pattern_duplicate_key_edge_case0(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3210,7 +3202,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case1(self): def test_mapping_pattern_duplicate_key_edge_case1(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3225,8 +3216,6 @@ class TestSyntaxErrors(unittest.TestCase):
pass pass
""") """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_duplicate_key_edge_case3(self): def test_mapping_pattern_duplicate_key_edge_case3(self):
self.assert_syntax_error(""" self.assert_syntax_error("""
match ...: match ...:
@@ -3258,7 +3247,6 @@ class TestTypeErrors(unittest.TestCase):
self.assertEqual(x, range(10)) self.assertEqual(x, range(10))
self.assertIs(y, None) self.assertIs(y, None)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_got_multiple_subpatterns_for_attribute_0(self): def test_got_multiple_subpatterns_for_attribute_0(self):
class Class: class Class:
__match_args__ = ("a", "a") __match_args__ = ("a", "a")
@@ -3273,7 +3261,6 @@ class TestTypeErrors(unittest.TestCase):
self.assertIs(y, None) self.assertIs(y, None)
self.assertIs(z, None) self.assertIs(z, None)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_got_multiple_subpatterns_for_attribute_1(self): def test_got_multiple_subpatterns_for_attribute_1(self):
class Class: class Class:
__match_args__ = ("a",) __match_args__ = ("a",)
@@ -3379,7 +3366,6 @@ class TestTypeErrors(unittest.TestCase):
class TestValueErrors(unittest.TestCase): class TestValueErrors(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_mapping_pattern_checks_duplicate_key_1(self): def test_mapping_pattern_checks_duplicate_key_1(self):
class Keys: class Keys:
KEY = "a" KEY = "a"
+3 -3
View File
@@ -2165,7 +2165,7 @@ if not SKIP_CORO_TESTS:
>>> def test_function(): >>> def test_function():
... asyncio.run(main(), loop_factory=asyncio.EventLoop) ... asyncio.run(main(), loop_factory=asyncio.EventLoop)
>>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE >>> with PdbTestInput([ # doctest: +ELLIPSIS
... 'x = await task', ... 'x = await task',
... 'p x', ... 'p x',
... 'x = await test()', ... 'x = await test()',
@@ -2280,7 +2280,7 @@ if not SKIP_CORO_TESTS:
>>> def test_function(): >>> def test_function():
... asyncio.run(main(), loop_factory=asyncio.EventLoop) ... asyncio.run(main(), loop_factory=asyncio.EventLoop)
>>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE >>> with PdbTestInput([
... 'p var.get()', ... 'p var.get()',
... 'print(await get_var())', ... 'print(await get_var())',
... 'print(await asyncio.create_task(set_var(100)))', ... 'print(await asyncio.create_task(set_var(100)))',
@@ -2768,7 +2768,7 @@ def test_pdb_multiline_statement():
>>> def test_function(): >>> def test_function():
... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() ... 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):', ... 'def f(x):',
... ' return x * 2', ... ' return x * 2',
... '', ... '',
-1
View File
@@ -157,7 +157,6 @@ class TestTranforms(BytecodeTestCase):
self.assertNotInBytecode(code, 'UNPACK_SEQUENCE') self.assertNotInBytecode(code, 'UNPACK_SEQUENCE')
self.check_lnotab(code) self.check_lnotab(code)
@unittest.expectedFailure # TODO: RUSTPYTHON; LOAD_CONST count mismatch in long-tuple branch
def test_constant_folding_tuples_of_constants(self): def test_constant_folding_tuples_of_constants(self):
for line, elem in ( for line, elem in (
('a = 1,2,3', (1, 2, 3)), ('a = 1,2,3', (1, 2, 3)),
+1 -1
View File
@@ -312,7 +312,7 @@ is *not* valid syntax.)
>>> f4.__annotations__ >>> f4.__annotations__
{'args': StarredB, 'arg1': <class 'int'>} {'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): Traceback (most recent call last):
... ...
SyntaxError: invalid syntax SyntaxError: invalid syntax
-1
View File
@@ -932,7 +932,6 @@ class PydocDocTest(unittest.TestCase):
synopsis = pydoc.synopsis(TESTFN, {}) synopsis = pydoc.synopsis(TESTFN, {})
self.assertEqual(synopsis, 'line 1: h\xe9') self.assertEqual(synopsis, 'line 1: h\xe9')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_source_synopsis(self): def test_source_synopsis(self):
def check(source, expected, encoding=None): def check(source, expected, encoding=None):
if isinstance(source, str): if isinstance(source, str):
-7
View File
@@ -117,7 +117,6 @@ class TestSimpleInteract(unittest.TestCase):
SyntaxError: duplicate argument 'x' in function definition""" SyntaxError: duplicate argument 'x' in function definition"""
self.assertIn(r, f.getvalue()) self.assertIn(r, f.getvalue())
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_runsource_shows_syntax_error_for_failed_compilation(self): def test_runsource_shows_syntax_error_for_failed_compilation(self):
console = InteractiveColoredConsole() console = InteractiveColoredConsole()
source = "print('Hello, world!'" source = "print('Hello, world!'"
@@ -133,7 +132,6 @@ SyntaxError: duplicate argument 'x' in function definition"""
console.runsource(source) console.runsource(source)
mock_showsyntaxerror.assert_called_once() mock_showsyntaxerror.assert_called_once()
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_runsource_survives_null_bytes(self): def test_runsource_survives_null_bytes(self):
console = InteractiveColoredConsole() console = InteractiveColoredConsole()
source = "\x00\n" source = "\x00\n"
@@ -155,7 +153,6 @@ SyntaxError: duplicate argument 'x' in function definition"""
self.assertFalse(result) self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n") self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_annotations(self): def test_future_annotations(self):
console = InteractiveColoredConsole() console = InteractiveColoredConsole()
source = dedent("""\ source = dedent("""\
@@ -210,7 +207,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>") console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code)) self.assertFalse(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiline_single_block(self): def test_multiline_single_block(self):
namespace = {} namespace = {}
code = dedent("""\ code = dedent("""\
@@ -227,7 +223,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>") console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code)) self.assertFalse(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiple_statements(self): def test_multiple_statements(self):
namespace = {} namespace = {}
code = dedent("""\ code = dedent("""\
@@ -237,7 +232,6 @@ class TestMoreLines(unittest.TestCase):
console = InteractiveColoredConsole(namespace, filename="<stdin>") console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code)) self.assertTrue(_more_lines(console, code))
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiple_blocks(self): def test_multiple_blocks(self):
namespace = {} namespace = {}
code = dedent("""\ code = dedent("""\
@@ -285,7 +279,6 @@ class TestMoreLines(unittest.TestCase):
class TestWarnings(unittest.TestCase): class TestWarnings(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_pep_765_warning(self): def test_pep_765_warning(self):
""" """
Test that a SyntaxWarning emitted from the Test that a SyntaxWarning emitted from the
-10
View File
@@ -466,7 +466,6 @@ class TestPyReplAutoindent(TestCase):
reader = ReadlineAlikeReader(console=console, config=config) reader = ReadlineAlikeReader(console=console, config=config)
return reader return reader
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_default(self): def test_auto_indent_default(self):
# fmt: off # fmt: off
input_code = ( input_code = (
@@ -486,7 +485,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, output_code) self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_continuation(self): def test_auto_indent_continuation(self):
# auto indenting according to previous user indentation # auto indenting according to previous user indentation
# fmt: off # fmt: off
@@ -514,7 +512,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, output_code) self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_prev_block(self): def test_auto_indent_prev_block(self):
# auto indenting according to indentation in different block # auto indenting according to indentation in different block
# fmt: off # fmt: off
@@ -546,7 +543,6 @@ class TestPyReplAutoindent(TestCase):
output2 = multiline_input(reader) output2 = multiline_input(reader)
self.assertEqual(output2, output_code) self.assertEqual(output2, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_multiline(self): def test_auto_indent_multiline(self):
# fmt: off # fmt: off
events = itertools.chain( events = itertools.chain(
@@ -586,7 +582,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, output_code) self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_with_comment(self): def test_auto_indent_with_comment(self):
# fmt: off # fmt: off
events = code_to_events( events = code_to_events(
@@ -605,7 +600,6 @@ class TestPyReplAutoindent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, output_code) self.assertEqual(output, output_code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_auto_indent_with_multicomment(self): def test_auto_indent_with_multicomment(self):
# fmt: off # fmt: off
events = code_to_events( events = code_to_events(
@@ -680,7 +674,6 @@ class TestPyReplOutput(ScreenEqualMixin, TestCase):
wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader) wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader)
self.assertIs(type(wrapper.get_line_buffer()), str) self.assertIs(type(wrapper.get_line_buffer()), str)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_multiline_edit(self): def test_multiline_edit(self):
events = itertools.chain( events = itertools.chain(
code_to_events("def f():\n...\n\n"), code_to_events("def f():\n...\n\n"),
@@ -744,7 +737,6 @@ class TestPyReplOutput(ScreenEqualMixin, TestCase):
self.assertEqual(output, "1+1") self.assertEqual(output, "1+1")
self.assert_screen_equal(reader, "1+1", clean=True) self.assert_screen_equal(reader, "1+1", clean=True)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_history_with_multiline_entries(self): def test_history_with_multiline_entries(self):
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n" code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
events = list(itertools.chain( events = list(itertools.chain(
@@ -1426,7 +1418,6 @@ class TestPasteEvent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, code) self.assertEqual(output, code)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_paste_mid_newlines_not_in_paste_mode(self): def test_paste_mid_newlines_not_in_paste_mode(self):
# fmt: off # fmt: off
code = ( code = (
@@ -1448,7 +1439,6 @@ class TestPasteEvent(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, expected) self.assertEqual(output, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_paste_not_in_paste_mode(self): def test_paste_not_in_paste_mode(self):
# fmt: off # fmt: off
input_code = ( input_code = (
-1
View File
@@ -180,7 +180,6 @@ class TestReader(ScreenEqualMixin, TestCase):
reader, _ = handle_all_events(events) reader, _ = handle_all_events(events)
self.assert_screen_equal(reader, "") self.assert_screen_equal(reader, "")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Lists differ
def test_newline_within_block_trailing_whitespace(self): def test_newline_within_block_trailing_whitespace(self):
# fmt: off # fmt: off
code = ( code = (
-1
View File
@@ -437,7 +437,6 @@ class TestAsyncioREPL(unittest.TestCase):
expected = "toplevel contextvar test: ok" expected = "toplevel contextvar test: ok"
self.assertIn(expected, output, expected) self.assertIn(expected, output, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 != 0
def test_toplevel_contextvars_async(self): def test_toplevel_contextvars_async(self):
user_input = dedent("""\ user_input = dedent("""\
from contextvars import ContextVar from contextvars import ContextVar
-1
View File
@@ -198,7 +198,6 @@ class SymtableTest(unittest.TestCase):
T = find_block(GenericMine, "T") T = find_block(GenericMine, "T")
U = find_block(GenericMine, "U") U = find_block(GenericMine, "U")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <SymbolTableType.ANNOTATION: 'annotation'> != 'type alias'
def test_type(self): def test_type(self):
self.assertEqual(self.top.get_type(), "module") self.assertEqual(self.top.get_type(), "module")
self.assertEqual(self.Mine.get_type(), "class") 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), (1, 'return', g_ident),
]) ])
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_exception_propagation(self): def test_exception_propagation(self):
def f(p): def f(p):
1/0 1/0
-15
View File
@@ -252,7 +252,6 @@ class TypeCommentTests(unittest.TestCase):
def classic_parse(self, source): def classic_parse(self, source):
return ast.parse(source) return ast.parse(source)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment'
def test_funcdef(self): def test_funcdef(self):
for tree in self.parse_all(funcdef): for tree in self.parse_all(funcdef):
self.assertEqual(tree.body[0].type_comment, "() -> int") 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[0].type_comment, None)
self.assertEqual(tree.body[1].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): def test_asyncdef(self):
for tree in self.parse_all(asyncdef, minver=5): for tree in self.parse_all(asyncdef, minver=5):
self.assertEqual(tree.body[0].type_comment, "() -> int") self.assertEqual(tree.body[0].type_comment, "() -> int")
@@ -274,12 +272,10 @@ class TypeCommentTests(unittest.TestCase):
with self.assertRaises(SyntaxError): with self.assertRaises(SyntaxError):
self.classic_parse(asyncvar) self.classic_parse(asyncvar)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_asynccomp(self): def test_asynccomp(self):
for tree in self.parse_all(asynccomp, minver=6): for tree in self.parse_all(asynccomp, minver=6):
pass pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_matmul(self): def test_matmul(self):
for tree in self.parse_all(matmul, minver=5): for tree in self.parse_all(matmul, minver=5):
pass pass
@@ -288,37 +284,31 @@ class TypeCommentTests(unittest.TestCase):
for tree in self.parse_all(fstring): for tree in self.parse_all(fstring):
pass pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_underscorednumber(self): def test_underscorednumber(self):
for tree in self.parse_all(underscorednumber, minver=6): for tree in self.parse_all(underscorednumber, minver=6):
pass pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_redundantdef(self): def test_redundantdef(self):
for tree in self.parse_all(redundantdef, maxver=0, for tree in self.parse_all(redundantdef, maxver=0,
expected_regex="^Cannot have two type comments on def"): expected_regex="^Cannot have two type comments on def"):
pass pass
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment'
def test_nonasciidef(self): def test_nonasciidef(self):
for tree in self.parse_all(nonasciidef): for tree in self.parse_all(nonasciidef):
self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt") self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt")
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'For' object has no attribute 'type_comment'
def test_forstmt(self): def test_forstmt(self):
for tree in self.parse_all(forstmt): for tree in self.parse_all(forstmt):
self.assertEqual(tree.body[0].type_comment, "int") self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(forstmt) tree = self.classic_parse(forstmt)
self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[0].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'With' object has no attribute 'type_comment'
def test_withstmt(self): def test_withstmt(self):
for tree in self.parse_all(withstmt): for tree in self.parse_all(withstmt):
self.assertEqual(tree.body[0].type_comment, "int") self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(withstmt) tree = self.classic_parse(withstmt)
self.assertEqual(tree.body[0].type_comment, None) 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): def test_parenthesized_withstmt(self):
for tree in self.parse_all(parenthesized_withstmt): for tree in self.parse_all(parenthesized_withstmt):
self.assertEqual(tree.body[0].type_comment, "int") 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[0].type_comment, None)
self.assertEqual(tree.body[1].type_comment, None) self.assertEqual(tree.body[1].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'int'
def test_vardecl(self): def test_vardecl(self):
for tree in self.parse_all(vardecl): for tree in self.parse_all(vardecl):
self.assertEqual(tree.body[0].type_comment, "int") self.assertEqual(tree.body[0].type_comment, "int")
tree = self.classic_parse(vardecl) tree = self.classic_parse(vardecl)
self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[0].type_comment, None)
@unittest.expectedFailure # TODO: RUSTPYTHON; + (11, ' whatever')]
def test_ignores(self): def test_ignores(self):
for tree in self.parse_all(ignores): for tree in self.parse_all(ignores):
self.assertEqual( self.assertEqual(
@@ -350,7 +338,6 @@ class TypeCommentTests(unittest.TestCase):
tree = self.classic_parse(ignores) tree = self.classic_parse(ignores)
self.assertEqual(tree.type_ignores, []) self.assertEqual(tree.type_ignores, [])
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4)
def test_longargs(self): def test_longargs(self):
for tree in self.parse_all(longargs, minver=8): for tree in self.parse_all(longargs, minver=8):
for t in tree.body: for t in tree.body:
@@ -381,7 +368,6 @@ class TypeCommentTests(unittest.TestCase):
self.assertIsNone(arg.type_comment, "%s(%s:%r)" % self.assertIsNone(arg.type_comment, "%s(%s:%r)" %
(t.name, arg.arg, arg.type_comment)) (t.name, arg.arg, arg.type_comment))
@unittest.expectedFailure # TODO: RUSTPYTHON; Tests for inappropriately-placed type comments.
def test_inappropriate_type_comments(self): def test_inappropriate_type_comments(self):
"""Tests for inappropriately-placed type comments. """Tests for inappropriately-placed type comments.
@@ -416,7 +402,6 @@ class TypeCommentTests(unittest.TestCase):
_testcapi.Py_CompileStringExFlags( _testcapi.Py_CompileStringExFlags(
b"def a(f=8, #type: \x80\n\x80", "<test>", 256, flags) 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 test_func_type_input(self):
def parse_func_type_input(source): 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.foo.__type_params__[0].__bound__, float)
self.assertIs(X.Alias.__value__, float) self.assertIs(X.Alias.__value__, float)
@unittest.expectedFailure # TODO: RUSTPYTHON; + global
def test_binding_uses_global(self): def test_binding_uses_global(self):
ns = run_code(""" ns = run_code("""
x = "global" x = "global"
@@ -1076,7 +1075,6 @@ class TypeParamsTypeVarTest(unittest.TestCase):
class TypeParamsTypeVarTupleTest(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): def test_typevartuple_01(self):
code = """def func1[*A: str](): pass""" code = """def func1[*A: str](): pass"""
check_syntax_error(self, code, "cannot use bound with TypeVarTuple") check_syntax_error(self, code, "cannot use bound with TypeVarTuple")
@@ -1100,7 +1098,6 @@ class TypeParamsTypeVarTupleTest(unittest.TestCase):
class TypeParamsTypeVarParamSpecTest(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): def test_paramspec_01(self):
code = """def func1[**A: str](): pass""" code = """def func1[**A: str](): pass"""
check_syntax_error(self, code, "cannot use bound with ParamSpec") 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 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): Traceback (most recent call last):
... ...
SyntaxError: dict unpacking cannot be used in dict comprehension 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 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): Traceback (most recent call last):
... ...
SyntaxError: cannot use starred expression here SyntaxError: cannot use starred expression here
@@ -366,12 +366,12 @@ Now some general starred expressions (all fail).
... ...
SyntaxError: cannot use starred expression here 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): Traceback (most recent call last):
... ...
SyntaxError: cannot use starred expression here 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): Traceback (most recent call last):
... ...
SyntaxError: cannot use starred expression here SyntaxError: cannot use starred expression here
+7 -27
View File
@@ -1,17 +1,13 @@
use crate::pystate::with_vm; use crate::pystate::with_vm;
use crate::unicodeobject::decode_fsdefault_and_size;
use core::ffi::{CStr, c_char, c_int}; use core::ffi::{CStr, c_char, c_int};
use core::ptr::NonNull; use core::ptr::NonNull;
use rustpython_vm::builtins::{PyCode, PyDict}; use rustpython_vm::builtins::{PyCode, PyDict};
use rustpython_vm::compiler::Mode;
use rustpython_vm::function::ArgMapping; use rustpython_vm::function::ArgMapping;
use rustpython_vm::scope::Scope; use rustpython_vm::scope::Scope;
use rustpython_vm::version;
use rustpython_vm::{AsObject, PyObject, TryFromObject}; 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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn Py_CompileString( pub unsafe extern "C" fn Py_CompileString(
code: *const c_char, code: *const c_char,
@@ -19,27 +15,11 @@ pub unsafe extern "C" fn Py_CompileString(
start: c_int, start: c_int,
) -> *mut PyObject { ) -> *mut PyObject {
with_vm(|vm| { with_vm(|vm| {
let code = unsafe { CStr::from_ptr(code) }.to_str().map_err(|_| { let code = unsafe { CStr::from_ptr(code) }.to_bytes();
vm.new_system_error("Py_CompileString called with non UTF-8 code string") let filename_size = unsafe { CStr::from_ptr(filename) }.to_bytes().len();
})?; let filename = decode_fsdefault_and_size(vm, filename, filename_size)?;
let filename = unsafe { CStr::from_ptr(filename) } let filename = filename.to_string_lossy();
.to_str() vm.compile_string_object_with_flags(code, &filename, start, 0, version::MINOR as c_int, -1)
.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)))
}) })
} }
+35 -19
View File
@@ -4,8 +4,8 @@ use core::ffi::{CStr, c_char, c_int};
use core::ptr::NonNull; use core::ptr::NonNull;
use core::slice; use core::slice;
use core::str; use core::str;
use rustpython_vm::PyObjectRef; use rustpython_vm::builtins::{PyStr, PyStrRef};
use rustpython_vm::builtins::PyStr; use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine};
define_py_check!(fn PyUnicode_Check, types.str_type); define_py_check!(fn PyUnicode_Check, types.str_type);
define_py_check!(exact fn PyUnicode_CheckExact, 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() .try_into()
.map_err(|_| vm.new_system_error("size must be non-negative"))?; .map_err(|_| vm.new_system_error("size must be non-negative"))?;
let bytes = if s.is_null() { decode_fsdefault_and_size(vm, s, size)
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_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)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn PyUnicode_EncodeFSDefault(unicode: *mut PyObject) -> *mut PyObject { pub unsafe extern "C" fn PyUnicode_EncodeFSDefault(unicode: *mut PyObject) -> *mut PyObject {
with_vm(|vm| { 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 rustpython_compiler_core::SourceLocation;
use thiserror::Error; 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>; // pub type CodegenError = rustpython_parser_core::source_code::LocatedError<CodegenErrorType>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -70,6 +55,8 @@ pub enum CodegenErrorType {
SyntaxError(String), SyntaxError(String),
/// Multiple `*` detected /// Multiple `*` detected
MultipleStarArgs, MultipleStarArgs,
MultipleStarredExpressionsInSequencePattern,
MultipleStarredNamesInSequencePattern,
/// Misplaced `*` expression /// Misplaced `*` expression
InvalidStarExpr, InvalidStarExpr,
/// Break statement outside of loop. /// Break statement outside of loop.
@@ -87,14 +74,17 @@ pub enum CodegenErrorType {
AsyncReturnValue, AsyncReturnValue,
InvalidFuturePlacement, InvalidFuturePlacement,
InvalidFutureFeature(String), InvalidFutureFeature(String),
FunctionImportStar, InvalidFutureBraces,
RecursionError,
TooManyStarUnpack, TooManyStarUnpack,
TooManyExpressionsInStarUnpackingSequencePattern,
EmptyWithItems, EmptyWithItems,
EmptyWithBody, EmptyWithBody,
ForbiddenName, ForbiddenName,
DuplicateStore(String), DuplicateStore(String),
UnreachablePattern(PatternUnreachableReason), UnreachableWildcardPattern,
RepeatedAttributePattern, UnreachableNameCapturePattern(String),
RepeatedAttributePattern(String),
ConflictingNameBindPattern, ConflictingNameBindPattern,
/// break/continue/return inside except* block /// break/continue/return inside except* block
BreakContinueReturnInExceptStar, BreakContinueReturnInExceptStar,
@@ -112,6 +102,12 @@ impl fmt::Display for CodegenErrorType {
Self::MultipleStarArgs => { Self::MultipleStarArgs => {
write!(f, "multiple starred expressions in assignment") 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::InvalidStarExpr => write!(f, "can't use starred expression here"),
Self::InvalidBreak => write!(f, "'break' outside loop"), Self::InvalidBreak => write!(f, "'break' outside loop"),
Self::InvalidContinue => write!(f, "'continue' not properly in 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::AsyncYieldFrom => write!(f, "'yield from' inside async function"),
Self::AsyncReturnValue => { Self::AsyncReturnValue => write!(f, "'return' with value in async generator"),
write!(f, "'return' with value inside async generator")
}
Self::InvalidFuturePlacement => write!( Self::InvalidFuturePlacement => write!(
f, f,
"from __future__ imports must occur at the beginning of the file" "from __future__ imports must occur at the beginning of the file"
@@ -138,12 +132,16 @@ impl fmt::Display for CodegenErrorType {
Self::InvalidFutureFeature(feat) => { Self::InvalidFutureFeature(feat) => {
write!(f, "future feature {feat} is not defined") write!(f, "future feature {feat} is not defined")
} }
Self::FunctionImportStar => { Self::InvalidFutureBraces => write!(f, "not a chance"),
write!(f, "import * only allowed at module level") Self::RecursionError => {
write!(f, "maximum recursion depth exceeded during compilation")
} }
Self::TooManyStarUnpack => { Self::TooManyStarUnpack => {
write!(f, "too many expressions in star-unpacking assignment") write!(f, "too many expressions in star-unpacking assignment")
} }
Self::TooManyExpressionsInStarUnpackingSequencePattern => {
write!(f, "too many expressions in star-unpacking sequence pattern")
}
Self::EmptyWithItems => { Self::EmptyWithItems => {
write!(f, "empty items on With") write!(f, "empty items on With")
} }
@@ -153,14 +151,18 @@ impl fmt::Display for CodegenErrorType {
Self::ForbiddenName => { Self::ForbiddenName => {
write!(f, "forbidden attribute name") write!(f, "forbidden attribute name")
} }
Self::DuplicateStore(s) => { Self::DuplicateStore(s) => write!(f, "multiple assignments to name '{s}' in pattern"),
write!(f, "duplicate store {s}") Self::UnreachableWildcardPattern => {
write!(f, "wildcard makes remaining patterns unreachable")
} }
Self::UnreachablePattern(reason) => { Self::UnreachableNameCapturePattern(name) => {
write!(f, "{reason} makes remaining patterns unreachable") write!(
f,
"name capture '{name}' makes remaining patterns unreachable"
)
} }
Self::RepeatedAttributePattern => { Self::RepeatedAttributePattern(name) => {
write!(f, "attribute name repeated in class pattern") write!(f, "attribute name repeated in class pattern: {name}")
} }
Self::ConflictingNameBindPattern => { Self::ConflictingNameBindPattern => {
write!(f, "alternative patterns bind different names") 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) { fn frozenset_key_contains(elements: &[ConstantData], needle: &ConstantData) -> bool {
// CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct if Self::constant_contains_nan(needle) {
// because Python-level NaN keys do not compare equal. return false;
if !Self::constant_contains_nan(&constant) }
&& let Some(idx) = self elements.iter().any(|element| {
.constants !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() .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); return (idx, false);
} }
let idx = self.constants.len(); let idx = self.constants.len();
@@ -95,14 +188,8 @@ impl ConstantPool {
} }
fn try_insert_full(&mut self, constant: ConstantData) -> crate::InternalResult<(usize, bool)> { fn try_insert_full(&mut self, constant: ConstantData) -> crate::InternalResult<(usize, bool)> {
// CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct let constant = Self::canonicalize_constant_key(constant)?;
// because Python-level NaN keys do not compare equal. if let Some(idx) = self.find_existing(&constant) {
if !Self::constant_contains_nan(&constant)
&& let Some(idx) = self
.constants
.iter()
.position(|existing| existing == &constant)
{
return Ok((idx, false)); return Ok((idx, false));
} }
self.constants self.constants
@@ -1485,6 +1572,7 @@ impl Blocks {
| PseudoInstruction::JumpIfTrue { .. }), | PseudoInstruction::JumpIfTrue { .. }),
) => { ) => {
let opcode = pseudo.into(); let opcode = pseudo.into();
let opcode_is_false = matches!(pseudo, PseudoInstruction::JumpIfFalse { .. });
match target.instr.pseudo().map(Into::into) { match target.instr.pseudo().map(Into::into) {
Some(PseudoOpcode::Jump) Some(PseudoOpcode::Jump)
if self.jump_thread(block_idx, i, &target, opcode)? => if self.jump_thread(block_idx, i, &target, opcode)? =>
@@ -1492,22 +1580,25 @@ impl Blocks {
continue; continue;
} }
Some(PseudoOpcode::JumpIfFalse) Some(PseudoOpcode::JumpIfFalse)
if matches!( if opcode_is_false
opcode, && self.jump_thread(block_idx, i, &target, opcode)? =>
AnyInstruction::Pseudo(PseudoInstruction::JumpIfFalse { .. })
) && self.jump_thread(block_idx, i, &target, opcode)? =>
{ {
continue; continue;
} }
Some(PseudoOpcode::JumpIfTrue) Some(PseudoOpcode::JumpIfTrue)
if matches!( if !opcode_is_false
opcode, && self.jump_thread(block_idx, i, &target, opcode)? =>
AnyInstruction::Pseudo(PseudoInstruction::JumpIfTrue { .. })
) && self.jump_thread(block_idx, i, &target, opcode)? =>
{ {
continue; 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; let next = self[inst.target].next;
debug_assert!(next != BlockIdx::NULL); debug_assert!(next != BlockIdx::NULL);
debug_assert!(next != inst.target); debug_assert!(next != inst.target);
@@ -2335,10 +2426,10 @@ impl Blocks {
Ok(()) 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 /// 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. /// exception handlers.
/// ///
/// Phase 2 (mark_cold): propagate "cold" from except_handler blocks via /// 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 /// empty unreachable placeholders left by remove_unreachable; they stay in
/// their original chain position (e.g. between entry and the post-try /// their original chain position (e.g. between entry and the post-try
/// continuation for a nested try/except whose inner_end was emptied by /// 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. /// optimize_load_fast to terminate fall-through at those placeholders.
/// flowgraph.c mark_warm /// flowgraph.c mark_warm
fn mark_warm(&mut self) -> crate::InternalResult<()> { 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 { impl<const N: usize> From<[Block; N]> for Blocks {
fn from(value: [Block; N]) -> Self { fn from(value: [Block; N]) -> Self {
Self(value.into()) 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 { impl Deref for Blocks {
type Target = [Block]; type Target = [Block];
@@ -3060,7 +3121,7 @@ impl IndexMut<BlockIdx> for Blocks {
} }
pub(crate) const START_DEPTH_UNSET: i32 = i32::MIN; pub(crate) const START_DEPTH_UNSET: i32 = i32::MIN;
const CO_MAXBLOCKS: usize = 20; const CO_MAXBLOCKS: usize = 21;
/// flowgraph.c struct _PyCfgExceptStack /// flowgraph.c struct _PyCfgExceptStack
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -3099,7 +3160,7 @@ impl CfgTraversalStack {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct InstructionSequenceLabelMap { pub(crate) struct InstructionSequenceLabelMap {
block_labels: Vec<InstructionSequenceLabel>, 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 /// `_PyInstructionSequence_UseLabel()` can map multiple labels to the same
/// instruction offset before `_PyCfg_FromInstructionSequence()` materializes /// instruction offset before `_PyCfg_FromInstructionSequence()` materializes
@@ -3305,7 +3366,7 @@ pub struct CodeInfo {
// Reference to the symbol table for this scope // Reference to the symbol table for this scope
pub symbol_table_index: usize, 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(). // when calling flowgraph.c _PyCfg_OptimizeCodeUnit().
pub nparams: usize, pub nparams: usize,
@@ -3488,7 +3549,7 @@ impl CodeInfo {
} }
fn prepare_cfg_from_codegen(&mut self) -> crate::InternalResult<InstructionSequence> { 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(). // u_instr_sequence directly into flowgraph.c _PyCfg_FromInstructionSequence().
self.take_recorded_instr_sequence() self.take_recorded_instr_sequence()
} }
@@ -3509,12 +3570,12 @@ fn optimize_code_unit(
optimize_cfg(metadata, blocks, metadata.firstlineno)?; optimize_cfg(metadata, blocks, metadata.firstlineno)?;
blocks.remove_unused_consts(&mut metadata.consts)?; blocks.remove_unused_consts(&mut metadata.consts)?;
add_checks_for_loads_of_uninitialized_variables(blocks, nlocals, nparams)?; 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 // later jump normalization / block reordering can create adjacencies
// that never exist at this stage in flowgraph.c. // that never exist at this stage in flowgraph.c.
blocks.insert_superinstructions()?; blocks.insert_superinstructions()?;
blocks.push_cold_blocks_to_end()?; 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)?; blocks.resolve_line_numbers(metadata.firstlineno)?;
Ok(()) Ok(())
} }
@@ -3525,20 +3586,20 @@ fn optimize_cfg(
firstlineno: OneIndexed, firstlineno: OneIndexed,
) -> crate::InternalResult<()> { ) -> crate::InternalResult<()> {
// flowgraph.c optimize_cfg // 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 // SystemError if a jump or scope exit is not the last instruction in
// its block. // its block.
blocks.check_cfg()?; blocks.check_cfg()?;
blocks.inline_small_or_no_lineno_blocks()?; 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 // after this point. Unreferenced label blocks left by jump inlining
// remain block boundaries and can preserve line-marker NOPs. // remain block boundaries and can preserve line-marker NOPs.
blocks.remove_unreachable()?; 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 // superinstruction insertion, so fusion decisions see propagated
// source locations. // source locations.
blocks.resolve_line_numbers(firstlineno)?; 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_basic_block() after line numbers are resolved.
optimize_load_const(metadata, blocks)?; optimize_load_const(metadata, blocks)?;
let mut block_idx = BlockIdx(0); let mut block_idx = BlockIdx(0);
@@ -3548,7 +3609,7 @@ fn optimize_cfg(
block_idx = next_block; block_idx = next_block;
} }
blocks.remove_redundant_nops_and_pairs()?; 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 // redundant NOP/jump chains before _PyCfg_OptimizeCodeUnit() prunes
// unused constants. // unused constants.
blocks.remove_unreachable()?; blocks.remove_unreachable()?;
@@ -3568,7 +3629,7 @@ fn optimized_cfg_to_instruction_sequence(
let max_stackdepth = blocks.calculate_stackdepth()?; let max_stackdepth = blocks.calculate_stackdepth()?;
debug_assert!(!is_generator(flags) || max_stackdepth != 0); debug_assert!(!is_generator(flags) || max_stackdepth != 0);
let nlocalsplus = prepare_localsplus(metadata, blocks, flags)?; 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. // localsplus preparation, before normalize_jumps.
convert_pseudo_ops(blocks)?; convert_pseudo_ops(blocks)?;
blocks.normalize_jumps()?; blocks.normalize_jumps()?;
@@ -3636,6 +3697,9 @@ impl CodeInfo {
kwonlyargcount: kwonlyarg_count, kwonlyargcount: kwonlyarg_count,
firstlineno: first_line_number, firstlineno: first_line_number,
} = metadata; } = metadata;
let code_arg_count = posonlyarg_count
.checked_add(arg_count)
.ok_or(InternalError::MalformedControlFlowGraph)?;
resolve_unconditional_jumps(&mut instr_sequence)?; resolve_unconditional_jumps(&mut instr_sequence)?;
resolve_jump_offsets(&mut instr_sequence)?; resolve_jump_offsets(&mut instr_sequence)?;
@@ -3653,7 +3717,7 @@ impl CodeInfo {
Ok(CodeObject { Ok(CodeObject {
flags, flags,
posonlyarg_count, posonlyarg_count,
arg_count, arg_count: code_arg_count,
kwonlyarg_count, kwonlyarg_count,
source_path, source_path,
first_line_number: Some(first_line_number), 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) const_folding_safe_multiply(right, left)
} }
(ConstantData::Tuple { elements }, ConstantData::Integer { value: n }) => { (ConstantData::Tuple { elements }, ConstantData::Integer { value: n }) => {
if elements.is_empty() {
return Some(ConstantData::Tuple {
elements: Vec::new(),
});
}
let n = n.to_usize()?; let n = n.to_usize()?;
if n != 0 && !elements.is_empty() { if n != 0 {
if n > MAX_COLLECTION_SIZE / elements.len() { if n > MAX_COLLECTION_SIZE / elements.len() {
return None; return None;
} }
@@ -4265,7 +4334,7 @@ fn eval_const_complex_binop(
BinOp::Add => left + right, BinOp::Add => left + right,
BinOp::Subtract => { BinOp::Subtract => {
let re = left.re - right.re; 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`. // minus zero-complex expressions such as `0 - 0j`.
let im = if left.re == 0.0 let im = if left.re == 0.0
&& left.im == 0.0 && left.im == 0.0
@@ -4829,7 +4898,7 @@ fn fold_constant_intrinsic_list_to_tuple(
Ok(false) 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( fn optimize_lists_and_sets(
metadata: &mut CodeUnitMetadata, metadata: &mut CodeUnitMetadata,
block: &mut Block, block: &mut Block,
@@ -4889,8 +4958,7 @@ fn optimize_lists_and_sets(
if !contains_or_iter { if !contains_or_iter {
debug_assert!(i >= 2); debug_assert!(i >= 2);
let folded_loc = block.instructions[i].location; let folded_loc = instr_location(&block.instructions[i]);
let end_loc = block.instructions[i].end_location;
nop_out(block, &operand_indices); nop_out(block, &operand_indices);
@@ -4901,9 +4969,7 @@ fn optimize_lists_and_sets(
} }
.into(); .into();
instr_set_op1(&mut block.instructions[i - 2], build_instr, OpArg::new(0)); instr_set_op1(&mut block.instructions[i - 2], build_instr, OpArg::new(0));
block.instructions[i - 2].location = folded_loc; instr_set_location(&mut block.instructions[i - 2], folded_loc);
block.instructions[i - 2].end_location = end_loc;
block.instructions[i - 2].lineno_override = None;
instr_set_op1( instr_set_op1(
&mut block.instructions[i - 1], &mut block.instructions[i - 1],
@@ -5152,7 +5218,7 @@ fn basicblock_optimize_load_const(
block: &mut Block, block: &mut Block,
) -> crate::InternalResult<()> { ) -> crate::InternalResult<()> {
let mut i = 0; let mut i = 0;
let mut effective_opcode = None; let mut effective_opcode = Instruction::Nop.into();
let mut effective_oparg = OpArg::new(0); let mut effective_oparg = OpArg::new(0);
while i < block.instruction_used { while i < block.instruction_used {
if matches!( if matches!(
@@ -5166,21 +5232,19 @@ fn basicblock_optimize_load_const(
let curr = block.instructions[i]; let curr = block.instructions[i];
let curr_arg = curr.arg; 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!( let is_copy_of_load_const = matches!(
(effective_opcode, curr_instr), (effective_opcode, curr.instr.real()),
(Some(Instruction::LoadConst { .. }), Instruction::Copy { i }) if i.get(curr_arg) == 1 (AnyInstruction::Real(Instruction::LoadConst { .. }), Some(Instruction::Copy { i }))
if i.get(curr_arg) == 1
); );
if !is_copy_of_load_const { if !is_copy_of_load_const {
effective_opcode = Some(curr_instr); effective_opcode = curr.instr;
effective_oparg = curr_arg; 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; i += 1;
continue; continue;
}; };
@@ -5273,7 +5337,7 @@ fn basicblock_optimize_load_const(
Opcode::PopJumpIfNone Opcode::PopJumpIfNone
} }
.into(); .into();
i = jump_idx; i += 1;
continue; continue;
} }
} }
@@ -6557,21 +6621,19 @@ fn get_max_label(blocks: &Blocks) -> i32 {
} }
/// flowgraph.c make_except_stack /// flowgraph.c make_except_stack
#[allow(clippy::unnecessary_wraps)] fn make_except_stack() -> CfgExceptStack {
fn make_except_stack() -> crate::InternalResult<CfgExceptStack> {
let handlers = [BlockIdx::NULL; CO_MAXBLOCKS + 2]; let handlers = [BlockIdx::NULL; CO_MAXBLOCKS + 2];
debug_assert_eq!(handlers[0], BlockIdx::NULL); debug_assert_eq!(handlers[0], BlockIdx::NULL);
Ok(CfgExceptStack { handlers, depth: 0 }) CfgExceptStack { handlers, depth: 0 }
} }
/// flowgraph.c copy_except_stack /// flowgraph.c copy_except_stack
#[allow(clippy::unnecessary_wraps)] fn copy_except_stack(stack: &CfgExceptStack) -> CfgExceptStack {
fn copy_except_stack(stack: &CfgExceptStack) -> crate::InternalResult<CfgExceptStack> {
debug_assert!(stack.depth <= CO_MAXBLOCKS + 1); debug_assert!(stack.depth <= CO_MAXBLOCKS + 1);
Ok(CfgExceptStack { CfgExceptStack {
handlers: stack.handlers, handlers: stack.handlers,
depth: stack.depth, depth: stack.depth,
}) }
} }
/// flowgraph.c except_stack_top /// flowgraph.c except_stack_top
@@ -6623,7 +6685,7 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
todo.push(BlockIdx(0)); todo.push(BlockIdx(0));
blocks[0].visited = true; 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() { while let Some(block_idx) = todo.pop() {
let bi = block_idx.idx(); let bi = block_idx.idx();
@@ -6650,7 +6712,7 @@ pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalRes
if !blocks[target].visited { if !blocks[target].visited {
blocks[target].except_stack = Some(copy_except_stack( blocks[target].except_stack = Some(copy_except_stack(
stack.as_ref().expect("active exception stack"), stack.as_ref().expect("active exception stack"),
)?); ));
todo.push(target); todo.push(target);
blocks[target].visited = true; 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]) { if bb_has_fallthrough(&blocks[bi]) {
blocks[target].except_stack = Some(copy_except_stack( blocks[target].except_stack = Some(copy_except_stack(
stack.as_ref().expect("active exception stack"), stack.as_ref().expect("active exception stack"),
)?); ));
} else { } else {
blocks[target].except_stack = stack.take(); blocks[target].except_stack = stack.take();
stack_transferred = true; stack_transferred = true;
@@ -6852,6 +6914,58 @@ pub(crate) fn fix_cell_offsets(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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 { fn test_location(line: u32) -> SourceLocation {
SourceLocation { SourceLocation {
@@ -6886,6 +7000,13 @@ mod tests {
instr 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) { fn test_block_push(block: &mut Block, info: InstructionInfo) {
let off = basicblock_next_instr(block).expect("test block instruction slot"); let off = basicblock_next_instr(block).expect("test block instruction slot");
block.instructions[off] = info; block.instructions[off] = info;
@@ -6970,7 +7091,7 @@ mod tests {
#[test] #[test]
fn except_stack_tracks_cpython_depth_and_handler_slots() { 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.depth, 0);
assert_eq!(stack.handlers.len(), CO_MAXBLOCKS + 2); assert_eq!(stack.handlers.len(), CO_MAXBLOCKS + 2);
assert_eq!(stack.handlers[0], BlockIdx::NULL); assert_eq!(stack.handlers[0], BlockIdx::NULL);
@@ -6994,7 +7115,7 @@ mod tests {
assert!(handler.preserve_lasti); assert!(handler.preserve_lasti);
assert!(blocks[1].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.depth, stack.depth);
assert_eq!(copy.handlers, stack.handlers); 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] #[test]
fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() { fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() {
let mut block = Block::default(); 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] #[test]
fn resolve_line_numbers_duplicates_exit_blocks_like_cpython() { fn resolve_line_numbers_duplicates_exit_blocks_like_cpython() {
let exit = BlockIdx::new(2); let exit = BlockIdx::new(2);
@@ -7621,4 +7806,51 @@ mod tests {
assert_eq!(threaded.target, BlockIdx::new(3)); assert_eq!(threaded.target, BlockIdx::new(3));
assert_eq!(u32::from(threaded.arg), 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; extern crate alloc;
use rustpython_compiler_core::bytecode::ConstantData;
type IndexMap<K, V> = indexmap::IndexMap<K, V, rapidhash::quality::RandomState>; type IndexMap<K, V> = indexmap::IndexMap<K, V, rapidhash::quality::RandomState>;
type IndexSet<T> = indexmap::IndexSet<T, rapidhash::quality::RandomState>; type IndexSet<T> = indexmap::IndexSet<T, rapidhash::quality::RandomState>;
pub mod compile; pub mod compile;
pub mod error; pub mod error;
pub mod ir; pub mod ir;
mod preprocess; pub mod preprocess;
mod string_parser; mod string_parser;
pub mod symboltable; pub mod symboltable;
mod unparse; mod unparse;
@@ -24,6 +26,73 @@ use ruff_python_ast as ast;
pub(crate) use compile::InternalResult; 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 { pub trait ToPythonName {
/// Returns a short name for the node suitable for use in error messages. /// Returns a short name for the node suitable for use in error messages.
fn python_name(&self) -> &'static str; fn python_name(&self) -> &'static str;
@@ -48,6 +117,19 @@ impl ToPythonName for ast::Expr {
} }
Self::EllipsisLiteral(_) => "ellipsis", Self::EllipsisLiteral(_) => "ellipsis",
Self::NoneLiteral(_) => "None", 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::NumberLiteral(_) | Self::BytesLiteral(_) | Self::StringLiteral(_) => "literal",
Self::Tuple(_) => "tuple", Self::Tuple(_) => "tuple",
Self::List { .. } => "list", Self::List { .. } => "list",
@@ -65,7 +147,7 @@ impl ToPythonName for ast::Expr {
Self::Lambda { .. } => "lambda", Self::Lambda { .. } => "lambda",
Self::If { .. } => "conditional expression", Self::If { .. } => "conditional expression",
Self::Named { .. } => "named 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}, visitor::transformer::{self, Transformer},
}; };
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use rustpython_compiler_core::bytecode;
const MAXDIGITS: usize = 3; const MAXDIGITS: usize = 3;
const F_LJUST: u8 = 1; const F_LJUST: u8 = 1;
pub(crate) fn preprocess_mod(module: &mut ast::Mod) { /// ast_preprocess.c ControlFlowInFinallyContext
let preprocessor = AstPreprocessor; #[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 { 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), 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 { impl AstPreprocessor {
fn visit_expr(&self, expr: &mut Expr) { fn visit_astfold_body(&self, body: &mut ast::Suite) {
transformer::walk_expr(self, expr); let mut docstring = body_starts_with_docstring(body);
if let Some(optimized) = optimize_format(expr) { if docstring && self.optimize >= 2 {
*expr = optimized; 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> { fn optimize_format(expr: &Expr) -> Option<Expr> {
let Expr::BinOp(binop) = expr else { let Expr::BinOp(binop) = expr else {
return None; return None;
@@ -38,9 +410,7 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
if !matches!(binop.op, Operator::Mod) { if !matches!(binop.op, Operator::Mod) {
return None; return None;
} }
let Expr::StringLiteral(format) = binop.left.as_ref() else { let (format, _) = string_literal_expr_value(&binop.left)?;
return None;
};
let Expr::Tuple(tuple) = binop.right.as_ref() else { let Expr::Tuple(tuple) = binop.right.as_ref() else {
return None; return None;
}; };
@@ -52,7 +422,7 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
return None; return None;
} }
let elements = parse_format(format.value.to_str(), &tuple.elts)?; let elements = parse_format(format, &tuple.elts)?;
Some(Expr::FString(ExprFString { Some(Expr::FString(ExprFString {
node_index: binop.node_index.clone(), node_index: binop.node_index.clone(),
range: binop.range, range: binop.range,
@@ -62,6 +432,8 @@ fn optimize_format(expr: &Expr) -> Option<Expr> {
elements: InterpolatedStringElements::from(elements), elements: InterpolatedStringElements::from(elements),
flags: FStringFlags::empty(), 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, debug_text: None,
conversion, conversion,
format_spec, 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(), 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) 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 { fn unparse_expr(&mut self, ast: &ast::Expr, level: u8) -> fmt::Result {
macro_rules! op_prec { macro_rules! op_prec {
($op_ty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { ($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, values,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
let (op, prec) = op_prec!(bin, op, ast::BoolOp, And("and", AND), Or("or", OR)); let (op, prec) = op_prec!(bin, op, ast::BoolOp, And("and", AND), Or("or", OR));
group_if!(prec, { group_if!(prec, {
@@ -102,6 +160,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value, value,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
group_if!(precedence::TUPLE, { group_if!(precedence::TUPLE, {
self.unparse_expr(target, precedence::ATOM)?; self.unparse_expr(target, precedence::ATOM)?;
@@ -115,6 +174,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
right, right,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
let right_associative = matches!(op, ast::Operator::Pow); let right_associative = matches!(op, ast::Operator::Pow);
let (op, prec) = op_prec!( let (op, prec) = op_prec!(
@@ -146,6 +206,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
operand, operand,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
let (op, prec) = op_prec!( let (op, prec) = op_prec!(
un, un,
@@ -166,6 +227,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
body, body,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
group_if!(precedence::TEST, { group_if!(precedence::TEST, {
if let Some(parameters) = parameters { if let Some(parameters) = parameters {
@@ -183,6 +245,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
orelse, orelse,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
group_if!(precedence::TEST, { group_if!(precedence::TEST, {
self.unparse_expr(body, precedence::TEST + 1)?; self.unparse_expr(body, precedence::TEST + 1)?;
@@ -196,6 +259,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
items, items,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("{")?; self.p("{")?;
let mut first = true; let mut first = true;
@@ -214,6 +278,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
elts, elts,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("{")?; self.p("{")?;
let mut first = true; let mut first = true;
@@ -228,6 +293,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators, generators,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("[")?; self.p("[")?;
self.unparse_expr(elt, precedence::TEST)?; self.unparse_expr(elt, precedence::TEST)?;
@@ -239,6 +305,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators, generators,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("{")?; self.p("{")?;
self.unparse_expr(elt, precedence::TEST)?; self.unparse_expr(elt, precedence::TEST)?;
@@ -251,6 +318,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators, generators,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("{")?; self.p("{")?;
self.unparse_expr(key, precedence::TEST)?; self.unparse_expr(key, precedence::TEST)?;
@@ -265,6 +333,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
generators, generators,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.p("(")?; self.p("(")?;
self.unparse_expr(elt, precedence::TEST)?; self.unparse_expr(elt, precedence::TEST)?;
@@ -275,6 +344,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value, value,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
group_if!(precedence::AWAIT, { group_if!(precedence::AWAIT, {
self.p("await ")?; self.p("await ")?;
@@ -285,6 +355,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value, value,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
if let Some(value) = value { if let Some(value) = value {
write!(self, "(yield {})", UnparseExpr::new(value, self.source))?; write!(self, "(yield {})", UnparseExpr::new(value, self.source))?;
@@ -296,6 +367,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
value, value,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
write!( write!(
self, self,
@@ -309,6 +381,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
comparators, comparators,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
group_if!(precedence::CMP, { group_if!(precedence::CMP, {
let new_lvl = precedence::CMP + 1; let new_lvl = precedence::CMP + 1;
@@ -326,6 +399,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
arguments: ast::Arguments { args, keywords, .. }, arguments: ast::Arguments { args, keywords, .. },
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
self.unparse_expr(func, precedence::ATOM)?; self.unparse_expr(func, precedence::ATOM)?;
self.p("(")?; self.p("(")?;
@@ -379,26 +453,13 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
.bytes_repr() .bytes_repr()
.fmt(self.f)? .fmt(self.f)?
} }
ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => { ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value {
#[allow(clippy::correctness, clippy::assertions_on_constants)] ast::Number::Int(int) => int.fmt(self.f)?,
const { &ast::Number::Float(fp) => self.unparse_float(fp)?,
assert!(f64::MAX_10_EXP == 308) &ast::Number::Complex { real, imag } => self.unparse_complex(real, imag)?,
}; },
ast::Expr::Constant(ast::ExprConstant { value, .. }) => {
let inf_str = "1e309"; self.unparse_constant_value(value)?
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::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => { ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => {
self.p(if *value { "True" } else { "False" })? self.p(if *value { "True" } else { "False" })?
@@ -460,6 +521,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
step, step,
node_index: _, node_index: _,
range: _range, range: _range,
..
}) => { }) => {
if let Some(lower) = lower { if let Some(lower) = lower {
self.unparse_expr(lower, precedence::TEST)?; self.unparse_expr(lower, precedence::TEST)?;
@@ -554,7 +616,9 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> {
let buffered = let buffered =
fmt::from_fn(|f| Unparser::new(f, self.source).unparse_expr(val, precedence::TEST + 1)) fmt::from_fn(|f| Unparser::new(f, self.source).unparse_expr(val, precedence::TEST + 1))
.to_string(); .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(leading)?;
self.p(self.source.slice(val.range()))?; self.p(self.source.slice(val.range()))?;
self.p(trailing)?; self.p(trailing)?;
+7
View File
@@ -468,6 +468,13 @@ bitflags! {
const COROUTINE = 0x0080; const COROUTINE = 0x0080;
const ITERABLE_COROUTINE = 0x0100; const ITERABLE_COROUTINE = 0x0100;
const ASYNC_GENERATOR = 0x0200; 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; const FUTURE_ANNOTATIONS = 0x1000000;
/// If a code object represents a function and has a docstring, /// 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. /// 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). /// Stack effect when the instruction takes its branch (jump=true).
/// ///
/// CPython equivalent: `stack_effect(opcode, oparg, jump=True)`. /// CPython equivalent: `stack_effect(opcode, oparg, jump=True)`.
/// For most instructions this equals the fallthrough effect. /// Current opcode metadata has the same real-opcode stack effect
/// Override for instructions where branch and fallthrough differ /// for jump and fallthrough stack-depth calculation.
/// (e.g. [`Self::ForIter`]: fallthrough = +1, branch = −1).
#[must_use] #[must_use]
pub fn stack_effect_jump(&self, oparg: u32) -> i32 { pub fn stack_effect_jump(&self, oparg: u32) -> i32 {
self.stack_effect(oparg) self.stack_effect(oparg)
@@ -1415,6 +1414,26 @@ mod tests {
assert!(!AnyInstruction::from(PseudoOpcode::Jump).has_const()); 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] #[test]
fn no_fallthrough_flags_match_cpython_basicblock_nofallthrough() { fn no_fallthrough_flags_match_cpython_basicblock_nofallthrough() {
assert!(Opcode::JumpForward.is_no_fallthrough()); assert!(Opcode::JumpForward.is_no_fallthrough());
+4 -3
View File
@@ -777,19 +777,20 @@ oparg_enum!(
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct UnpackExArgs { pub struct UnpackExArgs {
pub before: u8, pub before: u8,
pub after: u8, pub after: u32,
} }
impl From<u32> for UnpackExArgs { impl From<u32> for UnpackExArgs {
fn from(value: u32) -> Self { 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 } Self { before, after }
} }
} }
impl From<UnpackExArgs> for u32 { impl From<UnpackExArgs> for u32 {
fn from(value: UnpackExArgs) -> Self { 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 scope = vm.new_scope_with_builtins();
let code_obj = vm let code_obj = vm
.compile(source.trim(), Mode::Exec, FNAME) .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(); .unwrap();
scope.globals.set_item("code", code_obj.into(), vm).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 let py_code_obj = vm
.compile(py_source, Mode::Exec, FNAME) .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(); .unwrap();
vm.run_code_obj(py_code_obj, scope.clone()).unwrap(); vm.run_code_obj(py_code_obj, scope.clone()).unwrap();
@@ -1,5 +1,6 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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\"#)" 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 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>: Disassembly of <code object __annotate__ at 0xdeadbeef file "<?>", line 1>:
1 RESUME 0 1 RESUME 0
LOAD_FAST_BORROW 0 (format) LOAD_FAST_CHECK 0 (format)
LOAD_SMALL_INT 2 LOAD_SMALL_INT 2
COMPARE_OP 132 (>) COMPARE_OP 132 (>)
POP_JUMP_IF_FALSE 3 (to L1) POP_JUMP_IF_FALSE 3 (to L1)
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs source: crates/stdlib/src/_opcode.rs
expression: x = not True assertion_line: 281
expression: "dis(r#\"\nx = not True\n\"#)"
--- ---
0 RESUME 0 0 RESUME 0
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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 0 RESUME 0
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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 0 RESUME 0
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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 0 RESUME 0
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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 0 RESUME 0
@@ -1,6 +1,7 @@
--- ---
source: crates/stdlib/src/_opcode.rs 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 0 RESUME 0
+1
View File
@@ -70,6 +70,7 @@ static_assertions = { workspace = true }
strum = { workspace = true } strum = { workspace = true }
strum_macros = { workspace = true } strum_macros = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
thin-vec = { workspace = true }
memchr = { workspace = true } memchr = { workspace = true }
flamer = { workspace = true, optional = true } flamer = { workspace = true, optional = true }
+68 -10
View File
@@ -34,7 +34,7 @@ use core::{
ops::Deref, ops::Deref,
pin::Pin, pin::Pin,
ptr::NonNull, ptr::NonNull,
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering},
}; };
use indexmap::{IndexMap, map::Entry}; use indexmap::{IndexMap, map::Entry};
use itertools::Itertools; use itertools::Itertools;
@@ -53,6 +53,7 @@ pub struct PyType {
pub heaptype_ext: Option<Pin<Box<HeapTypeExt>>>, pub heaptype_ext: Option<Pin<Box<HeapTypeExt>>>,
/// Type version tag for inline caching. 0 means unassigned/invalidated. /// Type version tag for inline caching. 0 means unassigned/invalidated.
pub tp_version_tag: AtomicU32, pub tp_version_tag: AtomicU32,
pub abc_tpflags: AtomicU64,
} }
/// Monotonic counter for type version tags. Once it reaches `u32::MAX`, /// 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 // Check each base in order and inherit the first collection flag found
for base in bases { 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() { if !base_flags.is_empty() {
slots.flags |= base_flags; slots.flags |= base_flags;
return; 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 /// Check for __abc_tpflags__ and set the appropriate flags
/// This checks in attrs and all base classes for __abc_tpflags__ /// This checks in attrs and all base classes for __abc_tpflags__
fn check_abc_tpflags( fn check_abc_tpflags(
@@ -626,21 +681,19 @@ impl PyType {
.to_owned(), .to_owned(),
); );
} }
// Don't override flags already inherited from a base class. slots.flags.remove(COLLECTION_FLAGS);
if !slots.flags.intersects(COLLECTION_FLAGS) { slots.flags |= masked;
slots.flags |= masked;
}
return Ok(()); return Ok(());
} }
// No __abc_tpflags__ on this class — inheritance already happened // No __abc_tpflags__ on this class. Inheritance already happened in
// in inherit_patma_flags, so nothing more to do if those bits are set. // inherit_patma_flags, using base order and including ABC markers.
if slots.flags.intersects(COLLECTION_FLAGS) { if slots.flags.intersects(COLLECTION_FLAGS) {
return Ok(()); return Ok(());
} }
// Then check in base classes (legacy path for cases that bypass // Then check in base classes for legacy paths that bypassed
// inherit_patma_flags). // inherit_patma_flags.
for base in bases { for base in bases {
if let Some(abc_tpflags_obj) = base.find_name_in_mro(abc_tpflags_name) 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>() && let Some(int_obj) = abc_tpflags_obj.downcast_ref::<crate::builtins::int::PyInt>()
@@ -654,6 +707,7 @@ impl PyType {
.to_owned(), .to_owned(),
); );
} }
slots.flags.remove(COLLECTION_FLAGS);
slots.flags |= masked; slots.flags |= masked;
return Ok(()); return Ok(());
} }
@@ -716,6 +770,7 @@ impl PyType {
)); ));
} }
let inherited_abc_tpflags = Self::inherited_abc_tpflags(&bases);
let new_type = PyRef::new_ref( let new_type = PyRef::new_ref(
Self { Self {
base: Some(base), base: Some(base),
@@ -726,6 +781,7 @@ impl PyType {
slots, slots,
heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))), heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))),
tp_version_tag: AtomicU32::new(0), tp_version_tag: AtomicU32::new(0),
abc_tpflags: AtomicU64::new(inherited_abc_tpflags),
}, },
metaclass, metaclass,
None, None,
@@ -775,6 +831,7 @@ impl PyType {
slots.flags |= PyTypeFlags::MANAGED_WEAKREF; 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 bases = PyRwLock::new(vec![base.clone()]);
let mro = base.mro_map_collect(|x| x.to_owned()); let mro = base.mro_map_collect(|x| x.to_owned());
@@ -788,6 +845,7 @@ impl PyType {
slots, slots,
heaptype_ext: None, heaptype_ext: None,
tp_version_tag: AtomicU32::new(0), tp_version_tag: AtomicU32::new(0),
abc_tpflags: AtomicU64::new(inherited_abc_tpflags),
}, },
metaclass, metaclass,
None, 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:?}"); debug!("Code object: {bytecode:?}");
vm.run_code_obj(bytecode, scope) 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 let maybe_lineno = zelf
.as_object() .as_object()
.get_attr("lineno", vm) .get_attr("lineno", vm)
.and_then(|obj| obj.str_utf8(vm)) .ok()
.ok(); .filter(|obj| !vm.is_none(obj))
let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| { .and_then(|obj| obj.str_utf8(vm).ok());
obj.str(vm) let maybe_filename = zelf
.unwrap_or_else(|_| vm.ctx.new_str("<filename str() failed>")) .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) { let msg = match zelf.as_object().get_attr("msg", vm) {
Ok(obj) => obj Ok(obj) => obj
+69 -23
View File
@@ -3050,9 +3050,17 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value(); let subject = self.pop_value();
let nargs_val = nargs.get(arg) as usize; 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 // Check if subject is an instance of cls
if subject.is_instance(cls.as_ref(), vm)? { if subject.is_instance(cls.as_ref(), vm)? {
let mut extracted = vec![]; let mut extracted = vec![];
let seen_attrs = PySet::default().into_ref(&vm.ctx);
// Get __match_args__ for positional arguments if nargs > 0 // Get __match_args__ for positional arguments if nargs > 0
if nargs_val > 0 { if nargs_val > 0 {
@@ -3066,12 +3074,7 @@ impl ExecutingFrame<'_> {
Ok(tuple) => tuple, Ok(tuple) => tuple,
Err(match_args) => { Err(match_args) => {
// __match_args__ must be a tuple // __match_args__ must be a tuple
// Get type names for error message let type_name = type_name();
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 match_args_type_name = match_args.class().__name__(vm); let match_args_type_name = match_args.class().__name__(vm);
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"{type_name}.__match_args__ must be a tuple (got {match_args_type_name})" "{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 // Check if we have enough match args
if match_args.len() < nargs_val { 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!( 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(), match_args.len(),
plural,
nargs_val nargs_val
))); )));
} }
@@ -3094,11 +3100,20 @@ impl ExecutingFrame<'_> {
let attr_name_str = match attr_name.downcast_ref::<PyStr>() { let attr_name_str = match attr_name.downcast_ref::<PyStr>() {
Some(s) => s, Some(s) => s,
None => { None => {
return Err(vm.new_type_error( let attr_type_name = attr_name.class().name();
"__match_args__ elements must be strings", 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) { match subject.get_attr(attr_name_str, vm) {
Ok(value) => extracted.push(value), Ok(value) => extracted.push(value),
Err(e) Err(e)
@@ -3115,9 +3130,8 @@ impl ExecutingFrame<'_> {
// No __match_args__, check if this is a type with MATCH_SELF behavior // 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. // For built-in types like bool, int, str, list, tuple, dict, etc.
// they match the subject itself as the single positional argument // they match the subject itself as the single positional argument
let is_match_self_type = cls let is_match_self_type =
.downcast::<PyType>() cls_type.slots.flags.contains(PyTypeFlags::_MATCH_SELF);
.is_ok_and(|t| t.slots.flags.contains(PyTypeFlags::_MATCH_SELF));
if is_match_self_type { if is_match_self_type {
if nargs_val == 1 { if nargs_val == 1 {
@@ -3125,16 +3139,18 @@ impl ExecutingFrame<'_> {
extracted.push(subject.clone()); extracted.push(subject.clone());
} else if nargs_val > 1 { } else if nargs_val > 1 {
// Too many positional arguments for MATCH_SELF // Too many positional arguments for MATCH_SELF
return Err(vm.new_type_error( let type_name = type_name();
"class pattern accepts at most 1 positional sub-pattern for MATCH_SELF types", return Err(vm.new_type_error(format!(
)); "{type_name}() accepts 1 positional sub-pattern ({nargs_val} given)"
)));
} }
} else { } else {
// No __match_args__ and not a MATCH_SELF type // No __match_args__ and not a MATCH_SELF type
if nargs_val > 0 { if nargs_val > 0 {
return Err(vm.new_type_error( let type_name = type_name();
"class pattern defines no positional sub-patterns (__match_args__ missing)", 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 // Extract keyword attributes
for name in kwd_attrs { for name in kwd_attrs {
let name_str = name.downcast_ref::<PyStr>().unwrap(); 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) { match subject.get_attr(name_str, vm) {
Ok(value) => extracted.push(value), Ok(value) => extracted.push(value),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => { 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] let subject = self.nth_value(1); // stack[-2]
// Check if subject is a mapping and extract values for keys // 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 keys = keys_tuple.downcast_ref::<PyTuple>().unwrap();
let mut values = Vec::new(); let mut values = Vec::new();
let mut all_match = true; 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: // 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. // - 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); .new_base_object(vm.ctx.types.object_type.to_owned(), None);
for key in keys { 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) // value = map.get(key, dummy)
match get_method.call((key.as_object(), dummy.clone()), vm) { match get_method.call((key.as_object(), dummy.clone()), vm) {
Ok(value) => { Ok(value) => {
@@ -3202,6 +3237,13 @@ impl ExecutingFrame<'_> {
} else { } else {
// Fallback if .get() method is not available (shouldn't happen for mappings) // Fallback if .get() method is not available (shouldn't happen for mappings)
for key in keys { 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) { match subject.get_item(key.as_object(), vm) {
Ok(value) => values.push(value), Ok(value) => values.push(value),
Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => { Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {
@@ -3231,7 +3273,9 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value(); let subject = self.pop_value();
// Check if the type has the MAPPING flag // 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(subject);
self.push_value(vm.ctx.new_bool(is_mapping).into()); self.push_value(vm.ctx.new_bool(is_mapping).into());
@@ -3242,7 +3286,9 @@ impl ExecutingFrame<'_> {
let subject = self.pop_value(); let subject = self.pop_value();
// Check if the type has the SEQUENCE flag // 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(subject);
self.push_value(vm.ctx.new_bool(is_sequence).into()); 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 (before, after) = (before as usize, after as usize);
let value = self.pop_value(); let value = self.pop_value();
let not_iterable = value.class().slots.iter.load().is_none() let not_iterable = value.class().slots.iter.load().is_none()
+2 -2
View File
@@ -141,7 +141,7 @@ pub fn import_file(
file_path, file_path,
vm.compile_opts(), 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) 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>", "<source>",
vm.compile_opts(), 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) 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(), slots: PyType::make_slots(),
heaptype_ext: None, heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0), tp_version_tag: core::sync::atomic::AtomicU32::new(0),
abc_tpflags: core::sync::atomic::AtomicU64::new(0),
}; };
let object_payload = PyType { let object_payload = PyType {
base: None, base: None,
@@ -2489,6 +2490,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) {
slots: object::PyBaseObject::make_slots(), slots: object::PyBaseObject::make_slots(),
heaptype_ext: None, heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0), 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 // 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. // 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(), slots: PyWeak::make_slots(),
heaptype_ext: None, heaptype_ext: None,
tp_version_tag: core::sync::atomic::AtomicU32::new(0), 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); 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) // 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, event: TraceEvent,
arg: Option<PyObjectRef>, arg: Option<PyObjectRef>,
) -> PyResult<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) self._trace_event_inner(event, arg)
} else { } else {
Ok(None) Ok(None)
@@ -247,7 +247,9 @@ impl VirtualMachine {
// tracing function itself. // tracing function itself.
if is_trace_event && !self.is_none(&trace_func) { if is_trace_event && !self.is_none(&trace_func) {
self.use_tracing.set(false); self.use_tracing.set(false);
self.enter_tracing();
let res = trace_func.call(args.clone(), self); let res = trace_func.call(args.clone(), self);
self.leave_tracing();
self.use_tracing.set(true); self.use_tracing.set(true);
match res { match res {
Ok(result) => { Ok(result) => {
@@ -268,7 +270,9 @@ impl VirtualMachine {
if is_profile_event && !self.is_none(&profile_func) { if is_profile_event && !self.is_none(&profile_func) {
self.use_tracing.set(false); self.use_tracing.set(false);
self.enter_tracing();
let res = profile_func.call(args, self); let res = profile_func.call(args, self);
self.leave_tracing();
self.use_tracing.set(true); self.use_tracing.set(true);
if res.is_err() { if res.is_err() {
*self.profile_func.borrow_mut() = self.ctx.none(); *self.profile_func.borrow_mut() = self.ctx.none();
+19 -2
View File
@@ -9,11 +9,11 @@ pub(crate) use _abc::module_def;
mod _abc { mod _abc {
use crate::{ use crate::{
AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, 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, common::lock::PyRwLock,
convert::ToPyObject, convert::ToPyObject,
protocol::PyIterReturn, protocol::PyIterReturn,
types::Constructor, types::{Constructor, PyTypeFlags},
}; };
use core::sync::atomic::{AtomicU64, Ordering}; use core::sync::atomic::{AtomicU64, Ordering};
@@ -238,6 +238,23 @@ mod _abc {
// Invalidate negative cache // Invalidate negative cache
increment_invalidation_counter(); 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) 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; use rustpython_compiler_core::SourceFile;
pub(super) struct PositionalArguments { pub(super) struct PositionalArguments {
pub range: TextRange, range: TextRange,
pub args: Box<[ast::Expr]>, 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 { impl Node for PositionalArguments {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { args, range: _ } = self; match self.kind {
BoxedSlice(args).ast_to_object(vm, source_file) 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( fn ast_from_object(
@@ -18,10 +82,7 @@ impl Node for PositionalArguments {
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self { Ok(Self::from_args(TextRange::default(), args.0))
args: args.0,
range: TextRange::default(), // TODO
})
} }
} }
@@ -30,6 +91,21 @@ pub(super) struct KeywordArguments {
pub keywords: Box<[ast::Keyword]>, 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 { impl Node for KeywordArguments {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { keywords, range: _ } = self; let Self { keywords, range: _ } = self;
@@ -45,7 +121,7 @@ impl Node for KeywordArguments {
let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self { Ok(Self {
keywords: keywords.0, keywords: keywords.0,
range: TextRange::default(), // TODO range: TextRange::default(),
}) })
} }
} }
@@ -54,13 +130,16 @@ pub(super) fn merge_function_call_arguments(
pos_args: PositionalArguments, pos_args: PositionalArguments,
key_args: KeywordArguments, key_args: KeywordArguments,
) -> ast::Arguments { ) -> 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 { ast::Arguments {
node_index: Default::default(), node_index: Default::default(),
range, range,
args: pos_args.args, args,
keywords: key_args.keywords, keywords: key_args.keywords,
runtime_args,
runtime_bases: None,
} }
} }
@@ -68,10 +147,12 @@ pub(super) fn split_function_call_arguments(
args: ast::Arguments, args: ast::Arguments,
) -> (PositionalArguments, KeywordArguments) { ) -> (PositionalArguments, KeywordArguments) {
let ast::Arguments { let ast::Arguments {
node_index: _,
range: _, range: _,
args, args,
keywords, keywords,
runtime_args,
runtime_bases: _,
..
} = args; } = args;
let positional_arguments_range = args let positional_arguments_range = args
@@ -80,10 +161,10 @@ pub(super) fn split_function_call_arguments(
.reduce(|acc, next| acc.cover(next)) .reduce(|acc, next| acc.cover(next))
.unwrap_or_default(); .unwrap_or_default();
// debug_assert!(range.contains_range(positional_arguments_range)); // debug_assert!(range.contains_range(positional_arguments_range));
let positional_arguments = PositionalArguments { let positional_arguments = runtime_args.map_or_else(
range: positional_arguments_range, || PositionalArguments::from_args(positional_arguments_range, args),
args, |values| PositionalArguments::from_runtime_values(positional_arguments_range, values),
}; );
let keyword_arguments_range = keywords let keyword_arguments_range = keywords
.iter() .iter()
@@ -107,10 +188,12 @@ pub(super) fn split_class_def_args(
Some(args) => *args, Some(args) => *args,
}; };
let ast::Arguments { let ast::Arguments {
node_index: _,
range: _, range: _,
args, args,
keywords, keywords,
runtime_args: _,
runtime_bases,
..
} = args; } = args;
let positional_arguments_range = args let positional_arguments_range = args
@@ -119,10 +202,10 @@ pub(super) fn split_class_def_args(
.reduce(|acc, next| acc.cover(next)) .reduce(|acc, next| acc.cover(next))
.unwrap_or_default(); .unwrap_or_default();
// debug_assert!(range.contains_range(positional_arguments_range)); // debug_assert!(range.contains_range(positional_arguments_range));
let positional_arguments = PositionalArguments { let positional_arguments = runtime_bases.map_or_else(
range: positional_arguments_range, || PositionalArguments::from_args(positional_arguments_range, args),
args, |values| PositionalArguments::from_runtime_values(positional_arguments_range, values),
}; );
let keyword_arguments_range = keywords let keyword_arguments_range = keywords
.iter() .iter()
@@ -146,10 +229,10 @@ pub(super) fn merge_class_def_args(
return None; return None;
} }
let args = if let Some(positional_arguments) = positional_arguments { let (args, runtime_bases) = if let Some(positional_arguments) = positional_arguments {
positional_arguments.args positional_arguments.into_args_and_runtime_values()
} else { } else {
vec![].into_boxed_slice() (vec![].into_boxed_slice(), None)
}; };
let keywords = if let Some(keyword_arguments) = keyword_arguments { let keywords = if let Some(keyword_arguments) = keyword_arguments {
keyword_arguments.keywords keyword_arguments.keywords
@@ -162,5 +245,7 @@ pub(super) fn merge_class_def_args(
range: Default::default(), // TODO range: Default::default(), // TODO
args, args,
keywords, keywords,
runtime_args: None,
runtime_bases,
})) }))
} }
+7 -3
View File
@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::builtins::PyIntRef;
use rustpython_codegen::compile::ruff_int_to_bigint; use rustpython_codegen::compile::ruff_int_to_bigint;
use rustpython_compiler_core::SourceFile; use rustpython_compiler_core::SourceFile;
@@ -13,7 +14,11 @@ impl Node for ast::Identifier {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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())) Ok(Self::new(py_str.as_str(), TextRange::default()))
} }
} }
@@ -28,7 +33,6 @@ impl Node for ast::Int {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
// FIXME: performance
let value: PyIntRef = object.try_into_value(vm)?; let value: PyIntRef = object.try_into_value(vm)?;
let value = value.as_bigint().to_string(); let value = value.as_bigint().to_string();
Ok(value.parse().unwrap()) Ok(value.parse().unwrap())
@@ -45,6 +49,6 @@ impl Node for bool {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 super::*;
use crate::builtins::{PyComplex, PyFrozenSet, PyTuple}; use crate::builtins::{PyComplex, PyFrozenSet, PyTuple};
use ast::str_prefix::StringLiteralPrefix; 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)] #[derive(Debug)]
pub(super) struct Constant { pub(super) struct Constant {
pub(super) range: TextRange, pub(super) range: TextRange,
pub(super) value: ConstantLiteral, pub(super) value: ConstantLiteral,
kind: Option<Box<str>>,
invalid_type: Option<String>,
} }
impl Constant { impl Constant {
@@ -19,6 +22,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Str { value, prefix }, value: ConstantLiteral::Str { value, prefix },
kind: None,
invalid_type: None,
} }
} }
@@ -26,6 +31,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Int(value), value: ConstantLiteral::Int(value),
kind: None,
invalid_type: None,
} }
} }
@@ -33,6 +40,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Float(value), value: ConstantLiteral::Float(value),
kind: None,
invalid_type: None,
} }
} }
@@ -40,6 +49,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Complex { real, imag }, value: ConstantLiteral::Complex { real, imag },
kind: None,
invalid_type: None,
} }
} }
@@ -47,6 +58,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Bytes(value), value: ConstantLiteral::Bytes(value),
kind: None,
invalid_type: None,
} }
} }
@@ -54,6 +67,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Bool(value), value: ConstantLiteral::Bool(value),
kind: None,
invalid_type: None,
} }
} }
@@ -61,6 +76,8 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::None, value: ConstantLiteral::None,
kind: None,
invalid_type: None,
} }
} }
@@ -68,15 +85,29 @@ impl Constant {
Self { Self {
range, range,
value: ConstantLiteral::Ellipsis, value: ConstantLiteral::Ellipsis,
kind: None,
invalid_type: None,
} }
} }
pub(crate) fn into_expr(self) -> ast::Expr { 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 { pub(crate) enum ConstantLiteral {
None, None,
Bool(bool), Bool(bool),
@@ -96,20 +127,361 @@ pub(crate) enum ConstantLiteral {
Ellipsis, 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 // 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 { impl Node for Constant {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { 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 let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprConstant::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeExprConstant::static_type().to_owned())
.unwrap(); .unwrap();
let kind = match &value { let kind = kind
ConstantLiteral::Str { .or_else(|| constant_literal_kind(&value))
prefix: StringLiteralPrefix::Unicode, .map_or_else(|| vm.ctx.none(), |kind| vm.ctx.new_str(kind).into());
..
} => vm.ctx.new_str("u").into(),
_ => vm.ctx.none(),
};
let value = value.ast_to_object(vm, source_file); let value = value.ast_to_object(vm, source_file);
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("value", value, vm).unwrap(); dict.set_item("value", value, vm).unwrap();
@@ -123,16 +495,8 @@ impl Node for Constant {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let value_object = get_node_field(vm, &object, "value", "Constant")?; let range = range_from_object(vm, source_file, object.clone(), "Constant")?;
let value = Node::ast_from_object(vm, source_file, value_object)?; constant_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
@@ -201,8 +565,12 @@ impl Node for ConstantLiteral {
})?; })?;
let tuple = tuple let tuple = tuple
.into_iter() .into_iter()
.cloned() .map(|object| {
.map(|object| Node::ast_from_object(vm, source_file, object)) let object = object.clone();
vm.with_recursion(" during compilation", || {
Node::ast_from_object(vm, source_file, object)
})
})
.collect::<PyResult<_>>()?; .collect::<PyResult<_>>()?;
Self::Tuple(tuple) Self::Tuple(tuple)
} else if cls.is(vm.ctx.types.frozenset_type) { } else if cls.is(vm.ctx.types.frozenset_type) {
@@ -210,7 +578,11 @@ impl Node for ConstantLiteral {
let elements = set let elements = set
.elements() .elements()
.into_iter() .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<_>>()?; .collect::<PyResult<_>>()?;
Self::FrozenSet(elements) Self::FrozenSet(elements)
} else if cls.is(vm.ctx.types.float_type) { } 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( pub(super) fn number_literal_to_object(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
@@ -364,6 +625,7 @@ pub(super) fn number_literal_to_object(
node_index: _, node_index: _,
range, range,
value, value,
..
} = constant; } = constant;
let c = match value { let c = match value {
ast::Number::Int(n) => Constant::new_int(n, range), ast::Number::Int(n) => Constant::new_int(n, range),
@@ -382,6 +644,7 @@ pub(super) fn string_literal_to_object(
node_index: _, node_index: _,
range, range,
value, value,
..
} = constant; } = constant;
let prefix = value let prefix = value
.iter() .iter()
@@ -400,6 +663,7 @@ pub(super) fn bytes_literal_to_object(
node_index: _, node_index: _,
range, range,
value, value,
..
} = constant; } = constant;
let bytes = value.as_slice().iter().flat_map(|b| b.value.iter()); let bytes = value.as_slice().iter().flat_map(|b| b.value.iter());
let c = Constant::new_bytes(bytes.copied().collect(), range); let c = Constant::new_bytes(bytes.copied().collect(), range);
@@ -415,6 +679,7 @@ pub(super) fn boolean_literal_to_object(
node_index: _, node_index: _,
range, range,
value, value,
..
} = constant; } = constant;
let c = Constant::new_bool(value, range); let c = Constant::new_bool(value, range);
c.ast_to_object(vm, source_file) c.ast_to_object(vm, source_file)
@@ -428,6 +693,7 @@ pub(super) fn none_literal_to_object(
let ast::ExprNoneLiteral { let ast::ExprNoneLiteral {
node_index: _, node_index: _,
range, range,
..
} = constant; } = constant;
let c = Constant::new_none(range); let c = Constant::new_none(range);
c.ast_to_object(vm, source_file) c.ast_to_object(vm, source_file)
@@ -441,6 +707,7 @@ pub(super) fn ellipsis_literal_to_object(
let ast::ExprEllipsisLiteral { let ast::ExprEllipsisLiteral {
node_index: _, node_index: _,
range, range,
..
} = constant; } = constant;
let c = Constant::new_ellipsis(range); let c = Constant::new_ellipsis(range);
c.ast_to_object(vm, source_file) c.ast_to_object(vm, source_file)
+37 -16
View File
@@ -12,10 +12,15 @@ pub(super) fn ast_to_object(
range, range,
test, test,
body, body,
runtime_body,
runtime_orelse,
} = clause; } = clause;
let Some(test) = test else { let Some(test) = test else {
assert!(rest.len() == 0); 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 let node = NodeAst
.into_ref_with_type(vm, pyast::NodeStmtIf::static_type().to_owned()) .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) dict.set_item("test", test.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) let body = runtime_body.map_or_else(
.unwrap(); || 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() { if next.test.is_some() {
let next = ast::ElifElseClause { let next = ast::ElifElseClause {
range: TextRange::new(next.range.start(), range.end()), 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)]) .new_list(vec![ast_to_object(next, rest, vm, source_file)])
.into() .into()
} else { } 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 { } else {
vm.ctx.new_list(vec![]).into() vm.ctx.new_list(vec![]).into()
@@ -48,40 +61,45 @@ pub(super) fn ast_to_object(
node.into() node.into()
} }
pub(super) fn ast_from_object( pub(super) fn ast_from_object_with_range(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
range: TextRange,
) -> PyResult<ast::StmtIf> { ) -> PyResult<ast::StmtIf> {
let test = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "test", "If")?)?; let test = get_required_node_field(vm, source_file, &object, "test", "If")?;
let body = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "body", "If")?)?; let body: Vec<Option<ast::Stmt>> = get_node_list_field(vm, source_file, &object, "body", "If")?;
let orelse: Vec<ast::Stmt> = Node::ast_from_object( let orelse: Vec<Option<ast::Stmt>> =
vm, get_node_list_field(vm, source_file, &object, "orelse", "If")?;
source_file, let runtime_body = runtime_stmt_list_metadata(&body);
get_node_field(vm, &object, "orelse", "If")?, let runtime_orelse = runtime_stmt_list_metadata(&orelse);
)?; let body = lower_runtime_stmt_list(body);
let range = range_from_object(vm, source_file, object, "If")?; let orelse = lower_runtime_stmt_list(orelse);
let elif_else_clauses = if orelse.is_empty() { let elif_else_clauses = if orelse.is_empty() {
vec![] vec![]
} else if let [ast::Stmt::If(_)] = &*orelse { } else if let [ast::Stmt::If(_)] = &*orelse {
let Some(ast::Stmt::If(ast::StmtIf { let Some(ast::Stmt::If(ast::StmtIf {
node_index: _, node_index,
range, range,
test, test,
body, body,
mut elif_else_clauses, mut elif_else_clauses,
runtime_body,
})) = orelse.into_iter().next() })) = orelse.into_iter().next()
else { else {
unreachable!() unreachable!()
}; };
debug_assert!(runtime_orelse.is_none());
elif_else_clauses.insert( elif_else_clauses.insert(
0, 0,
ast::ElifElseClause { ast::ElifElseClause {
node_index: Default::default(), node_index,
range, range,
test: Some(*test), test: Some(*test),
body, body,
runtime_body,
runtime_orelse: None,
}, },
); );
elif_else_clauses elif_else_clauses
@@ -91,6 +109,8 @@ pub(super) fn ast_from_object(
range, range,
test: None, test: None,
body: orelse, body: orelse,
runtime_body: runtime_orelse,
runtime_orelse: None,
}] }]
}; };
@@ -100,5 +120,6 @@ pub(super) fn ast_from_object(
body, body,
elif_else_clauses, elif_else_clauses,
range, range,
runtime_body,
}) })
} }
+67 -32
View File
@@ -1,6 +1,22 @@
use super::*; use super::*;
use rustpython_compiler_core::SourceFile; 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 // sum
impl Node for ast::ExceptHandler { impl Node for ast::ExceptHandler {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
@@ -13,25 +29,53 @@ impl Node for ast::ExceptHandler {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); ensure_excepthandler_node(vm, &object)?;
Ok( let range = excepthandler_range_from_object(vm, source_file, object.clone())?;
if cls.is(pyast::NodeExceptHandlerExceptHandler::static_type()) { Ok(Self::ExceptHandler(except_handler_from_object_with_range(
Self::ExceptHandler(ast::ExceptHandlerExceptHandler::ast_from_object( vm,
vm, source_file,
source_file, object,
object, range,
)?) )?))
} else {
return Err(vm.new_type_error(format!(
"expected some sort of excepthandler, but got {}",
object.repr(vm)?
)));
},
)
} }
} }
// constructor // 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 { impl Node for ast::ExceptHandlerExceptHandler {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -40,6 +84,7 @@ impl Node for ast::ExceptHandlerExceptHandler {
name, name,
body, body,
range, range,
runtime_body,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type( .into_ref_with_type(
@@ -52,8 +97,11 @@ impl Node for ast::ExceptHandlerExceptHandler {
.unwrap(); .unwrap();
dict.set_item("name", name.ast_to_object(vm, source_file), vm) dict.set_item("name", name.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) let body = runtime_body.map_or_else(
.unwrap(); || 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_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -63,20 +111,7 @@ impl Node for ast::ExceptHandlerExceptHandler {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "ExceptHandler")?;
node_index: Default::default(), except_handler_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
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 /// - `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`). /// annotations, representing the type hints of a function (e.g., `def add(x: int, y: int) -> int`).
pub(super) enum Mod { pub(super) enum Mod {
Module(ast::ModModule), Module(ModModule),
Interactive(ModInteractive), Interactive(ModInteractive),
Expression(ast::ModExpression), Expression(ast::ModExpression),
FunctionType(ModFunctionType), FunctionType(ModFunctionType),
@@ -40,53 +40,66 @@ impl Node for Mod {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(
Ok(if cls.is(pyast::NodeModModule::static_type()) { if object.is_instance(pyast::NodeModModule::static_type().as_object(), vm)? {
Self::Module(ast::ModModule::ast_from_object(vm, source_file, object)?) Self::Module(ModModule::ast_from_object(vm, source_file, object)?)
} else if cls.is(pyast::NodeModInteractive::static_type()) { } else if object
Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?) .is_instance(pyast::NodeModInteractive::static_type().as_object(), vm)?
} else if cls.is(pyast::NodeModExpression::static_type()) { {
Self::Expression(ast::ModExpression::ast_from_object( Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?)
vm, } else if object.is_instance(pyast::NodeModExpression::static_type().as_object(), vm)? {
source_file, Self::Expression(ast::ModExpression::ast_from_object(
object, vm,
)?) source_file,
} else if cls.is(pyast::NodeModFunctionType::static_type()) { object,
Self::FunctionType(ModFunctionType::ast_from_object(vm, source_file, object)?) )?)
} else { } else if object
return Err(vm.new_type_error(format!( .is_instance(pyast::NodeModFunctionType::static_type().as_object(), vm)?
"expected some sort of mod, but got {}", {
object.repr(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 // constructor
impl Node for ast::ModModule { impl Node for ModModule {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
module,
type_ignores,
} = self;
let ast::ModModule {
node_index: _, node_index: _,
body, body,
// type_ignores, range: _,
range, runtime_body,
} = self; } = module;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModModule::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeModModule::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) let body = runtime_body.map_or_else(
.unwrap(); || body.ast_to_object(vm, source_file),
// TODO: Improve ruff API |values| values.ast_to_object(vm, source_file),
// ruff ignores type_ignore comments currently. );
let type_ignores: Vec<TypeIgnore> = vec![]; dict.set_item("body", body, vm).unwrap();
dict.set_item( dict.set_item(
"type_ignores", "type_ignores",
type_ignores.ast_to_object(vm, source_file), type_ignores.ast_to_object(vm, source_file),
vm, vm,
) )
.unwrap(); .unwrap();
let _ = range;
node.into() node.into()
} }
@@ -95,38 +108,45 @@ impl Node for ast::ModModule {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 { Ok(Self {
node_index: Default::default(), module: ast::ModModule {
body: Node::ast_from_object( node_index: Default::default(),
vm, body,
source_file, range: Default::default(),
get_node_field(vm, &object, "body", "Module")?, runtime_body,
)?, },
// type_ignores: Node::ast_from_object( type_ignores,
// _vm,
// get_node_field(_vm, &_object, "type_ignores", "Module")?,
// )?,
range: Default::default(),
}) })
} }
} }
pub(super) struct ModInteractive { pub(super) struct ModInteractive {
pub(crate) range: TextRange, 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 // constructor
impl Node for ModInteractive { impl Node for ModInteractive {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { 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 let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModInteractive::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeModInteractive::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) let body = runtime_body.map_or_else(
.unwrap(); || body.ast_to_object(vm, source_file),
let _ = range; |values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
node.into() node.into()
} }
@@ -135,13 +155,13 @@ impl Node for ModInteractive {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 { Ok(Self {
body: Node::ast_from_object( body,
vm,
source_file,
get_node_field(vm, &object, "body", "Interactive")?,
)?,
range: Default::default(), range: Default::default(),
runtime_body,
}) })
} }
} }
@@ -152,7 +172,7 @@ impl Node for ast::ModExpression {
let Self { let Self {
node_index: _, node_index: _,
body, body,
range, range: _,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModExpression::static_type().to_owned()) .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(); let dict = node.as_object().dict().unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) dict.set_item("body", body.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
let _ = range;
node.into() node.into()
} }
@@ -171,11 +190,7 @@ impl Node for ast::ModExpression {
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
body: Node::ast_from_object( body: get_required_node_field(vm, source_file, &object, "body", "Expression")?,
vm,
source_file,
get_node_field(vm, &object, "body", "Expression")?,
)?,
range: Default::default(), range: Default::default(),
}) })
} }
@@ -184,7 +199,7 @@ impl Node for ast::ModExpression {
pub(super) struct ModFunctionType { pub(super) struct ModFunctionType {
pub(crate) argtypes: Box<[ast::Expr]>, pub(crate) argtypes: Box<[ast::Expr]>,
pub(crate) returns: ast::Expr, pub(crate) returns: ast::Expr,
pub(crate) range: TextRange, pub(crate) runtime_argtypes: Option<Vec<Option<ast::Expr>>>,
} }
// constructor // constructor
@@ -193,21 +208,19 @@ impl Node for ModFunctionType {
let Self { let Self {
argtypes, argtypes,
returns, returns,
range, runtime_argtypes,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodeModFunctionType::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeModFunctionType::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item( let argtypes = runtime_argtypes.map_or_else(
"argtypes", || BoxedSlice(argtypes).ast_to_object(vm, source_file),
BoxedSlice(argtypes).ast_to_object(vm, source_file), |values| values.ast_to_object(vm, source_file),
vm, );
) dict.set_item("argtypes", argtypes, vm).unwrap();
.unwrap();
dict.set_item("returns", returns.ast_to_object(vm, source_file), vm) dict.set_item("returns", returns.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
let _ = range;
node.into() node.into()
} }
@@ -216,21 +229,13 @@ impl Node for ModFunctionType {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 { Ok(Self {
argtypes: { argtypes: argtypes.into_boxed_slice(),
let argtypes: BoxedSlice<_> = Node::ast_from_object( returns: get_required_node_field(vm, source_file, &object, "returns", "FunctionType")?,
vm, runtime_argtypes,
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(),
}) })
} }
} }
+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 rustpython_compiler_core::SourceFile;
use thin_vec::ThinVec;
pub(crate) trait Node: Sized { pub(crate) trait Node: Sized {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef; 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, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
// Recursion guard for each element: prevents stack overflow when a let list = object.downcast_ref::<PyList>().ok_or_else(|| {
// sequence element transitively references the sequence itself vm.new_type_error(format!(
// (e.g. `l = ast.List(...); l.elts = [l]`). See issue #4862. "AST list field must be a list, not a {}",
vm.extract_elements_with(&object, |obj| { object.class().name()
vm.with_recursion("while traversing AST node", || { ))
Node::ast_from_object(vm, source_file, obj) })?;
}) 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, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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", || { vm.with_recursion("while traversing AST node", || {
T::ast_from_object(vm, source_file, object).map(Self::new) 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, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(
Ok(if cls.is(pyast::NodeBoolOpAnd::static_type()) { if is_node_instance(vm, &object, pyast::NodeBoolOpAnd::static_type())? {
Self::And Self::And
} else if cls.is(pyast::NodeBoolOpOr::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeBoolOpOr::static_type())? {
Self::Or Self::Or
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of boolop, but got {}", "expected some sort of boolop, but got {}",
object.repr(vm)? object.repr(vm)?
))); )));
}) },
)
} }
} }
@@ -56,39 +57,40 @@ impl Node for ast::Operator {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(
Ok(if cls.is(pyast::NodeOperatorAdd::static_type()) { if is_node_instance(vm, &object, pyast::NodeOperatorAdd::static_type())? {
Self::Add Self::Add
} else if cls.is(pyast::NodeOperatorSub::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorSub::static_type())? {
Self::Sub Self::Sub
} else if cls.is(pyast::NodeOperatorMult::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorMult::static_type())? {
Self::Mult Self::Mult
} else if cls.is(pyast::NodeOperatorMatMult::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorMatMult::static_type())? {
Self::MatMult Self::MatMult
} else if cls.is(pyast::NodeOperatorDiv::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorDiv::static_type())? {
Self::Div Self::Div
} else if cls.is(pyast::NodeOperatorMod::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorMod::static_type())? {
Self::Mod Self::Mod
} else if cls.is(pyast::NodeOperatorPow::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorPow::static_type())? {
Self::Pow Self::Pow
} else if cls.is(pyast::NodeOperatorLShift::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorLShift::static_type())? {
Self::LShift Self::LShift
} else if cls.is(pyast::NodeOperatorRShift::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorRShift::static_type())? {
Self::RShift Self::RShift
} else if cls.is(pyast::NodeOperatorBitOr::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorBitOr::static_type())? {
Self::BitOr Self::BitOr
} else if cls.is(pyast::NodeOperatorBitXor::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorBitXor::static_type())? {
Self::BitXor Self::BitXor
} else if cls.is(pyast::NodeOperatorBitAnd::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorBitAnd::static_type())? {
Self::BitAnd Self::BitAnd
} else if cls.is(pyast::NodeOperatorFloorDiv::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeOperatorFloorDiv::static_type())? {
Self::FloorDiv Self::FloorDiv
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of operator, but got {}", "expected some sort of operator, but got {}",
object.repr(vm)? object.repr(vm)?
))); )));
}) },
)
} }
} }
@@ -109,21 +111,22 @@ impl Node for ast::UnaryOp {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(
Ok(if cls.is(pyast::NodeUnaryOpInvert::static_type()) { if is_node_instance(vm, &object, pyast::NodeUnaryOpInvert::static_type())? {
Self::Invert Self::Invert
} else if cls.is(pyast::NodeUnaryOpNot::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeUnaryOpNot::static_type())? {
Self::Not Self::Not
} else if cls.is(pyast::NodeUnaryOpUAdd::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeUnaryOpUAdd::static_type())? {
Self::UAdd Self::UAdd
} else if cls.is(pyast::NodeUnaryOpUSub::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeUnaryOpUSub::static_type())? {
Self::USub Self::USub
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of unaryop, but got {}", "expected some sort of unaryop, but got {}",
object.repr(vm)? object.repr(vm)?
))); )));
}) },
)
} }
} }
@@ -150,32 +153,33 @@ impl Node for ast::CmpOp {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(
Ok(if cls.is(pyast::NodeCmpOpEq::static_type()) { if is_node_instance(vm, &object, pyast::NodeCmpOpEq::static_type())? {
Self::Eq Self::Eq
} else if cls.is(pyast::NodeCmpOpNotEq::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpNotEq::static_type())? {
Self::NotEq Self::NotEq
} else if cls.is(pyast::NodeCmpOpLt::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpLt::static_type())? {
Self::Lt Self::Lt
} else if cls.is(pyast::NodeCmpOpLtE::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpLtE::static_type())? {
Self::LtE Self::LtE
} else if cls.is(pyast::NodeCmpOpGt::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpGt::static_type())? {
Self::Gt Self::Gt
} else if cls.is(pyast::NodeCmpOpGtE::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpGtE::static_type())? {
Self::GtE Self::GtE
} else if cls.is(pyast::NodeCmpOpIs::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpIs::static_type())? {
Self::Is Self::Is
} else if cls.is(pyast::NodeCmpOpIsNot::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpIsNot::static_type())? {
Self::IsNot Self::IsNot
} else if cls.is(pyast::NodeCmpOpIn::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpIn::static_type())? {
Self::In Self::In
} else if cls.is(pyast::NodeCmpOpNotIn::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeCmpOpNotIn::static_type())? {
Self::NotIn Self::NotIn
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of cmpop, but got {}", "expected some sort of cmpop, but got {}",
object.repr(vm)? object.repr(vm)?
))); )));
}) },
)
} }
} }
+14 -15
View File
@@ -11,14 +11,12 @@ impl Node for ast::ConversionFlag {
_source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
// Python's AST uses ASCII codes: 's', 'r', 'a', -1=None match node_object_to_i32(vm, object)? {
// Note: 255 is -1i8 as u8 (ruff's ConversionFlag::None) -1 => Ok(Self::None),
match i32::try_from_object(vm, object)? {
-1 | 255 => Ok(Self::None),
x if x == b's' as i32 => Ok(Self::Str), x if x == b's' as i32 => Ok(Self::Str),
x if x == b'r' as i32 => Ok(Self::Repr), x if x == b'r' as i32 => Ok(Self::Repr),
x if x == b'a' as i32 => Ok(Self::Ascii), 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, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
match object.downcast::<PyStr>() { if !object.class().is(vm.ctx.types.str_type) {
Ok(name) => Ok(Self::new(name)), return Err(vm.new_type_error("AST identifier must be of type str"));
Err(_) => Err(vm.new_value_error("expected str for name")),
} }
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> { ) -> PyResult<Self> {
Ok(Self { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
name: Node::ast_from_object( name: get_required_identifier_field(vm, source_file, &object, "name", "alias")?,
vm,
source_file,
get_node_field_required(vm, &object, "name", "alias")?,
)?,
asname: get_node_field_opt(vm, &object, "asname")? asname: get_node_field_opt(vm, &object, "asname")?
.map(|obj| Node::ast_from_object(vm, source_file, obj)) .map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?, .transpose()?,
@@ -137,10 +134,12 @@ impl Node for ast::WithItem {
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
context_expr: Node::ast_from_object( context_expr: get_required_node_field(
vm, vm,
source_file, 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")? optional_vars: get_node_field_opt(vm, &object, "optional_vars")?
.map(|obj| Node::ast_from_object(vm, source_file, obj)) .map(|obj| Node::ast_from_object(vm, source_file, obj))
+161 -75
View File
@@ -11,10 +11,12 @@ impl Node for ast::Parameters {
vararg, vararg,
kwonlyargs, kwonlyargs,
kwarg, kwarg,
range, range: _,
runtime_defaults,
} = self; } = self;
let (posonlyargs, args, defaults) = let (posonlyargs, args, mut defaults) =
extract_positional_parameter_defaults(posonlyargs, args); extract_positional_parameter_defaults(posonlyargs, args);
defaults.runtime_defaults = runtime_defaults;
let (kwonlyargs, kw_defaults) = extract_keyword_parameter_defaults(kwonlyargs); let (kwonlyargs, kw_defaults) = extract_keyword_parameter_defaults(kwonlyargs);
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned())
@@ -40,9 +42,12 @@ impl Node for ast::Parameters {
.unwrap(); .unwrap();
dict.set_item("kwarg", kwarg.ast_to_object(vm, source_file), vm) dict.set_item("kwarg", kwarg.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item("defaults", defaults.ast_to_object(vm, source_file), vm) let runtime_defaults = defaults.runtime_defaults.take();
.unwrap(); let defaults = runtime_defaults.map_or_else(
let _ = range; || defaults.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("defaults", defaults, vm).unwrap();
node.into() node.into()
} }
@@ -51,33 +56,44 @@ impl Node for ast::Parameters {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let kwonlyargs = Node::ast_from_object( let posonlyargs = PositionalParameters::ast_from_field(
vm, vm,
source_file, 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, vm,
source_file, 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( let ParameterDefaults {
vm, runtime_defaults,
source_file, defaults,
get_node_field(vm, &object, "posonlyargs", "arguments")?, _range: _,
)?; } = defaults;
let args = Node::ast_from_object( let kwonlyargs = merge_keyword_parameter_defaults(vm, kwonlyargs, kw_defaults)?;
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 (posonlyargs, args) = let (posonlyargs, args) =
merge_positional_parameter_defaults(vm, posonlyargs, args, defaults)?; merge_positional_parameter_defaults(vm, posonlyargs, args, defaults)?;
@@ -85,14 +101,11 @@ impl Node for ast::Parameters {
node_index: Default::default(), node_index: Default::default(),
posonlyargs, posonlyargs,
args, args,
vararg: get_node_field_opt(vm, &object, "vararg")? vararg,
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
kwonlyargs, kwonlyargs,
kwarg: get_node_field_opt(vm, &object, "kwarg")? kwarg,
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?,
range: Default::default(), range: Default::default(),
runtime_defaults,
}) })
} }
@@ -103,62 +116,68 @@ impl Node for ast::Parameters {
// product // product
impl Node for ast::Parameter { 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 { let Self {
node_index: _, node_index: _,
name, name,
annotation, annotation,
// type_comment, // type_comment,
range, range,
runtime_type_comment,
runtime_type_comment_bytes,
} = self; } = self;
// ruff covers the ** in range but python expects it to start at the ident // ruff covers the ** in range but python expects it to start at the ident
let range = TextRange::new(name.start(), range.end()); let range = TextRange::new(name.start(), range.end());
let node = NodeAst 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(); .unwrap();
let dict = node.as_object().dict().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(); .unwrap();
dict.set_item( dict.set_item("annotation", annotation.ast_to_object(vm, source_file), vm)
"annotation", .unwrap();
annotation.ast_to_object(_vm, source_file), let type_comment = super::constant::runtime_string_object(
_vm, vm,
runtime_type_comment,
runtime_type_comment_bytes,
) )
.unwrap(); .unwrap_or_else(|| vm.ctx.none());
// Ruff AST doesn't track type_comment, so always set to None dict.set_item("type_comment", type_comment, vm).unwrap();
dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap(); node_add_location(&dict, range, vm, source_file);
node_add_location(&dict, range, _vm, source_file);
node.into() node.into()
} }
fn ast_from_object( fn ast_from_object(
_vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
_object: PyObjectRef, _object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
name: Node::ast_from_object( name,
_vm, annotation,
source_file, range,
get_node_field_required(_vm, &_object, "arg", "arg")?, runtime_type_comment,
)?, runtime_type_comment_bytes,
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")?,
}) })
} }
} }
// product // product
impl Node for ast::Keyword { 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 { let Self {
node_index: _, node_index: _,
arg, arg,
@@ -166,32 +185,28 @@ impl Node for ast::Keyword {
range: _range, range: _range,
} = self; } = self;
let node = NodeAst 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(); .unwrap();
let dict = node.as_object().dict().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(); .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(); .unwrap();
node_add_location(&dict, _range, _vm, source_file); node_add_location(&dict, _range, vm, source_file);
node.into() node.into()
} }
fn ast_from_object( fn ast_from_object(
_vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
_object: PyObjectRef, _object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
arg: get_node_field_opt(_vm, &_object, "arg")? arg: get_node_field_opt(vm, &_object, "arg")?
.map(|obj| Node::ast_from_object(_vm, source_file, obj)) .map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?, .transpose()?,
value: Node::ast_from_object( value: get_required_node_field(vm, source_file, &_object, "value", "keyword")?,
_vm, range: range_from_object(vm, source_file, _object, "keyword")?,
source_file,
get_node_field_required(_vm, &_object, "value", "keyword")?,
)?,
range: range_from_object(_vm, source_file, _object, "keyword")?,
}) })
} }
} }
@@ -201,6 +216,21 @@ struct PositionalParameters {
pub args: Box<[ast::Parameter]>, 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 { impl Node for PositionalParameters {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.args).ast_to_object(vm, source_file) 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)?; let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self { Ok(Self {
args: args.0, args: args.0,
_range: TextRange::default(), // TODO _range: TextRange::default(),
}) })
} }
} }
@@ -224,6 +254,21 @@ struct KeywordParameters {
pub keywords: Box<[ast::Parameter]>, 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 { impl Node for KeywordParameters {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.keywords).ast_to_object(vm, source_file) 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)?; let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self { Ok(Self {
keywords: keywords.0, keywords: keywords.0,
_range: TextRange::default(), // TODO _range: TextRange::default(),
}) })
} }
} }
struct ParameterDefaults { struct ParameterDefaults {
pub _range: TextRange, // TODO: Use this pub _range: TextRange, // TODO: Use this
runtime_defaults: Option<Vec<Option<ast::Expr>>>,
defaults: Box<[Option<Box<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 { impl Node for ParameterDefaults {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
BoxedSlice(self.defaults).ast_to_object(vm, source_file) 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)?; let defaults: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?;
Ok(Self { Ok(Self {
defaults: defaults.0, 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()) .map(|item| item.range())
.reduce(|acc, next| acc.cover(next)) .reduce(|acc, next| acc.cover(next))
.unwrap_or_default(), .unwrap_or_default(),
runtime_defaults: None,
defaults: defaults.into_boxed_slice(), defaults: defaults.into_boxed_slice(),
}; };
@@ -325,14 +411,13 @@ fn merge_positional_parameter_defaults(
vm: &VirtualMachine, vm: &VirtualMachine,
posonlyargs: PositionalParameters, posonlyargs: PositionalParameters,
args: PositionalParameters, args: PositionalParameters,
defaults: ParameterDefaults, defaults: Box<[Option<Box<ast::Expr>>]>,
) -> PyResult<( ) -> PyResult<(
Vec<ast::ParameterWithDefault>, Vec<ast::ParameterWithDefault>,
Vec<ast::ParameterWithDefault>, Vec<ast::ParameterWithDefault>,
)> { )> {
let posonlyargs = posonlyargs.args; let posonlyargs = posonlyargs.args;
let args = args.args; let args = args.args;
let defaults = defaults.defaults;
let mut posonlyargs: Vec<_> = <Box<[_]> as IntoIterator>::into_iter(posonlyargs) let mut posonlyargs: Vec<_> = <Box<[_]> as IntoIterator>::into_iter(posonlyargs)
.map(|parameter| ast::ParameterWithDefault { .map(|parameter| ast::ParameterWithDefault {
@@ -383,6 +468,7 @@ fn extract_keyword_parameter_defaults(
.map(|item| item.range()) .map(|item| item.range())
.reduce(|acc, next| acc.cover(next)) .reduce(|acc, next| acc.cover(next))
.unwrap_or_default(), .unwrap_or_default(),
runtime_defaults: None,
defaults: defaults.into_boxed_slice(), defaults: defaults.into_boxed_slice(),
}; };
@@ -422,5 +508,5 @@ fn merge_keyword_parameter_defaults(
default, default,
range: Default::default(), range: Default::default(),
}) })
.collect()) .collect::<Vec<_>>())
} }
+386 -189
View File
@@ -10,6 +10,7 @@ impl Node for ast::MatchCase {
guard, guard,
body, body,
range: _, range: _,
runtime_body,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodeMatchCase::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeMatchCase::static_type().to_owned())
@@ -19,8 +20,11 @@ impl Node for ast::MatchCase {
.unwrap(); .unwrap();
dict.set_item("guard", guard.ast_to_object(vm, source_file), vm) dict.set_item("guard", guard.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item("body", body.ast_to_object(vm, source_file), vm) let body = runtime_body.map_or_else(
.unwrap(); || body.ast_to_object(vm, source_file),
|values| values.ast_to_object(vm, source_file),
);
dict.set_item("body", body, vm).unwrap();
node.into() node.into()
} }
@@ -29,22 +33,18 @@ impl Node for ast::MatchCase {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> 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 { Ok(Self {
node_index: Default::default(), node_index: Default::default(),
pattern: Node::ast_from_object( pattern: get_required_node_field(vm, source_file, &object, "pattern", "match_case")?,
vm,
source_file,
get_node_field_required(vm, &object, "pattern", "match_case")?,
)?,
guard: get_node_field_opt(vm, &object, "guard")? guard: get_node_field_opt(vm, &object, "guard")?
.map(|obj| Node::ast_from_object(vm, source_file, obj)) .map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?, .transpose()?,
body: Node::ast_from_object( body,
vm,
source_file,
get_node_field(vm, &object, "body", "match_case")?,
)?,
range: Default::default(), range: Default::default(),
runtime_body,
}) })
} }
} }
@@ -68,64 +68,152 @@ impl Node for ast::Pattern {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); if vm.is_none(&object) {
Ok(if cls.is(pyast::NodePatternMatchValue::static_type()) { return Err(vm.new_type_error(format!(
Self::MatchValue(ast::PatternMatchValue::ast_from_object( "expected some sort of pattern, but got {}",
vm, object.repr(vm)?
source_file, )));
object, }
)?) enum PatternKind {
} else if cls.is(pyast::NodePatternMatchSingleton::static_type()) { Value,
Self::MatchSingleton(ast::PatternMatchSingleton::ast_from_object( Singleton,
vm, Sequence,
source_file, Mapping,
object, Class,
)?) Star,
} else if cls.is(pyast::NodePatternMatchSequence::static_type()) { As,
Self::MatchSequence(ast::PatternMatchSequence::ast_from_object( Or,
vm, }
source_file, let kind = if is_node_instance(vm, &object, pyast::NodePatternMatchValue::static_type())? {
object, PatternKind::Value
)?) } else if is_node_instance(vm, &object, pyast::NodePatternMatchSingleton::static_type())? {
} else if cls.is(pyast::NodePatternMatchMapping::static_type()) { PatternKind::Singleton
Self::MatchMapping(ast::PatternMatchMapping::ast_from_object( } else if is_node_instance(vm, &object, pyast::NodePatternMatchSequence::static_type())? {
vm, PatternKind::Sequence
source_file, } else if is_node_instance(vm, &object, pyast::NodePatternMatchMapping::static_type())? {
object, PatternKind::Mapping
)?) } else if is_node_instance(vm, &object, pyast::NodePatternMatchClass::static_type())? {
} else if cls.is(pyast::NodePatternMatchClass::static_type()) { PatternKind::Class
Self::MatchClass(ast::PatternMatchClass::ast_from_object( } else if is_node_instance(vm, &object, pyast::NodePatternMatchStar::static_type())? {
vm, PatternKind::Star
source_file, } else if is_node_instance(vm, &object, pyast::NodePatternMatchAs::static_type())? {
object, PatternKind::As
)?) } else if is_node_instance(vm, &object, pyast::NodePatternMatchOr::static_type())? {
} else if cls.is(pyast::NodePatternMatchStar::static_type()) { PatternKind::Or
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,
)?)
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of pattern, but got {}", "expected some sort of pattern, but got {}",
object.repr(vm)? 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 // 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 { impl Node for ast::PatternMatchValue {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -148,19 +236,29 @@ impl Node for ast::PatternMatchValue {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchValue")?;
node_index: Default::default(), pattern_match_value_from_object_with_range(vm, source_file, object, range)
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "MatchValue")?,
)?,
range: range_from_object(vm, source_file, object, "MatchValue")?,
})
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchSingleton {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -186,15 +284,8 @@ impl Node for ast::PatternMatchSingleton {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchSingleton")?;
node_index: Default::default(), pattern_match_singleton_from_object_with_range(vm, source_file, object, range)
value: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "value", "MatchSingleton")?,
)?,
range: range_from_object(vm, source_file, object, "MatchSingleton")?,
})
} }
} }
@@ -219,21 +310,35 @@ impl Node for ast::Singleton {
} else if object.is(&vm.ctx.false_value) { } else if object.is(&vm.ctx.false_value) {
Ok(Self::False) Ok(Self::False)
} else { } else {
Err(vm.new_value_error(format!( Err(vm.new_value_error("MatchSingleton can only contain True, False and None"))
"Expected None, True, or False, got {:?}",
object.class().name()
)))
} }
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchSequence {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
node_index: _, node_index: _,
patterns, patterns,
range, range,
runtime_patterns,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type( .into_ref_with_type(
@@ -242,8 +347,11 @@ impl Node for ast::PatternMatchSequence {
) )
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) let patterns = runtime_patterns.map_or_else(
.unwrap(); || 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_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -253,19 +361,40 @@ impl Node for ast::PatternMatchSequence {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchSequence")?;
node_index: Default::default(), pattern_match_sequence_from_object_with_range(vm, source_file, object, range)
patterns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchSequence")?,
)?,
range: range_from_object(vm, source_file, object, "MatchSequence")?,
})
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchMapping {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -274,15 +403,23 @@ impl Node for ast::PatternMatchMapping {
patterns, patterns,
rest, rest,
range, range,
runtime_keys,
runtime_patterns,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchMapping::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodePatternMatchMapping::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("keys", keys.ast_to_object(vm, source_file), vm) let keys = runtime_keys.map_or_else(
.unwrap(); || keys.ast_to_object(vm, source_file),
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) |values| values.ast_to_object(vm, source_file),
.unwrap(); );
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) dict.set_item("rest", rest.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
node_add_location(&dict, range, vm, source_file); node_add_location(&dict, range, vm, source_file);
@@ -294,27 +431,57 @@ impl Node for ast::PatternMatchMapping {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchMapping")?;
node_index: Default::default(), pattern_match_mapping_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchClass {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -322,24 +489,36 @@ impl Node for ast::PatternMatchClass {
cls, cls,
arguments, arguments,
range, range,
runtime_patterns,
runtime_kwd_attrs,
runtime_kwd_patterns,
} = self; } = self;
let (patterns, kwd_attrs, kwd_patterns) = split_pattern_match_class(arguments);
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchClass::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodePatternMatchClass::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("cls", cls.ast_to_object(vm, source_file), vm) dict.set_item("cls", cls.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) let (patterns, kwd_attrs, kwd_patterns) =
.unwrap(); if let (Some(patterns), Some(kwd_attrs), Some(kwd_patterns)) =
dict.set_item("kwd_attrs", kwd_attrs.ast_to_object(vm, source_file), vm) (runtime_patterns, runtime_kwd_attrs, runtime_kwd_patterns)
.unwrap(); {
dict.set_item( (
"kwd_patterns", patterns.ast_to_object(vm, source_file),
kwd_patterns.ast_to_object(vm, source_file), kwd_attrs.ast_to_object(vm, source_file),
vm, kwd_patterns.ast_to_object(vm, source_file),
) )
.unwrap(); } 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_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -349,41 +528,8 @@ impl Node for ast::PatternMatchClass {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let patterns = Node::ast_from_object( let range = range_from_object(vm, source_file, object.clone(), "MatchClass")?;
vm, pattern_match_class_from_object_with_range(vm, source_file, object, range)
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,
},
})
} }
} }
@@ -405,6 +551,24 @@ impl Node for PatternMatchClassPatterns {
struct PatternMatchClassKeywordAttributes(Vec<ast::Identifier>); 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 { impl Node for PatternMatchClassKeywordAttributes {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
self.0.ast_to_object(vm, source_file) self.0.ast_to_object(vm, source_file)
@@ -435,6 +599,21 @@ impl Node for PatternMatchClassKeywordPatterns {
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchStar {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -457,17 +636,30 @@ impl Node for ast::PatternMatchStar {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchStar")?;
node_index: Default::default(), pattern_match_star_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchAs {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -493,33 +685,45 @@ impl Node for ast::PatternMatchAs {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchAs")?;
node_index: Default::default(), pattern_match_as_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
// constructor // 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 { impl Node for ast::PatternMatchOr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
node_index: _, node_index: _,
patterns, patterns,
range, range,
runtime_patterns,
} = self; } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type(vm, pyast::NodePatternMatchOr::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodePatternMatchOr::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) let patterns = runtime_patterns.map_or_else(
.unwrap(); || 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_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -528,15 +732,8 @@ impl Node for ast::PatternMatchOr {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "MatchOr")?;
node_index: Default::default(), pattern_match_or_from_object_with_range(vm, source_file, object, range)
patterns: Node::ast_from_object(
vm,
source_file,
get_node_field(vm, &object, "patterns", "MatchOr")?,
)?,
range: range_from_object(vm, source_file, object, "MatchOr")?,
})
} }
} }
+46 -58
View File
@@ -3,7 +3,6 @@ use crate::builtins::{PyGenericAlias, PyTuple, PyTupleRef, PyTypeRef, make_union
use crate::common::ascii; use crate::common::ascii;
use crate::convert::ToPyObject; use crate::convert::ToPyObject;
use crate::function::FuncArgs; use crate::function::FuncArgs;
use crate::types::Initializer;
macro_rules! impl_node { macro_rules! impl_node {
( (
@@ -61,6 +60,12 @@ macro_rules! impl_node {
macro_rules! impl_base_node { macro_rules! impl_base_node {
// Base node without fields/attributes (e.g. NodeMod, NodeExpr) // Base node without fields/attributes (e.g. NodeMod, NodeExpr)
($name:ident) => { ($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))] #[pyclass(flags(HAS_DICT, BASETYPE))]
impl $name { impl $name {
#[pymethod] #[pymethod]
@@ -83,9 +88,24 @@ macro_rules! impl_base_node {
(*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE);
} }
class.set_attr( class.set_attr(
identifier!(ctx, _attributes), identifier!(ctx, _fields),
ctx.empty_tuple.clone().into(), 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)] #[repr(transparent)]
pub(crate) struct NodeStmt(NodeAst); 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!( impl_node!(
#[pyclass(module = "_ast", name = "FunctionType", base = NodeMod)] #[pyclass(module = "_ast", name = "FunctionType", base = NodeMod)]
@@ -378,7 +402,11 @@ impl_node!(
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct NodeExpr(NodeAst); 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!( impl_node!(
#[pyclass(module = "_ast", name = "Continue", base = NodeStmt)] #[pyclass(module = "_ast", name = "Continue", base = NodeStmt)]
@@ -533,12 +561,11 @@ impl_node!(
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], 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)] #[pyclass(module = "_ast", name = "Constant", base = NodeExpr)]
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct NodeExprConstant(NodeExpr); pub(crate) struct NodeExprConstant(NodeExpr);
#[pyclass(flags(HAS_DICT, BASETYPE), with(Initializer))] #[pyclass(flags(HAS_DICT, BASETYPE))]
impl NodeExprConstant { impl NodeExprConstant {
#[extend_class] #[extend_class]
fn extend_class_with_fields(ctx: &Context, class: &'static Py<PyType>) { 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!( impl_node!(
#[pyclass(module = "_ast", name = "Attribute", base = NodeExpr)] #[pyclass(module = "_ast", name = "Attribute", base = NodeExpr)]
pub(crate) struct NodeExprAttribute, pub(crate) struct NodeExprAttribute,
@@ -841,7 +850,11 @@ impl_node!(
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct NodeExceptHandler(NodeAst); 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!( impl_node!(
#[pyclass(module = "_ast", name = "comprehension", base = NodeAst)] #[pyclass(module = "_ast", name = "comprehension", base = NodeAst)]
@@ -893,7 +906,10 @@ impl_node!(
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct NodePattern(NodeAst); pub(crate) struct NodePattern(NodeAst);
impl_base_node!(NodePattern); impl_base_node!(
NodePattern,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"]
);
impl_node!( impl_node!(
#[pyclass(module = "_ast", name = "match_case", base = NodeAst)] #[pyclass(module = "_ast", name = "match_case", base = NodeAst)]
@@ -967,7 +983,10 @@ impl_node!(
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct NodeTypeParam(NodeAst); pub(crate) struct NodeTypeParam(NodeAst);
impl_base_node!(NodeTypeParam); impl_base_node!(
NodeTypeParam,
attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"]
);
impl_node!( impl_node!(
#[pyclass(module = "_ast", name = "TypeIgnore", base = NodeTypeIgnore)] #[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 field_types_attr = vm.ctx.intern_str("_field_types");
let annotations_attr = vm.ctx.intern_str("__annotations__"); let annotations_attr = vm.ctx.intern_str("__annotations__");
let empty_dict: PyObjectRef = vm.ctx.new_dict().into();
for &(class_name, fields) in FIELD_TYPES { for &(class_name, fields) in FIELD_TYPES {
if fields.is_empty() { 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); 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>) { fn populate_singletons(vm: &VirtualMachine, module: &Py<PyModule>) {
+179 -97
View File
@@ -7,13 +7,10 @@ use super::{
#[pymodule] #[pymodule]
pub(crate) mod _ast { pub(crate) mod _ast {
use crate::{ use crate::{
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{ builtins::{PyDictRef, PySet, PyStr, PyTupleRef, PyType, PyTypeRef, PyUtf8Str},
PyDictRef, PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, PyUtf8StrRef,
},
class::{PyClassImpl, StaticType}, class::{PyClassImpl, StaticType},
common::wtf8::Wtf8, function::{ArgIterable, FuncArgs, KwArgs, PyMethodDef, PyMethodFlags},
function::{FuncArgs, KwArgs, PyMethodDef, PyMethodFlags},
stdlib::_ast::repr, stdlib::_ast::repr,
types::{Constructor, Initializer}, types::{Constructor, Initializer},
warn, warn,
@@ -117,10 +114,12 @@ pub(crate) mod _ast {
let fields = cls.get_attr(vm.ctx.intern_str("_fields")); let fields = cls.get_attr(vm.ctx.intern_str("_fields"));
if let Some(fields) = 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(); let mut positional: Vec<PyObjectRef> = Vec::new();
for field in fields { for i in 0..numfields {
if dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)?.is_some() { let field = fields.get_item(i as isize, vm)?;
if dict.get_item_opt(&*field, vm)?.is_some() {
positional.push(vm.ctx.none()); positional.push(vm.ctx.none());
} else { } else {
break; break;
@@ -136,6 +135,85 @@ pub(crate) mod _ast {
.new_tuple(vec![type_obj, vm.ctx.new_tuple(vec![]).into(), dict.into()])) .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 { pub(crate) fn ast_replace(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
if !args.args.is_empty() { if !args.args.is_empty() {
return Err(vm.new_type_error("__replace__() takes no positional arguments")); 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 attributes = cls.get_attr(vm.ctx.intern_str("_attributes"));
let dict = zelf.as_object().dict(); let dict = zelf.as_object().dict();
let mut expecting: std::collections::HashSet<String> = std::collections::HashSet::new(); let expecting = PySet::default().into_ref(&vm.ctx);
if let Some(fields) = fields.clone() { ast_replace_set_update(&expecting, fields.as_ref(), vm)?;
let fields: Vec<PyUtf8StrRef> = fields.try_to_value(vm)?; ast_replace_set_update(&expecting, attributes.as_ref(), 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());
}
}
for (key, _value) in &args.kwargs { 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!( return Err(vm.new_type_error(format!(
"{}.__replace__ got an unexpected keyword argument '{}'.", "{}.__replace__ got an unexpected keyword argument '{}'.",
cls.name(), cls.name(),
@@ -172,16 +241,9 @@ pub(crate) mod _ast {
if let Some(dict) = dict.as_ref() { if let Some(dict) = dict.as_ref() {
for (key, _value) in dict.items_vec() { for (key, _value) in dict.items_vec() {
if let Ok(key) = key.downcast::<PyUtf8Str>() { ast_replace_set_discard(&expecting, &key, vm)?;
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_difference_update(&expecting, attributes.as_ref(), vm)?;
} }
// Discard optional fields (T | None). // Discard optional fields (T | None).
@@ -189,20 +251,18 @@ pub(crate) mod _ast {
&& let Ok(field_types) = field_types.downcast::<crate::builtins::PyDict>() && let Ok(field_types) = field_types.downcast::<crate::builtins::PyDict>()
{ {
for (key, value) in field_types.items_vec() { 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) { 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 remaining = expecting.elements();
let mut names: Vec<String> = expecting if !remaining.is_empty() {
.into_iter() let mut names = Vec::with_capacity(remaining.len());
.map(|name| format!("{name:?}")) for name in &remaining {
.collect(); names.push(name.repr(vm)?.to_string());
}
names.sort(); names.sort();
let missing = names.join(", "); let missing = names.join(", ");
let count = names.len(); let count = names.len();
@@ -217,22 +277,8 @@ pub(crate) mod _ast {
let payload = vm.ctx.new_dict(); let payload = vm.ctx.new_dict();
if let Some(dict) = dict { if let Some(dict) = dict {
if let Some(fields) = fields { ast_replace_update_payload(&payload, fields.as_ref(), &dict, vm)?;
let fields: Vec<PyStrRef> = fields.try_to_value(vm)?; ast_replace_update_payload(&payload, attributes.as_ref(), &dict, 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)?;
}
}
}
} }
for (key, value) in args.kwargs { for (key, value) in args.kwargs {
payload.set_item(vm.ctx.intern_str(key), value, vm)?; 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> { 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() 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(); let n_args = args.args.len();
if n_args > fields.len() { if n_args > numfields {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"{} constructor takes at most {} positional argument{}", "{} constructor takes at most {} positional argument{}",
zelf.class().name(), zelf.class().name(),
fields.len(), numfields,
if fields.len() == 1 { "" } else { "s" }, if numfields == 1 { "" } else { "s" },
))); )));
} }
// Track which fields were set let mut attributes: Option<PyObjectRef> = None;
let mut set_fields = std::collections::HashSet::new();
let mut attributes: Option<Vec<PyStrRef>> = None;
for (name, arg) in fields.iter().zip(args.args) { for (i, arg) in args.args.into_iter().enumerate() {
zelf.set_attr(name, arg, vm)?; let name = fields_seq.get_item(i as isize, vm)?;
set_fields.insert(name.as_str().to_owned()); ast_set_attr(&zelf, &name, arg, vm)?;
ast_replace_set_discard(&remaining_fields, &name, vm)?;
} }
for (key, value) in args.kwargs { for (key, value) in args.kwargs {
if let Some(pos) = fields.iter().position(|f| f.as_bytes() == key.as_bytes()) let key_obj: PyObjectRef = vm.ctx.new_str(key.as_str()).into();
&& pos < n_args let contains = fields_seq.contains(&key_obj, vm)?;
{ if contains {
return Err(vm.new_type_error(format!( if !ast_replace_set_discard(&remaining_fields, &key_obj, vm)? {
"{} got multiple values for argument '{}'", return Err(vm.new_type_error(format!(
zelf.class().name(), "{} got multiple values for argument '{}'",
key zelf.class().name(),
))); key
} )));
}
if fields } else {
.iter() let attrs = if let Some(attributes) = &attributes {
.all(|field| field.as_bytes() != key.as_bytes()) attributes
{
let attrs = if let Some(attrs) = &attributes {
attrs
} else { } else {
let attrs = zelf let attrs = zelf
.class() .class()
.get_attr(vm.ctx.intern_str("_attributes")) .get_attr(vm.ctx.intern_str("_attributes"))
.and_then(|attr| attr.try_to_value::<Vec<PyStrRef>>(vm).ok()) .ok_or_else(|| {
.unwrap_or_default(); vm.new_attribute_error(format!(
"type object '{}' has no attribute '_attributes'",
zelf.class().name()
))
})?;
attributes = Some(attrs); attributes = Some(attrs);
attributes.as_ref().unwrap() 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!( let message = vm.ctx.new_str(format!(
"{}.__init__ got an unexpected keyword argument '{}'. \ "{}.__init__ got an unexpected keyword argument '{}'. \
Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.", 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)?; 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 = let expr_ctx_type: PyObjectRef =
super::super::pyast::NodeExprContext::make_static_type().into(); super::super::pyast::NodeExprContext::make_static_type().into();
for field in &fields { for field in remaining_fields.elements() {
if set_fields.contains(field.as_str()) { if let Some(ftype) = ft_dict.get_item_opt(&*field, vm)? {
continue;
}
if let Some(ftype) = ft_dict.get_item_opt::<Wtf8>(field.as_wtf8(), vm)? {
if ftype.fast_isinstance(vm.ctx.types.union_type) { if ftype.fast_isinstance(vm.ctx.types.union_type) {
// Optional field (T | None) — no default // Optional field (T | None) — no default
} else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) { } else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) {
// List field (list[T]) — default to [] // List field (list[T]) — default to []
let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into(); 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) { } else if ftype.is(&expr_ctx_type) {
// expr_context — default to Load() // expr_context — default to Load()
let load_type = let load_type =
@@ -445,13 +490,15 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
.unwrap_or_else(|| { .unwrap_or_else(|| {
vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict())) 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 { } else {
// Required field missing: emit DeprecationWarning. // Required field missing: emit DeprecationWarning.
let field_repr = field.repr(vm)?;
let message = vm.ctx.new_str(format!( 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(), zelf.class().name(),
field.as_wtf8() field_repr
)); ));
warn::warn( warn::warn(
message.into(), message.into(),
@@ -461,6 +508,21 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt
vm, 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"))?; .map_err(|_| vm.new_type_error("AST is not a type"))?;
let ctx = &vm.ctx; let ctx = &vm.ctx;
let empty_tuple = ctx.empty_tuple.clone(); 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("_fields", empty_tuple.clone(), ctx);
ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx); ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx);
ast_type.set_str_attr("__match_args__", empty_tuple, 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( const AST_REDUCE: PyMethodDef = PyMethodDef::new_const(
"__reduce__", "__reduce__",
+20 -16
View File
@@ -1,7 +1,8 @@
use crate::{ use crate::{
AsObject, PyObjectRef, PyResult, VirtualMachine, AsObject, PyObjectRef, PyResult, VirtualMachine,
builtins::{PyList, PyTuple}, builtins::{PyList, PyStr, PyTuple},
class::PyClassImpl, class::PyClassImpl,
recursion::ReprGuard,
stdlib::_ast::NodeAst, stdlib::_ast::NodeAst,
}; };
use rustpython_common::wtf8::Wtf8Buf; 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]); rendered.push_wtf8(&parts[0]);
} }
if items.len() > 2 { if items.len() > 2 {
if !parts[0].is_empty() { rendered.push_wtf8(", ...".as_ref());
rendered.push_wtf8(", ...".as_ref());
}
if parts.len() > 1 { if parts.len() > 1 {
rendered.push_wtf8(", ".as_ref()); rendered.push_wtf8(", ".as_ref());
rendered.push_wtf8(&parts[1]); 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]); rendered.push_wtf8(&parts[0]);
} }
if items.len() > 2 { if items.len() > 2 {
if !parts[0].is_empty() { rendered.push_wtf8(", ...".as_ref());
rendered.push_wtf8(", ...".as_ref());
}
if parts.len() > 1 { if parts.len() > 1 {
rendered.push_wtf8(", ".as_ref()); rendered.push_wtf8(", ".as_ref());
rendered.push_wtf8(&parts[1]); 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(", ".as_ref());
rendered.push_wtf8(&parts[1]); rendered.push_wtf8(&parts[1]);
} }
if items.len() == 1 {
rendered.push_wtf8(",".as_ref());
}
rendered.push_wtf8(")".as_ref()); rendered.push_wtf8(")".as_ref());
Ok(rendered) Ok(rendered)
} }
@@ -104,18 +98,24 @@ pub(crate) fn repr_ast_node(
s.push_wtf8("(...)".as_ref()); s.push_wtf8("(...)".as_ref());
return Ok(s); 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 cls.get_attr(vm.ctx.intern_str("_fields")) {
let fields = match fields { Some(fields) => fields,
Some(fields) => fields.try_to_value::<Vec<crate::builtins::PyStrRef>>(vm)?,
None => { None => {
let mut s = Wtf8Buf::from(&*cls.name()); let mut s = Wtf8Buf::from(&*cls.name());
s.push_wtf8("(...)".as_ref()); s.push_wtf8("(...)".as_ref());
return Ok(s); 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()); let mut s = Wtf8Buf::from(&*cls.name());
s.push_wtf8("()".as_ref()); s.push_wtf8("()".as_ref());
return Ok(s); return Ok(s);
@@ -124,8 +124,12 @@ pub(crate) fn repr_ast_node(
let mut rendered = Wtf8Buf::from(&*cls.name()); let mut rendered = Wtf8Buf::from(&*cls.name());
rendered.push_wtf8("(".as_ref()); rendered.push_wtf8("(".as_ref());
for (idx, field) in fields.iter().enumerate() { for idx in 0..numfields {
let value = obj.get_attr(field, vm)?; 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 value_repr = if value.fast_isinstance(vm.ctx.types.list_type) {
let list = value let list = value
.downcast::<PyList>() .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( fn ruff_fstring_element_to_joined_str_part(
vm: &VirtualMachine,
element: ast::InterpolatedStringElement, element: ast::InterpolatedStringElement,
) -> JoinedStrPart { ) -> JoinedStrPart {
match element { match element {
@@ -38,12 +39,21 @@ fn ruff_fstring_element_to_joined_str_part(
conversion, conversion,
format_spec, format_spec,
node_index: _, node_index: _,
}) => JoinedStrPart::FormattedValue(FormattedValue { runtime_str: _,
value: expression, runtime_interpolation_format_spec: _,
conversion, runtime_formatted_value_format_spec,
format_spec: ruff_format_spec_to_joined_str(format_spec), }) => {
range, 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( fn ruff_format_spec_to_joined_str(
vm: &VirtualMachine,
format_spec: Option<Box<ast::InterpolatedStringFormatSpec>>, format_spec: Option<Box<ast::InterpolatedStringFormatSpec>>,
) -> Option<Box<JoinedStr>> { ) -> Option<Box<JoinedStr>> {
match format_spec { match format_spec {
@@ -254,10 +265,14 @@ fn ruff_format_spec_to_joined_str(
range range
}; };
let values: Vec<_> = ruff_fstring_element_into_iter(elements) 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(); .collect();
let values = normalize_joined_str_parts(values).into_boxed_slice(); 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( fn format_spec_expr_to_ruff_format_spec(
joined_str: Option<Box<JoinedStr>>, format_spec: Option<Box<ast::Expr>>,
) -> Option<Box<ast::InterpolatedStringFormatSpec>> { ) -> Option<Box<ast::InterpolatedStringFormatSpec>> {
match joined_str { let format_spec = format_spec?;
None => None, let ast::Expr::FString(mut fstring) = *format_spec else {
Some(joined_str) => { return None;
let JoinedStr { range, values } = *joined_str; };
let elements: Vec<_> = Box::into_iter(values) let ast::ExprFString {
.map(joined_str_part_to_ruff_fstring_element) range,
.collect(); ref mut value,
let format_spec = ast::InterpolatedStringFormatSpec { node_index: _,
node_index: Default::default(), 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, range,
elements: elements.into(), value,
}; node_index: _,
Some(Box::new(format_spec)) 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)] #[derive(Debug)]
pub(super) struct JoinedStr { pub(super) struct JoinedStr {
pub(super) range: TextRange, pub(super) range: TextRange,
pub(super) values: Box<[JoinedStrPart]>, pub(super) values: Box<[JoinedStrPart]>,
pub(super) runtime_values: Option<Vec<Option<ast::Expr>>>,
} }
impl JoinedStr { impl JoinedStr {
pub(super) fn into_expr(self) -> ast::Expr { pub(super) fn into_expr(self, from_ast_object: bool) -> ast::Expr {
let Self { range, values } = self; 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 { ast::Expr::FString(ast::ExprFString {
node_index: Default::default(), node_index: Default::default(),
range: Default::default(), range,
value: match values.len() { value: match values.len() {
// ruff represents an empty fstring like this:
0 => ast::FStringValue::single(ast::FString { 0 => ast::FStringValue::single(ast::FString {
node_index: Default::default(), node_index: Default::default(),
range, range,
@@ -332,7 +405,8 @@ impl JoinedStr {
}), }),
1 => ast::FStringValue::single( 1 => ast::FStringValue::single(
Box::<[_]>::into_iter(values) 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 { .map(|element| ast::FString {
node_index: Default::default(), node_index: Default::default(),
range, range,
@@ -344,54 +418,108 @@ impl JoinedStr {
), ),
_ => ast::FStringValue::concatenated( _ => ast::FStringValue::concatenated(
Box::<[_]>::into_iter(values) 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) .map(ruff_fstring_element_to_ruff_fstring_part)
.collect(), .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 { match part {
JoinedStrPart::FormattedValue(value) => { JoinedStrPart::FormattedValue(value) => {
ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { let format_spec = value.format_spec.clone();
node_index: Default::default(), let runtime_formatted_value_format_spec = (from_ast_object && format_spec.is_some())
range: value.range, .then_some(format_spec.clone())
expression: value.value.clone(), .flatten();
debug_text: None, // TODO: What is this? Some(ast::InterpolatedStringElement::Interpolation(
conversion: value.conversion, ast::InterpolatedElement {
format_spec: joined_str_to_ruff_format_spec(value.format_spec), 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) => { JoinedStrPart::Constant(value) => {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { let Constant { range, value, .. } = value;
node_index: Default::default(), let ConstantLiteral::Str { value, .. } = value else {
range: value.range, return None;
value: match value.value { };
ConstantLiteral::Str { value, .. } => value, Some(ast::InterpolatedStringElement::Literal(
_ => todo!(), ast::InterpolatedStringLiteralElement {
node_index: Default::default(),
range,
value,
}, },
}) ))
} }
} }
} }
// constructor // 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 { impl Node for JoinedStr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { 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 let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprJoinedStr::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeExprJoinedStr::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item( let values = if let Some(runtime_values) = runtime_values {
"values", BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file)
BoxedSlice(values).ast_to_object(vm, source_file), } else {
vm, BoxedSlice(values).ast_to_object(vm, source_file)
) };
.unwrap(); dict.set_item("values", values, vm).unwrap();
node_add_location(&dict, range, vm, source_file); node_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -400,15 +528,8 @@ impl Node for JoinedStr {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let values: BoxedSlice<_> = Node::ast_from_object( let range = range_from_object(vm, source_file, object.clone(), "JoinedStr")?;
vm, joined_str_from_object_with_range(vm, source_file, object, range)
source_file,
get_node_field(vm, &object, "values", "JoinedStr")?,
)?;
Ok(Self {
values: values.0,
range: range_from_object(vm, source_file, object, "JoinedStr")?,
})
} }
} }
@@ -431,8 +552,7 @@ impl Node for JoinedStrPart {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); if is_node_instance(vm, &object, pyast::NodeExprFormattedValue::static_type())? {
if cls.is(pyast::NodeExprFormattedValue::static_type()) {
Ok(Self::FormattedValue(Node::ast_from_object( Ok(Self::FormattedValue(Node::ast_from_object(
vm, vm,
source_file, source_file,
@@ -452,11 +572,31 @@ impl Node for JoinedStrPart {
pub(super) struct FormattedValue { pub(super) struct FormattedValue {
value: Box<ast::Expr>, value: Box<ast::Expr>,
conversion: ast::ConversionFlag, conversion: ast::ConversionFlag,
format_spec: Option<Box<JoinedStr>>, format_spec: Option<Box<ast::Expr>>,
range: TextRange, range: TextRange,
} }
// constructor // 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 { impl Node for FormattedValue {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -487,25 +627,24 @@ impl Node for FormattedValue {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = range_from_object(vm, source_file, object.clone(), "FormattedValue")?;
value: Node::ast_from_object( formatted_value_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
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( pub(super) fn fstring_to_object(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
@@ -515,7 +654,27 @@ pub(super) fn fstring_to_object(
range, range,
mut value, mut value,
node_index: _, node_index: _,
runtime_joined_str,
runtime_values,
} = expression; } = 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 { let default_part = ast::FStringPart::FString(ast::FString {
node_index: Default::default(), node_index: Default::default(),
range: Default::default(), range: Default::default(),
@@ -545,7 +704,7 @@ pub(super) fn fstring_to_object(
node_index: _, node_index: _,
}) => { }) => {
for element in ruff_fstring_element_into_iter(elements) { 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 if let JoinedStrPart::FormattedValue(value) = part
&& let Some(format_spec) = &value.format_spec && 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 { let c = JoinedStr {
range, range,
values: values.into_boxed_slice(), values: values.into_boxed_slice(),
runtime_values: None,
}; };
c.ast_to_object(vm, source_file) c.ast_to_object(vm, source_file)
} }
@@ -568,8 +728,9 @@ pub(super) fn fstring_to_object(
// ===== TString (Template String) Support ===== // ===== TString (Template String) Support =====
fn ruff_tstring_element_to_template_str_part( fn ruff_tstring_element_to_template_str_part(
element: ast::InterpolatedStringElement, vm: &VirtualMachine,
source_file: &SourceFile, source_file: &SourceFile,
element: ast::InterpolatedStringElement,
) -> TemplateStrPart { ) -> TemplateStrPart {
match element { match element {
ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement {
@@ -588,6 +749,9 @@ fn ruff_tstring_element_to_template_str_part(
conversion, conversion,
format_spec, format_spec,
node_index: _, node_index: _,
runtime_str,
runtime_interpolation_format_spec,
runtime_formatted_value_format_spec: _,
}) => { }) => {
let expr_range = let expr_range =
extend_expr_range_with_wrapping_parens(source_file, range, expression.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 { } else {
tstring_interpolation_expr_str(source_file, range, expr_range) 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 { TemplateStrPart::Interpolation(TStringInterpolation {
value: expression, value: expression,
str: expr_str, str: runtime_interpolation
.as_ref()
.map_or_else(|| vm.ctx.new_str(expr_str).into(), |(str, _)| str.clone()),
conversion, 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, range,
}) })
} }
@@ -695,34 +871,51 @@ fn strip_interpolation_expr(expr_source: &str) -> String {
pub(super) struct TemplateStr { pub(super) struct TemplateStr {
pub(super) range: TextRange, pub(super) range: TextRange,
pub(super) values: Box<[TemplateStrPart]>, pub(super) values: Box<[TemplateStrPart]>,
pub(super) runtime_values: Option<Vec<Option<ast::Expr>>>,
} }
pub(super) fn template_str_to_expr( pub(super) fn template_str_to_expr(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile,
template: TemplateStr, template: TemplateStr,
) -> PyResult<ast::Expr> { ) -> PyResult<ast::Expr> {
let TemplateStr { range, values } = template; let TemplateStr {
let elements = template_parts_to_elements(vm, values)?; range,
values,
runtime_values: raw_runtime_values,
} = template;
let elements = template_parts_to_elements(vm, source_file, values)?;
let tstring = ast::TString { let tstring = ast::TString {
range, range,
node_index: Default::default(), node_index: Default::default(),
elements, elements,
flags: ast::TStringFlags::empty(), 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 { Ok(ast::Expr::TString(ast::ExprTString {
node_index: Default::default(), node_index: Default::default(),
range, range,
value: ast::TStringValue::single(tstring), value: ast::TStringValue::single(tstring),
runtime_template_str,
runtime_values,
})) }))
} }
pub(super) fn interpolation_to_expr( pub(super) fn interpolation_to_expr(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile,
interpolation: TStringInterpolation, interpolation: TStringInterpolation,
) -> PyResult<ast::Expr> { ) -> PyResult<ast::Expr> {
let range = interpolation.range;
let part = TemplateStrPart::Interpolation(interpolation); let part = TemplateStrPart::Interpolation(interpolation);
let elements = template_parts_to_elements(vm, vec![part].into_boxed_slice())?; let elements = template_parts_to_elements(vm, source_file, vec![part].into_boxed_slice())?;
let range = TextRange::default();
let tstring = ast::TString { let tstring = ast::TString {
range, range,
node_index: Default::default(), node_index: Default::default(),
@@ -733,22 +926,26 @@ pub(super) fn interpolation_to_expr(
node_index: Default::default(), node_index: Default::default(),
range, range,
value: ast::TStringValue::single(tstring), value: ast::TStringValue::single(tstring),
runtime_template_str: None,
runtime_values: None,
})) }))
} }
fn template_parts_to_elements( fn template_parts_to_elements(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile,
values: Box<[TemplateStrPart]>, values: Box<[TemplateStrPart]>,
) -> PyResult<ast::InterpolatedStringElements> { ) -> PyResult<ast::InterpolatedStringElements> {
let mut elements = Vec::with_capacity(values.len()); let mut elements = Vec::with_capacity(values.len());
for value in values.into_vec() { 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)) Ok(ast::InterpolatedStringElements::from(elements))
} }
fn template_part_to_element( fn template_part_to_element(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile,
part: TemplateStrPart, part: TemplateStrPart,
) -> PyResult<ast::InterpolatedStringElement> { ) -> PyResult<ast::InterpolatedStringElement> {
match part { match part {
@@ -767,12 +964,18 @@ fn template_part_to_element(
TemplateStrPart::Interpolation(interpolation) => { TemplateStrPart::Interpolation(interpolation) => {
let TStringInterpolation { let TStringInterpolation {
value, value,
str,
conversion, conversion,
format_spec, format_spec,
range, range,
..
} = interpolation; } = 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( Ok(ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement { ast::InterpolatedElement {
range, range,
@@ -781,6 +984,9 @@ fn template_part_to_element(
debug_text: None, debug_text: None,
conversion, conversion,
format_spec, format_spec,
runtime_str,
runtime_interpolation_format_spec,
runtime_formatted_value_format_spec: None,
}, },
)) ))
} }
@@ -788,19 +994,38 @@ fn template_part_to_element(
} }
// constructor // 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 { impl Node for TemplateStr {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { 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 let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprTemplateStr::static_type().to_owned()) .into_ref_with_type(vm, pyast::NodeExprTemplateStr::static_type().to_owned())
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item( let values = if let Some(runtime_values) = runtime_values {
"values", BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file)
BoxedSlice(values).ast_to_object(vm, source_file), } else {
vm, BoxedSlice(values).ast_to_object(vm, source_file)
) };
.unwrap(); dict.set_item("values", values, vm).unwrap();
node_add_location(&dict, range, vm, source_file); node_add_location(&dict, range, vm, source_file);
node.into() node.into()
} }
@@ -809,15 +1034,8 @@ impl Node for TemplateStr {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let values: BoxedSlice<_> = Node::ast_from_object( let range = range_from_object(vm, source_file, object.clone(), "TemplateStr")?;
vm, template_str_from_object_with_range(vm, source_file, object, range)
source_file,
get_node_field(vm, &object, "values", "TemplateStr")?,
)?;
Ok(Self {
values: values.0,
range: range_from_object(vm, source_file, object, "TemplateStr")?,
})
} }
} }
@@ -840,8 +1058,7 @@ impl Node for TemplateStrPart {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); if is_node_instance(vm, &object, pyast::NodeExprInterpolation::static_type())? {
if cls.is(pyast::NodeExprInterpolation::static_type()) {
Ok(Self::Interpolation(Node::ast_from_object( Ok(Self::Interpolation(Node::ast_from_object(
vm, vm,
source_file, source_file,
@@ -860,13 +1077,38 @@ impl Node for TemplateStrPart {
#[derive(Debug)] #[derive(Debug)]
pub(super) struct TStringInterpolation { pub(super) struct TStringInterpolation {
value: Box<ast::Expr>, value: Box<ast::Expr>,
str: String, str: PyObjectRef,
conversion: ast::ConversionFlag, conversion: ast::ConversionFlag,
format_spec: Option<Box<JoinedStr>>, format_spec: Option<Box<ast::Expr>>,
range: TextRange, range: TextRange,
} }
// constructor // 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 { impl Node for TStringInterpolation {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -882,8 +1124,7 @@ impl Node for TStringInterpolation {
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().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(); .unwrap();
dict.set_item("str", vm.ctx.new_str(str).into(), vm) dict.set_item("str", str, vm).unwrap();
.unwrap();
dict.set_item("conversion", conversion.ast_to_object(vm, source_file), vm) dict.set_item("conversion", conversion.ast_to_object(vm, source_file), vm)
.unwrap(); .unwrap();
dict.set_item( dict.set_item(
@@ -900,25 +1141,8 @@ impl Node for TStringInterpolation {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let str_obj = get_node_field(vm, &object, "str", "Interpolation")?; let range = range_from_object(vm, source_file, object.clone(), "Interpolation")?;
let str_val: String = str_obj.try_into_value(vm)?; tstring_interpolation_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
@@ -931,7 +1155,42 @@ pub(super) fn tstring_to_object(
range, range,
mut value, mut value,
node_index: _, node_index: _,
runtime_template_str,
runtime_values,
} = expression; } = 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 { let default_tstring = ast::TString {
node_index: Default::default(), node_index: Default::default(),
range: 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()); let tstring = core::mem::replace(value.iter_mut().nth(i).unwrap(), default_tstring.clone());
for element in ruff_fstring_element_into_iter(tstring.elements) { for element in ruff_fstring_element_into_iter(tstring.elements) {
values.push(ruff_tstring_element_to_template_str_part( values.push(ruff_tstring_element_to_template_str_part(
element, vm,
source_file, source_file,
element,
)); ));
} }
} }
@@ -952,6 +1212,37 @@ pub(super) fn tstring_to_object(
let c = TemplateStr { let c = TemplateStr {
range, range,
values: values.into_boxed_slice(), values: values.into_boxed_slice(),
runtime_values: None,
}; };
c.ast_to_object(vm, source_file) 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; use rustpython_compiler_core::SourceFile;
pub(super) enum TypeIgnore { pub(super) enum TypeIgnore {
None,
TypeIgnore(TypeIgnoreTypeIgnore), TypeIgnore(TypeIgnoreTypeIgnore),
} }
@@ -9,6 +10,7 @@ pub(super) enum TypeIgnore {
impl Node for TypeIgnore { impl Node for TypeIgnore {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
match self { match self {
Self::None => vm.ctx.none(),
Self::TypeIgnore(cons) => cons.ast_to_object(vm, source_file), Self::TypeIgnore(cons) => cons.ast_to_object(vm, source_file),
} }
} }
@@ -17,8 +19,9 @@ impl Node for TypeIgnore {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); Ok(if vm.is_none(&object) {
Ok(if cls.is(pyast::NodeTypeIgnoreTypeIgnore::static_type()) { Self::None
} else if is_node_instance(vm, &object, pyast::NodeTypeIgnoreTypeIgnore::static_type())? {
Self::TypeIgnore(TypeIgnoreTypeIgnore::ast_from_object( Self::TypeIgnore(TypeIgnoreTypeIgnore::ast_from_object(
vm, vm,
source_file, source_file,
@@ -34,15 +37,14 @@ impl Node for TypeIgnore {
} }
pub(super) struct TypeIgnoreTypeIgnore { pub(super) struct TypeIgnoreTypeIgnore {
range: TextRange, lineno: i32,
lineno: PyRefExact<PyInt>, tag: PyObjectRef,
tag: PyRefExact<PyStr>,
} }
// constructor // constructor
impl Node for TypeIgnoreTypeIgnore { impl Node for TypeIgnoreTypeIgnore {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, _source_file: &SourceFile) -> PyObjectRef {
let Self { lineno, tag, range } = self; let Self { lineno, tag } = self;
let node = NodeAst let node = NodeAst
.into_ref_with_type( .into_ref_with_type(
vm, vm,
@@ -50,25 +52,20 @@ impl Node for TypeIgnoreTypeIgnore {
) )
.unwrap(); .unwrap();
let dict = node.as_object().dict().unwrap(); let dict = node.as_object().dict().unwrap();
dict.set_item("lineno", lineno.to_pyobject(vm), vm).unwrap(); dict.set_item("lineno", vm.ctx.new_int(lineno).into(), vm)
dict.set_item("tag", tag.to_pyobject(vm), vm).unwrap(); .unwrap();
node_add_location(&dict, range, vm, source_file); dict.set_item("tag", tag, vm).unwrap();
node.into() node.into()
} }
fn ast_from_object( fn ast_from_object(
vm: &VirtualMachine, vm: &VirtualMachine,
source_file: &SourceFile, _source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { Ok(Self {
lineno: get_node_field(vm, &object, "lineno", "TypeIgnore")? lineno: get_int_field(vm, &object, "lineno", "TypeIgnore")?,
.downcast_exact(vm) tag: node_object_to_ast_string(vm, get_node_field(vm, &object, "tag", "TypeIgnore")?)?,
.unwrap(),
tag: get_node_field(vm, &object, "tag", "TypeIgnore")?
.downcast_exact(vm)
.unwrap(),
range: range_from_object(vm, source_file, object, "TypeIgnore")?,
}) })
} }
} }
+135 -75
View File
@@ -3,7 +3,10 @@ use rustpython_compiler_core::SourceFile;
impl Node for ast::TypeParams { impl Node for ast::TypeParams {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { 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( fn ast_from_object(
@@ -11,22 +14,51 @@ impl Node for ast::TypeParams {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let type_params: Vec<ast::TypeParam> = Node::ast_from_object(vm, source_file, object)?; Ok(type_params_from_values(
let range = Option::zip(type_params.first(), type_params.last()) vm,
.map(|(first, last)| first.range().cover(last.range())) Node::ast_from_object(vm, source_file, object)?,
.unwrap_or_default(); ))
Ok(Self {
node_index: Default::default(),
type_params,
range,
})
} }
fn is_none(&self) -> bool { 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 // sum
impl Node for ast::TypeParam { impl Node for ast::TypeParam {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
@@ -42,35 +74,70 @@ impl Node for ast::TypeParam {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
let cls = object.class(); if vm.is_none(&object) {
Ok(if cls.is(pyast::NodeTypeParamTypeVar::static_type()) { return Err(vm.new_type_error(format!(
Self::TypeVar(ast::TypeParamTypeVar::ast_from_object( "expected some sort of type_param, but got {}",
vm, object.repr(vm)?
source_file, )));
object, }
)?) enum TypeParamKind {
} else if cls.is(pyast::NodeTypeParamParamSpec::static_type()) { TypeVar,
Self::ParamSpec(ast::TypeParamParamSpec::ast_from_object( ParamSpec,
vm, TypeVarTuple,
source_file, }
object, let kind = if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVar::static_type())? {
)?) TypeParamKind::TypeVar
} else if cls.is(pyast::NodeTypeParamTypeVarTuple::static_type()) { } else if is_node_instance(vm, &object, pyast::NodeTypeParamParamSpec::static_type())? {
Self::TypeVarTuple(ast::TypeParamTypeVarTuple::ast_from_object( TypeParamKind::ParamSpec
vm, } else if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVarTuple::static_type())? {
source_file, TypeParamKind::TypeVarTuple
object,
)?)
} else { } else {
return Err(vm.new_type_error(format!( return Err(vm.new_type_error(format!(
"expected some sort of type_param, but got {}", "expected some sort of type_param, but got {}",
object.repr(vm)? 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 // 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 { impl Node for ast::TypeParamTypeVar {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -99,27 +166,28 @@ impl Node for ast::TypeParamTypeVar {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = type_param_range_from_object(vm, source_file, object.clone())?;
node_index: Default::default(), type_var_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
// constructor // 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 { impl Node for ast::TypeParamParamSpec {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -145,24 +213,28 @@ impl Node for ast::TypeParamParamSpec {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = type_param_range_from_object(vm, source_file, object.clone())?;
node_index: Default::default(), param_spec_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
// constructor // 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 { impl Node for ast::TypeParamTypeVarTuple {
fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef {
let Self { let Self {
@@ -191,19 +263,7 @@ impl Node for ast::TypeParamTypeVarTuple {
source_file: &SourceFile, source_file: &SourceFile,
object: PyObjectRef, object: PyObjectRef,
) -> PyResult<Self> { ) -> PyResult<Self> {
Ok(Self { let range = type_param_range_from_object(vm, source_file, object.clone())?;
node_index: Default::default(), type_var_tuple_from_object_with_range(vm, source_file, object, range)
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")?,
})
} }
} }
+378 -67
View File
@@ -1,8 +1,10 @@
// spell-checker: ignore assignlist ifexp // spell-checker: ignore assignlist ifexp
use super::module::Mod; use super::module::Mod;
use crate::{PyResult, VirtualMachine}; use crate::{PyResult, VirtualMachine, compiler::CompileError};
use ruff_python_ast as ast; 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 { fn expr_context_name(ctx: ast::ExprContext) -> &'static str {
match ctx { 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<()> { fn validate_name(vm: &VirtualMachine, name: &ast::name::Name) -> PyResult<()> {
match name.as_str() { match name.as_str() {
"None" | "True" | "False" => Err(vm.new_value_error(format!( "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 { for comp in gens {
validate_expr(vm, &comp.target, ast::ExprContext::Store)?; validate_expr(vm, &comp.target, ast::ExprContext::Store)?;
validate_expr(vm, &comp.iter, ast::ExprContext::Load)?; 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)?; validate_exprs(vm, &comp.ifs, ast::ExprContext::Load, false)?;
} }
Ok(()) Ok(())
@@ -42,30 +56,49 @@ fn validate_keywords(vm: &VirtualMachine, keywords: &[ast::Keyword]) -> PyResult
Ok(()) 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<()> { fn validate_parameters(vm: &VirtualMachine, params: &ast::Parameters) -> PyResult<()> {
for param in params for param in params.posonlyargs.iter().chain(&params.args) {
.posonlyargs validate_parameter_annotation(vm, &param.parameter)?;
.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)?;
}
} }
if let Some(vararg) = &params.vararg if let Some(vararg) = &params.vararg
&& let Some(annotation) = &vararg.annotation && let Some(annotation) = &vararg.annotation
{ {
validate_expr(vm, annotation, ast::ExprContext::Load)?; validate_expr(vm, annotation, ast::ExprContext::Load)?;
} }
for param in &params.kwonlyargs {
validate_parameter_annotation(vm, &param.parameter)?;
}
if let Some(kwarg) = &params.kwarg if let Some(kwarg) = &params.kwarg
&& let Some(annotation) = &kwarg.annotation && let Some(annotation) = &kwarg.annotation
{ {
validate_expr(vm, annotation, ast::ExprContext::Load)?; 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(()) Ok(())
} }
@@ -99,8 +132,14 @@ fn validate_assignlist(
validate_exprs(vm, targets, ctx, false) 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_nonempty_seq(vm, body.len(), "body", owner)?;
validate_runtime_stmt_list_slots(vm, metadata)?;
validate_stmts(vm, body) validate_stmts(vm, body)
} }
@@ -111,34 +150,83 @@ fn validate_interpolated_elements<'a>(
for element in elements { for element in elements {
if let ast::InterpolatedStringElementRef::Interpolation(interpolation) = element { if let ast::InterpolatedStringElementRef::Interpolation(interpolation) = element {
validate_expr(vm, &interpolation.expression, ast::ExprContext::Load)?; validate_expr(vm, &interpolation.expression, ast::ExprContext::Load)?;
if let Some(format_spec) = &interpolation.format_spec { if let Some(format_spec) = interpolation.runtime_formatted_value_format_spec.as_deref()
for spec_element in &format_spec.elements { {
if let ast::InterpolatedStringElement::Interpolation(spec_interp) = spec_element validate_expr(vm, format_spec, ast::ExprContext::Load)?;
{ } else if let Some(format_spec) =
validate_expr(vm, &spec_interp.expression, ast::ExprContext::Load)?; 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(()) 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<()> { fn validate_pattern_match_value(vm: &VirtualMachine, expr: &ast::Expr) -> PyResult<()> {
validate_expr(vm, expr, ast::ExprContext::Load)?; 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 { match expr {
ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => { ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => {
Ok(()) Ok(())
} }
ast::Expr::Attribute(_) => Ok(()), ast::Expr::Attribute(_) => Ok(()),
ast::Expr::UnaryOp(op) => match &*op.operand { ast::Expr::UnaryOp(_) if ensure_literal_negative(expr, true, true) => Ok(()),
ast::Expr::NumberLiteral(_) => Ok(()), ast::Expr::BinOp(_) if ensure_literal_complex(expr) => 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::FString(_) | ast::Expr::TString(_) => Ok(()), ast::Expr::FString(_) | ast::Expr::TString(_) => Ok(()),
ast::Expr::BooleanLiteral(_) ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_) | 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::Pattern::MatchSingleton(singleton) => match singleton.value {
ast::Singleton::None | ast::Singleton::True | ast::Singleton::False => Ok(()), 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) => { ast::Pattern::MatchMapping(mapping) => {
if mapping.keys.len() != mapping.patterns.len() { if mapping.keys.len() != mapping.patterns.len() {
return Err(vm.new_value_error( 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 { if let Some(rest) = &mapping.rest {
validate_capture(vm, rest)?; validate_capture(vm, rest)?;
} }
validate_runtime_expr_option_list_slots(vm, mapping.runtime_keys.as_ref())?;
for key in &mapping.keys { 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; continue;
} }
validate_pattern_match_value(vm, key)?; validate_pattern_match_value(vm, key)?;
} }
validate_runtime_pattern_list_slots(vm, mapping.runtime_patterns.as_ref())?;
validate_patterns(vm, &mapping.patterns, false) validate_patterns(vm, &mapping.patterns, false)
} }
ast::Pattern::MatchClass(match_class) => { 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)?; validate_expr(vm, &match_class.cls, ast::ExprContext::Load)?;
let mut cls = match_class.cls.as_ref(); let mut cls = match_class.cls.as_ref();
loop { loop {
@@ -199,7 +309,13 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool)
for keyword in &match_class.arguments.keywords { for keyword in &match_class.arguments.keywords {
validate_name(vm, keyword.attr.id())?; 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)?; 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 { for keyword in &match_class.arguments.keywords {
validate_pattern(vm, &keyword.pattern, false)?; 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 { if match_or.patterns.len() < 2 {
return Err(vm.new_value_error("MatchOr requires at least 2 patterns")); 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) 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( fn validate_patterns(
vm: &VirtualMachine, vm: &VirtualMachine,
patterns: &[ast::Pattern], patterns: &[ast::Pattern],
@@ -282,6 +467,12 @@ fn validate_type_params(
type_params: Option<&ast::TypeParams>, type_params: Option<&ast::TypeParams>,
) -> PyResult<()> { ) -> PyResult<()> {
if let Some(type_params) = type_params { 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 { for tp in &type_params.type_params {
validate_typeparam(vm, tp)?; validate_typeparam(vm, tp)?;
} }
@@ -337,6 +528,11 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
if op.values.len() < 2 { if op.values.len() < 2 {
return Err(vm.new_value_error("BoolOp with less than 2 values")); 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) validate_exprs(vm, &op.values, ast::ExprContext::Load, false)
} }
ast::Expr::Named(named) => { 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) validate_expr(vm, &ifexp.orelse, ast::ExprContext::Load)
} }
ast::Expr::Dict(dict) => { ast::Expr::Dict(dict) => {
validate_runtime_expr_list_slots(
vm,
dict.runtime_values.as_ref(),
ast::ExprContext::Load,
)?;
for item in &dict.items { for item in &dict.items {
if let Some(key) = &item.key { if let Some(key) = &item.key {
validate_expr(vm, key, ast::ExprContext::Load)?; validate_expr(vm, key, ast::ExprContext::Load)?;
@@ -370,7 +571,14 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
} }
Ok(()) 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) => { ast::Expr::ListComp(list) => {
validate_comprehension(vm, &list.generators)?; validate_comprehension(vm, &list.generators)?;
validate_expr(vm, &list.elt, ast::ExprContext::Load) 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", "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_exprs(vm, &compare.comparators, ast::ExprContext::Load, false)?;
validate_expr(vm, &compare.left, ast::ExprContext::Load) validate_expr(vm, &compare.left, ast::ExprContext::Load)
} }
ast::Expr::Call(call) => { ast::Expr::Call(call) => {
validate_expr(vm, &call.func, ast::ExprContext::Load)?; 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_exprs(vm, &call.arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &call.arguments.keywords) validate_keywords(vm, &call.arguments.keywords)
} }
ast::Expr::FString(fstring) => validate_interpolated_elements( ast::Expr::FString(fstring) => {
vm, validate_runtime_expr_list_slots(
fstring vm,
.value fstring.runtime_values.as_ref(),
.elements() ast::ExprContext::Load,
.map(ast::InterpolatedStringElementRef::from), )?;
), if let Some(joined_str) = fstring.runtime_joined_str.as_ref() {
ast::Expr::TString(tstring) => validate_interpolated_elements( validate_exprs(vm, joined_str, ast::ExprContext::Load, false)
vm, } else {
tstring validate_interpolated_elements(
.value vm,
.elements() fstring
.map(ast::InterpolatedStringElementRef::from), .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::StringLiteral(_)
| ast::Expr::BytesLiteral(_) | ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_) | ast::Expr::NumberLiteral(_)
| ast::Expr::Constant(_)
| ast::Expr::BooleanLiteral(_) | ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_) | 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::Attribute(attr) => validate_expr(vm, &attr.value, ast::ExprContext::Load),
ast::Expr::Subscript(sub) => { ast::Expr::Subscript(sub) => {
validate_expr(vm, &sub.slice, ast::ExprContext::Load)?; 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::Starred(star) => validate_expr(vm, &star.value, ctx),
ast::Expr::Name(_) => Ok(()), ast::Expr::Name(_) => Ok(()),
ast::Expr::List(list) => validate_exprs(vm, &list.elts, ctx, false), ast::Expr::List(list) => {
ast::Expr::Tuple(tuple) => validate_exprs(vm, &tuple.elts, ctx, false), 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) => { ast::Expr::Slice(slice) => {
if let Some(lower) = &slice.lower { if let Some(lower) = &slice.lower {
validate_expr(vm, lower, ast::ExprContext::Load)?; validate_expr(vm, lower, ast::ExprContext::Load)?;
@@ -458,7 +711,7 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
} }
Ok(()) 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 { } else {
"FunctionDef" "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_type_params(vm, func.type_params.as_deref())?;
validate_parameters(vm, &func.parameters)?; 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)?; validate_decorators(vm, &func.decorator_list)?;
if let Some(returns) = &func.returns { if let Some(returns) = &func.returns {
validate_expr(vm, returns, ast::ExprContext::Load)?; validate_expr(vm, returns, ast::ExprContext::Load)?;
@@ -487,12 +745,27 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(()) Ok(())
} }
ast::Stmt::ClassDef(class_def) => { 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())?; validate_type_params(vm, class_def.type_params.as_deref())?;
if let Some(arguments) = &class_def.arguments { 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_exprs(vm, &arguments.args, ast::ExprContext::Load, false)?;
validate_keywords(vm, &arguments.keywords)?; 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) validate_decorators(vm, &class_def.decorator_list)
} }
ast::Stmt::Return(ret) => { ast::Stmt::Return(ret) => {
@@ -501,8 +774,20 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
} }
Ok(()) 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) => { 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_assignlist(vm, &assign.targets, ast::ExprContext::Store)?;
validate_expr(vm, &assign.value, ast::ExprContext::Load) 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" }; let owner = if for_stmt.is_async { "AsyncFor" } else { "For" };
validate_expr(vm, &for_stmt.target, ast::ExprContext::Store)?; validate_expr(vm, &for_stmt.target, ast::ExprContext::Store)?;
validate_expr(vm, &for_stmt.iter, ast::ExprContext::Load)?; 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) validate_stmts(vm, &for_stmt.orelse)
} }
ast::Stmt::While(while_stmt) => { ast::Stmt::While(while_stmt) => {
validate_expr(vm, &while_stmt.test, ast::ExprContext::Load)?; 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) validate_stmts(vm, &while_stmt.orelse)
} }
ast::Stmt::If(if_stmt) => { ast::Stmt::If(if_stmt) => {
validate_expr(vm, &if_stmt.test, ast::ExprContext::Load)?; 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 { for clause in &if_stmt.elif_else_clauses {
if let Some(test) = &clause.test { if let Some(test) = &clause.test {
validate_expr(vm, test, ast::ExprContext::Load)?; 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(()) Ok(())
} }
@@ -564,7 +857,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
validate_expr(vm, optional_vars, ast::ExprContext::Store)?; 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) => { ast::Stmt::Match(match_stmt) => {
validate_expr(vm, &match_stmt.subject, ast::ExprContext::Load)?; 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 { if let Some(guard) = &case.guard {
validate_expr(vm, guard, ast::ExprContext::Load)?; 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(()) Ok(())
} }
@@ -591,7 +884,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
} }
ast::Stmt::Try(try_stmt) => { ast::Stmt::Try(try_stmt) => {
let owner = if try_stmt.is_star { "TryStar" } else { "Try" }; 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() { if try_stmt.handlers.is_empty() && try_stmt.finalbody.is_empty() {
return Err(vm.new_value_error(format!( return Err(vm.new_value_error(format!(
"{owner} has neither except handlers nor finalbody" "{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")) 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 { for handler in &try_stmt.handlers {
let ast::ExceptHandler::ExceptHandler(handler) = handler; let ast::ExceptHandler::ExceptHandler(handler) = handler;
if let Some(type_expr) = &handler.type_ { if let Some(type_expr) = &handler.type_ {
validate_expr(vm, type_expr, ast::ExprContext::Load)?; 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_stmts(vm, &try_stmt.finalbody)?;
validate_runtime_stmt_list_slots(vm, try_stmt.runtime_orelse.as_ref())?;
validate_stmts(vm, &try_stmt.orelse) validate_stmts(vm, &try_stmt.orelse)
} }
ast::Stmt::Assert(assert_stmt) => { ast::Stmt::Assert(assert_stmt) => {
@@ -624,6 +925,11 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(()) Ok(())
} }
ast::Stmt::ImportFrom(import) => { 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")?; validate_nonempty_seq(vm, import.names.len(), "names", "ImportFrom")?;
Ok(()) Ok(())
} }
@@ -636,10 +942,8 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> {
Ok(()) Ok(())
} }
ast::Stmt::Expr(expr) => validate_expr(vm, &expr.value, ast::ExprContext::Load), ast::Stmt::Expr(expr) => validate_expr(vm, &expr.value, ast::ExprContext::Load),
ast::Stmt::Pass(_) ast::Stmt::Pass(_) | ast::Stmt::Break(_) | ast::Stmt::Continue(_) => Ok(()),
| ast::Stmt::Break(_) ast::Stmt::IpyEscapeCommand(_) => Err(invalid_syntax_error(vm)),
| ast::Stmt::Continue(_)
| ast::Stmt::IpyEscapeCommand(_) => Ok(()),
} }
} }
@@ -652,10 +956,17 @@ fn validate_stmts(vm: &VirtualMachine, stmts: &[ast::Stmt]) -> PyResult<()> {
pub(super) fn validate_mod(vm: &VirtualMachine, module: &Mod) -> PyResult<()> { pub(super) fn validate_mod(vm: &VirtualMachine, module: &Mod) -> PyResult<()> {
match module { match module {
Mod::Module(module) => validate_stmts(vm, &module.body), Mod::Module(module) => {
Mod::Interactive(module) => validate_stmts(vm, &module.body), 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::Expression(expr) => validate_expr(vm, &expr.body, ast::ExprContext::Load),
Mod::FunctionType(func_type) => { 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_exprs(vm, &func_type.argtypes, ast::ExprContext::Load, false)?;
validate_expr(vm, &func_type.returns, ast::ExprContext::Load) validate_expr(vm, &func_type.returns, ast::ExprContext::Load)
} }
+2
View File
@@ -153,7 +153,9 @@ mod _symtable {
CompilerScope::Class => TYPE_CLASS, CompilerScope::Class => TYPE_CLASS,
CompilerScope::Module => TYPE_MODULE, CompilerScope::Module => TYPE_MODULE,
CompilerScope::Annotation => TYPE_ANNOTATION, CompilerScope::Annotation => TYPE_ANNOTATION,
CompilerScope::TypeAlias => TYPE_TYPE_ALIAS,
CompilerScope::TypeParams => TYPE_TYPE_PARAMETERS, 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, PyByteArray, PyBytes, PyDictRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType,
PyUtf8StrRef, PyUtf8StrRef,
enumerate::PyReverseSequenceIterator, enumerate::PyReverseSequenceIterator,
function::{PyCellRef, PyFunction}, function::{PyCell, PyCellRef, PyFunction},
int::PyIntRef, int::PyIntRef,
iter::PyCallableIterator, iter::PyCallableIterator,
list::{PyList, SortOptions}, list::{PyList, SortOptions},
}, },
bytecode,
common::hash::PyHash, common::hash::PyHash,
function::{ function::{
ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping, ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping,
@@ -29,9 +30,13 @@ mod builtins {
readline::{Readline, ReadlineResult}, readline::{Readline, ReadlineResult},
stdlib::sys, stdlib::sys,
types::PyComparisonOp, 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 itertools::Itertools;
use num_traits::{Signed, ToPrimitive, Zero}; use num_traits::{Signed, ToPrimitive};
use rustpython_common::wtf8::CodePoint; use rustpython_common::wtf8::CodePoint;
#[cfg(not(feature = "rustpython-compiler"))] #[cfg(not(feature = "rustpython-compiler"))]
@@ -103,8 +108,8 @@ mod builtins {
filename: PyObjectRef, filename: PyObjectRef,
mode: PyUtf8StrRef, mode: PyUtf8StrRef,
// CPython parity: flags / optimize accept any object with __index__, // CPython parity: flags / optimize accept any object with __index__,
// not just exact int. Matches the behavior of `int(x)` arg conversion // not just exact int. Matches the argument conversion used by
// used by Python/Python-ast.c::compile. // builtin_compile_impl.
#[pyarg(any, optional)] #[pyarg(any, optional)]
flags: OptionalArg<ArgPrimitiveIndex<i32>>, flags: OptionalArg<ArgPrimitiveIndex<i32>>,
// CPython parity: dont_inherit goes through PyObject_IsTrue, so // CPython parity: dont_inherit goes through PyObject_IsTrue, so
@@ -114,174 +119,67 @@ mod builtins {
dont_inherit: OptionalArg<ArgIntoBool>, dont_inherit: OptionalArg<ArgIntoBool>,
#[pyarg(any, optional)] #[pyarg(any, optional)]
optimize: OptionalArg<ArgPrimitiveIndex<i32>>, optimize: OptionalArg<ArgPrimitiveIndex<i32>>,
#[pyarg(any, optional)] #[pyarg(named, optional)]
_feature_version: OptionalArg<i32>, _feature_version: OptionalArg<i32>,
} }
/// Detect PEP 263 encoding cookie from source bytes. fn merge_compile_future_features(
/// Checks first two lines for `# coding[:=] <encoding>` pattern. flags: i32,
/// Returns the encoding name if found, or None for default (UTF-8). dont_inherit: bool,
#[cfg(feature = "parser")] vm: &VirtualMachine,
fn detect_source_encoding(source: &[u8]) -> Option<String> { ) -> bytecode::CodeFlags {
fn find_encoding_in_line(line: &[u8]) -> Option<String> { let mut future_features = compile_future_features_from_flags(flags);
// PEP 263: '#' must be preceded only by whitespace/formfeed if !dont_inherit && let Some(frame) = vm.current_frame() {
let hash_pos = line.iter().position(|&b| b == b'#')?; future_features |= bytecode::CodeFlags::from_bits_truncate(
if !line[..hash_pos] frame.code.flags.bits() & compile_future_feature_mask().bits(),
.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))
}
} }
future_features
// 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)
} }
/// Match CPython's Parser/tokenizer/helpers.c:get_normal_name(). fn audit_compile_source(vm: &VirtualMachine, source: &[u8], filename: &str) -> PyResult<()> {
#[cfg(feature = "parser")] vm.sys_module.get_attr("audit", vm)?.call(
fn normalize_source_encoding(name: &str) -> String { (
let mut normalized = String::with_capacity(name.len().min(12)); vm.ctx.new_str("compile"),
for ch in name.chars().take(12) { vm.ctx.new_bytes(source.to_vec()),
if ch == '_' { vm.ctx.new_str(filename),
normalized.push('-'); ),
} else { vm,
normalized.push(ch.to_ascii_lowercase()); )?;
} Ok(())
} }
if normalized == "utf-8" || normalized.starts_with("utf-8-") { fn trim_eval_source_bytes(mut source: &[u8]) -> &[u8] {
"utf-8".to_owned() while let Some((&first, rest)) = source.split_first()
} else if normalized == "latin-1" && matches!(first, b' ' | b'\t')
|| 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-")
{ {
"iso-8859-1".to_owned() source = rest;
} else {
name.to_owned()
} }
source
} }
/// Decode source bytes to a string, handling PEP 263 encoding declarations fn decode_eval_exec_source_bytes(
/// and BOM. Raises SyntaxError for invalid UTF-8 without an encoding vm: &VirtualMachine,
/// declaration. source: &[u8],
#[cfg(feature = "parser")] filename: &str,
fn is_utf8_encoding(name: &str) -> bool { ) -> PyResult<String> {
name == "utf-8" #[cfg(feature = "parser")]
} {
vm.decode_source_bytes(source, filename, false)
#[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(),
));
} }
#[cfg(not(feature = "parser"))]
if is_utf8 { {
let src = if has_bom { &source[3..] } else { source }; _ = filename;
match core::str::from_utf8(src) { core::str::from_utf8(source)
Ok(s) => Ok(s.to_owned()), .map(str::to_owned)
Err(e) => { .map_err(|err| {
let bad_byte = src[e.valid_up_to()]; let msg = format!(
let line = src[..e.valid_up_to()] "(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte",
.iter() source[err.valid_up_to()],
.filter(|&&b| b == b'\n') err.valid_up_to()
.count() );
+ 1; vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into())
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())
} }
} }
@@ -303,30 +201,60 @@ mod builtins {
use crate::{class::PyClassImpl, stdlib::_ast}; 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 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: i32 = args.optimize.map_or(-1, |v| v.value);
let optimize: u8 = match optimize { let optimize: u8 = match optimize {
-1 => vm.state.config.settings.optimize, -1 => vm.state.config.settings.optimize.min(2),
0..=2 => optimize as u8, 0..=2 => optimize as u8,
_ => return Err(vm.new_value_error("compile(): invalid optimize value")), _ => 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 let start = if mode_str == "exec" {
.source PY_FILE_INPUT
.fast_isinstance(&_ast::NodeAst::make_static_type()) } else if mode_str == "eval" {
{ PY_EVAL_INPUT
let flags: i32 = args.flags.map_or(0, |v| v.value); } else if mode_str == "single" {
let is_ast_only = !(flags & _ast::PY_CF_ONLY_AST).is_zero(); PY_SINGLE_INPUT
} else if mode_str == "func_type" {
// func_type mode requires PyCF_ONLY_AST if !is_ast_only {
if mode_str == "func_type" && !is_ast_only {
return Err(vm.new_value_error( return Err(vm.new_value_error(
"compile() mode 'func_type' requires flag PyCF_ONLY_AST", "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 // compile(ast_node, ..., PyCF_ONLY_AST) returns the AST after validation
if is_ast_only { if is_ast_only {
@@ -336,15 +264,29 @@ mod builtins {
"compile() mode must be 'exec', 'eval', 'single' or 'func_type'", "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!( return Err(vm.new_type_error(format!(
"expected {} node, got {}", "expected {} node, got {}",
expected_name, expected_name,
args.source.class().name() args.source.class().name()
))); )));
} }
_ast::validate_ast_object(vm, args.source.clone())?; #[cfg(not(feature = "rustpython-codegen"))]
return Ok(args.source); {
_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"))] #[cfg(not(feature = "rustpython-codegen"))]
@@ -353,133 +295,104 @@ mod builtins {
} }
#[cfg(feature = "rustpython-codegen")] #[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 let mode = mode_str
.parse::<crate::compiler::Mode>() .parse::<crate::compiler::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?; .map_err(|err| vm.new_value_error(err.to_string()))?;
return _ast::compile( let mut opts = vm.compile_opts();
vm, opts.optimize = optimize;
args.source, opts.allow_top_level_await = cf.contains(CompilerFlags::ALLOW_TOP_LEVEL_AWAIT);
&filename.to_string_lossy(), opts.future_features = future_features;
mode, return _ast::compile(vm, args.source, &filename.to_string_lossy(), mode, opts);
Some(optimize),
);
} }
} }
#[cfg(not(feature = "parser"))] #[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")] #[cfg(feature = "parser")]
{ {
use crate::convert::ToPyException;
use ruff_python_parser as parser;
let source = ArgStrOrBytesLike::try_from_object(vm, args.source)?; 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 mut compile_flags = flags | future_features.bits() as i32;
let source = source.as_str(); #[cfg(feature = "rustpython-compiler")]
let compile_source = |source: &[u8], compile_flags: i32| {
let flags: i32 = args.flags.map_or(0, |v| v.value); vm.compile_string_object_with_flags(
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,
source, source,
mode, &filename.to_string_lossy(),
optimize_level, start,
compile_flags,
feature_version, feature_version,
type_comments, optimize as i32,
) )
.map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm))?; };
match &source {
if mode_str == "single" { ArgStrOrBytesLike::Str(source) => {
return _ast::wrap_interactive(vm, parsed); 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] #[pyfunction]
fn delattr(obj: PyObjectRef, attr: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { 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!( vm.new_type_error(format!(
"attribute name must be string, not '{}'", "attribute name must be string, not '{}'",
attr.class().name() attr.class().name()
@@ -507,42 +420,39 @@ mod builtins {
} }
impl ScopeArgs { 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( fn make_scope(
self, self,
vm: &VirtualMachine, vm: &VirtualMachine,
func_name: &'static str, func_name: &'static str,
) -> PyResult<crate::scope::Scope> { ) -> 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 { let (globals, locals) = match self.globals {
Some(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)?; let globals = PyDictRef::try_from_object(vm, globals)?;
if !globals.contains_key(identifier!(vm, __builtins__), vm) { 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] #[pyfunction]
fn eval( fn eval(
source: Either<ArgStrOrBytesLike, PyRef<crate::builtins::PyCode>>, source: Either<ArgStrOrBytesLike, PyRef<crate::builtins::PyCode>>,
@@ -581,38 +546,93 @@ mod builtins {
// source as string // source as string
let code = match source { let code = match source {
Either::A(either) => { Either::A(either) => {
let source: &[u8] = &either.borrow_bytes(); let source = match &either {
if source.contains(&0) { ArgStrOrBytesLike::Str(source) => {
return Err(vm.new_exception_msg( if source.as_bytes().contains(&0) {
vm.ctx.exceptions.syntax_error.to_owned(), return Err(vm.new_exception_msg(
"source code string cannot contain null bytes".into(), 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 source = source.expect_str().trim_start_matches([' ', '\t']);
let msg = format!( audit_compile_source(vm, source.as_bytes(), "<string>")?;
"(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte", source.to_owned()
source[err.valid_up_to()], }
err.valid_up_to() ArgStrOrBytesLike::Buf(source) => {
); let source: &[u8] = &source.borrow_buf();
if source.contains(&0) {
vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into()) return Err(vm.new_exception_msg(
})?; vm.ctx.exceptions.syntax_error.to_owned(),
Ok(Either::A(vm.ctx.new_utf8_str(source.trim_start()))) "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)), 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] #[pyfunction]
fn exec( fn exec(args: ExecArgs, vm: &VirtualMachine) -> PyResult {
source: Either<PyUtf8StrRef, PyRef<crate::builtins::PyCode>>, let ExecArgs {
scope: ScopeArgs, source,
vm: &VirtualMachine, globals,
) -> PyResult { locals,
let scope = scope.make_scope(vm, "exec")?; closure,
run_code(vm, source, scope, crate::compiler::Mode::Exec, "exec") } = 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( fn run_code(
@@ -621,28 +641,39 @@ mod builtins {
scope: crate::scope::Scope, scope: crate::scope::Scope,
#[allow(unused_variables)] mode: crate::compiler::Mode, #[allow(unused_variables)] mode: crate::compiler::Mode,
func: &str, func: &str,
closure: Option<PyRef<PyTuple<PyCellRef>>>,
) -> PyResult { ) -> PyResult {
// Determine code object: // Determine code object:
let code_obj = match source { let code_obj = match source {
#[cfg(feature = "rustpython-compiler")] #[cfg(feature = "rustpython-compiler")]
Either::A(string) => { Either::A(string) => {
let source = string.as_str(); let source = string.as_str();
vm.compile(source, mode, "<string>") let mut opts = vm.compile_opts();
.map_err(|err| vm.new_syntax_error(&err, Some(source)))? 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"))] #[cfg(not(feature = "rustpython-compiler"))]
Either::A(_) => return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED)), Either::A(_) => return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED)),
Either::B(code_obj) => code_obj, 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!( return Err(vm.new_type_error(format!(
"code object passed to {func}() may not contain free variables" "code object passed to {func}() may not contain free variables"
))); )));
} }
// Run the code: // Run the code:
vm.run_code_obj(code_obj, scope) vm.run_code_obj_with_closure(code_obj, scope, closure)
} }
#[pyfunction] #[pyfunction]
@@ -1002,8 +1033,8 @@ mod builtins {
modulus, modulus,
} = args; } = args;
let modulus = modulus let modulus = modulus
.as_ref() .as_deref()
.map_or_else(|| vm.ctx.none.as_object(), |m| m); .unwrap_or_else(|| vm.ctx.none.as_object());
vm._pow(&x, &y, modulus) vm._pow(&x, &y, modulus)
} }
+50 -5
View File
@@ -751,7 +751,7 @@ pub mod sys {
.read_to_string(&mut source) .read_to_string(&mut source)
.map_err(|e| vm.new_os_error(format!("Error reading from stdin: {e}")))?; .map_err(|e| vm.new_os_error(format!("Error reading from stdin: {e}")))?;
vm.compile(&source, crate::compiler::Mode::Single, "<stdin>") 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(()) Ok(())
} }
@@ -1228,7 +1228,7 @@ pub mod sys {
vm.state.int_max_str_digits.store(maxdigits); vm.state.int_max_str_digits.store(maxdigits);
Ok(()) Ok(())
} else { } 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)) Err(vm.new_value_error(error))
} }
} }
@@ -1744,12 +1744,54 @@ pub mod sys {
} }
for hook in hooks { for hook in hooks {
hook.call((event.clone(), args.clone()), vm)?; call_audit_hook(&hook, event.clone().into(), args, vm)?;
} }
Ok(()) 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] #[pyfunction]
fn audit(event: PyStrRef, args: PosArgs, vm: &VirtualMachine) -> PyResult<()> { fn audit(event: PyStrRef, args: PosArgs, vm: &VirtualMachine) -> PyResult<()> {
if vm.audit_hooks.borrow().is_empty() { if vm.audit_hooks.borrow().is_empty() {
@@ -1773,10 +1815,13 @@ pub mod sys {
let event: PyObjectRef = vm.ctx.new_str("sys.addaudithook").into(); let event: PyObjectRef = vm.ctx.new_str("sys.addaudithook").into();
for existing_hook in hooks { 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; continue;
}; };
if exc.class().fast_issubclass(vm.ctx.exceptions.runtime_error) { if exc
.class()
.fast_issubclass(vm.ctx.exceptions.exception_type)
{
return Ok(()); return Ok(());
} }
return Err(exc); return Err(exc);
+3 -1
View File
@@ -747,7 +747,7 @@ fn fire(
cb_extra: &[PyObjectRef], cb_extra: &[PyObjectRef],
) -> PyResult<()> { ) -> PyResult<()> {
// Prevent recursive event firing // Prevent recursive event firing
if FIRING.with(|f| f.get()) { if vm.tracing_is_suppressed() || FIRING.with(|f| f.get()) {
return Ok(()); return Ok(());
} }
@@ -795,6 +795,7 @@ fn fire(
let args = FuncArgs::from(args_vec); let args = FuncArgs::from(args_vec);
FIRING.with(|f| f.set(true)); FIRING.with(|f| f.set(true));
vm.enter_tracing();
let result = (|| { let result = (|| {
for (tool, cb) in callbacks { for (tool, cb) in callbacks {
let result = cb.call(args.clone(), vm)?; let result = cb.call(args.clone(), vm)?;
@@ -817,6 +818,7 @@ fn fire(
} }
Ok(()) Ok(())
})(); })();
vm.leave_tracing();
FIRING.with(|f| f.set(false)); FIRING.with(|f| f.set(false));
result 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 scope = vm.new_scope_with_builtins();
/// let source = r#"print("Hello World!")"#; /// let source = r#"print("Hello World!")"#;
/// let code_obj = vm.compile( /// let code_obj = vm.compile(
/// source, Mode::Exec, "<embedded>" /// source,
/// ).map_err(|err| vm.new_syntax_error(&err, Some(source))).unwrap(); /// Mode::Exec,
/// "<embedded>",
/// ).map_err(|err| err.into_pyexception(vm, Some(source))).unwrap();
/// vm.run_code_obj(code_obj, scope).unwrap(); /// vm.run_code_obj(code_obj, scope).unwrap();
/// }); /// });
/// ``` /// ```
+47 -26
View File
@@ -5,6 +5,9 @@
#[cfg(feature = "rustpython-compiler")] #[cfg(feature = "rustpython-compiler")]
mod compile; mod compile;
pub(crate) mod compile_mode;
#[cfg(feature = "rustpython-compiler")]
pub use compile::VmCompileError;
mod context; mod context;
mod interpreter; mod interpreter;
mod method; mod method;
@@ -82,6 +85,7 @@ pub struct VirtualMachine {
pub profile_func: RefCell<PyObjectRef>, pub profile_func: RefCell<PyObjectRef>,
pub trace_func: RefCell<PyObjectRef>, pub trace_func: RefCell<PyObjectRef>,
pub use_tracing: Cell<bool>, pub use_tracing: Cell<bool>,
tracing_depth: Cell<usize>,
pub recursion_limit: Cell<usize>, pub recursion_limit: Cell<usize>,
pub(crate) signal_handlers: OnceCell<SignalHandlers>, pub(crate) signal_handlers: OnceCell<SignalHandlers>,
pub(crate) signal_rx: Option<signal::UserSignalReceiver>, pub(crate) signal_rx: Option<signal::UserSignalReceiver>,
@@ -739,6 +743,7 @@ impl VirtualMachine {
profile_func, profile_func,
trace_func, trace_func,
use_tracing: Cell::new(false), use_tracing: Cell::new(false),
tracing_depth: Cell::new(0),
recursion_limit: Cell::new(if cfg!(debug_assertions) { 256 } else { 1000 }), recursion_limit: Cell::new(if cfg!(debug_assertions) { 256 } else { 1000 }),
signal_handlers, signal_handlers,
signal_rx: None, signal_rx: None,
@@ -1095,30 +1100,22 @@ impl VirtualMachine {
} }
pub fn run_code_obj(&self, code: PyRef<PyCode>, scope: Scope) -> PyResult { 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 pub(crate) fn run_code_obj_with_closure(
let func = PyFunction::new(code.clone(), scope.globals.clone(), self)?; &self,
let func_obj = func.into_ref(&self.ctx).into(); 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 // Create a function object for module code, similar to PyEval_EvalCode
let builtins = match scope let mut func = PyFunction::new(code, scope.globals.clone(), self)?;
.globals func.closure = closure;
.get_item_opt(identifier!(self, __builtins__), self)? let func = func.into_ref(&self.ctx);
{ func.invoke_with_locals(FuncArgs::default(), scope.locals, 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)
} }
#[cold] #[cold]
@@ -1434,9 +1431,11 @@ impl VirtualMachine {
} }
/// Stack margin bytes (like _PyOS_STACK_MARGIN_BYTES). /// 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))] #[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. /// Get the stack boundaries using platform-specific APIs.
/// Returns (base, top) where base is the lowest address and top is the highest. /// Returns (base, top) where base is the lowest address and top is the highest.
@@ -1709,11 +1708,33 @@ impl VirtualMachine {
#[cfg(feature = "rustpython-codegen")] #[cfg(feature = "rustpython-codegen")]
pub fn compile_opts(&self) -> crate::compiler::CompileOpts { pub fn compile_opts(&self) -> crate::compiler::CompileOpts {
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, 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. // To be called right before raising the recursion depth.
fn check_recursive_call(&self, _where: &str) -> PyResult<()> { fn check_recursive_call(&self, _where: &str) -> PyResult<()> {
if self.recursion_depth.get() >= self.recursion_limit.get() { 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 source = "from dir_module.dir_module_inner import value2";
let code_obj = vm let code_obj = vm
.compile(source, vm::compiler::Mode::Exec, "<embedded>") .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(); .unwrap();
if let Err(e) = vm.run_code_obj(code_obj, scope) { 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 { pub fn run_string(&self, scope: Scope, source: &str, source_path: &str) -> PyResult {
let code_obj = self let code_obj = self
.compile(source, compiler::Mode::Exec, source_path) .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) // linecache._register_code(code, source, filename)
let _ = self.register_code_in_linecache(&code_obj, source); let _ = self.register_code_in_linecache(&code_obj, source);
self.run_code_obj(code_obj, scope) self.run_code_obj(code_obj, scope)
@@ -47,7 +47,7 @@ impl VirtualMachine {
pub fn run_block_expr(&self, scope: Scope, source: &str) -> PyResult { pub fn run_block_expr(&self, scope: Scope, source: &str) -> PyResult {
let code_obj = self let code_obj = self
.compile(source, compiler::Mode::BlockExpr, "<embedded>") .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) self.run_code_obj(code_obj, scope)
} }
} }
@@ -105,11 +105,19 @@ mod file_run {
if path != "<stdin>" { if path != "<stdin>" {
set_main_loader(module_dict, path, "SourceFileLoader", self)?; set_main_loader(module_dict, path, "SourceFileLoader", self)?;
} }
match crate::host_env::fs::read_to_string(path) { match crate::host_env::fs::read(path) {
Ok(source) => { 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 let code_obj = self
.compile(&source, compiler::Mode::Exec, path) .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)?; self.run_code_obj(code_obj, scope)?;
} }
Err(err) => { Err(err) => {
+1
View File
@@ -719,6 +719,7 @@ impl VirtualMachine {
profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())), profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())),
trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())), trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())),
use_tracing: Cell::new(use_tracing), use_tracing: Cell::new(use_tracing),
tracing_depth: Cell::new(0),
recursion_limit: self.recursion_limit.clone(), recursion_limit: self.recursion_limit.clone(),
signal_handlers: core::cell::OnceCell::new(), signal_handlers: core::cell::OnceCell::new(),
signal_rx: None, signal_rx: None,
+82 -24
View File
@@ -54,15 +54,6 @@ impl SyntaxErrorInfo {
Self { msg, narrow_caret } 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")] #[cfg(feature = "parser")]
#[must_use] #[must_use]
const fn handle_expected_token(expected: TokenKind, found: TokenKind) -> &'static str { const fn handle_expected_token(expected: TokenKind, found: TokenKind) -> &'static str {
@@ -114,7 +105,7 @@ impl SyntaxErrorInfo {
} }
ParseErrorType::InvalidStarredExpressionUsage => { ParseErrorType::InvalidStarredExpressionUsage => {
self.with_narrow_caret(true); self.narrow_caret = true;
"invalid syntax".into() "invalid syntax".into()
} }
@@ -134,7 +125,7 @@ impl SyntaxErrorInfo {
ParseErrorType::EmptyTypeParams => "Type parameter list cannot be empty".into(), ParseErrorType::EmptyTypeParams => "Type parameter list cannot be empty".into(),
ParseErrorType::InvalidStarPatternUsage => { ParseErrorType::InvalidStarPatternUsage => {
self.with_narrow_caret(true); self.narrow_caret = true;
"cannot use starred expression here".into() "cannot use starred expression here".into()
} }
@@ -179,12 +170,22 @@ impl SyntaxErrorInfo {
| ParseErrorType::SimpleAndCompoundStatementOnSameLine | ParseErrorType::SimpleAndCompoundStatementOnSameLine
| ParseErrorType::ExpectedExpression => "invalid syntax".into(), | ParseErrorType::ExpectedExpression => "invalid syntax".into(),
ParseErrorType::OtherError(s) if s.starts_with("Expected an identifier") => {
"invalid syntax".into()
}
ParseErrorType::OtherError(s) 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() "invalid syntax".into()
} }
ParseErrorType::OtherError(s) if s.eq_ignore_ascii_case("Expected a statement") => {
"invalid syntax".into()
}
ParseErrorType::OtherError(s) ParseErrorType::OtherError(s)
if s.eq_ignore_ascii_case( if s.eq_ignore_ascii_case(
"bytes literal cannot be mixed with non-bytes literals", "bytes literal cannot be mixed with non-bytes literals",
@@ -262,7 +263,7 @@ impl SyntaxErrorInfo {
_ => return, _ => return,
}; };
self.with_msg(&msg); self.msg = msg;
} }
} }
@@ -576,6 +577,16 @@ impl VirtualMachine {
source: Option<&str>, source: Option<&str>,
allow_incomplete: bool, allow_incomplete: bool,
) -> PyBaseExceptionRef { ) -> 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> { let incomplete_or_syntax = |allow| -> &'static Py<crate::builtins::PyType> {
if allow { if allow {
self.ctx.exceptions.incomplete_input_error self.ctx.exceptions.incomplete_input_error
@@ -687,7 +698,9 @@ impl VirtualMachine {
raw_location, 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 { if allow_incomplete {
// Check that all chars in the error are whitespace, if so, the source is // Check that all chars in the error are whitespace, if so, the source is
// incomplete. Otherwise, we've found code that might violates // incomplete. Otherwise, we've found code that might violates
@@ -717,6 +730,12 @@ impl VirtualMachine {
} else { } else {
self.ctx.exceptions.indentation_error 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 { } else {
self.ctx.exceptions.syntax_error self.ctx.exceptions.syntax_error
} }
@@ -738,7 +757,29 @@ impl VirtualMachine {
let statement = source.and_then(|src| get_statement(src, error.location())); let statement = source.and_then(|src| get_statement(src, error.location()));
let mut msg = error.to_string(); 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(); msg.make_ascii_lowercase();
} }
@@ -753,17 +794,26 @@ impl VirtualMachine {
}; };
if syntax_error_type.is(self.ctx.exceptions.tab_error) { 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 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 syntax_error = self.new_exception_msg(syntax_error_type, msg.into());
let (lineno, offset) = error.python_location(); let (lineno_raw, offset_raw) = error.python_location();
let lineno = self.ctx.new_int(lineno); let lineno = self.ctx.new_int(lineno_raw);
let offset = self.ctx.new_int(offset); let offset = self.ctx.new_int(offset_raw);
set_attrs!( set_attrs!(
syntax_error.as_object(), self, unwrap, syntax_error.as_object(), self, unwrap,
"lineno" => lineno, "lineno" => lineno,
@@ -772,15 +822,23 @@ impl VirtualMachine {
// Set end_lineno and end_offset if available // Set end_lineno and end_offset if available
if let Some((end_lineno, end_offset)) = error.python_end_location() { 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(); let (l, o) = error.python_location();
(l, o + 1) (l, (o + 1) as isize)
} else { } else {
(end_lineno, end_offset) (end_lineno, end_offset as isize)
}; };
let end_lineno = self.ctx.new_int(end_lineno); let end_lineno = self.ctx.new_int(end_lineno);
let end_offset = self.ctx.new_int(end_offset); let end_offset = self.ctx.new_int(end_offset);
set_attrs!( set_attrs!(
syntax_error.as_object(), self, unwrap, syntax_error.as_object(), self, unwrap,
"end_lineno" => end_lineno, "end_lineno" => end_lineno,
+1
View File
@@ -20,6 +20,7 @@ no-start-func = []
rustpython-common = { workspace = true } rustpython-common = { workspace = true }
rustpython-pylib = { workspace = true, optional = true } rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, default-features = false, 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 # make sure no threading! otherwise wasm build will fail
rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde", "wasmbind"] } 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 { if let Some(js_vars) = js_vars {
vm.add_to_scope("js_vars".into(), js_vars.into())?; 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 /// Evaluate Python code
+64 -7
View File
@@ -6,9 +6,14 @@ use crate::{
use alloc::rc::{Rc, Weak}; use alloc::rc::{Rc, Weak};
use core::cell::RefCell; use core::cell::RefCell;
use js_sys::{Object, TypeError}; use js_sys::{Object, TypeError};
use ruff_text_size::Ranged;
use rustpython_vm::{ use rustpython_vm::{
Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine, builtins::PyWeak, Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine,
compiler::Mode, function::ArgMapping, scope::Scope, builtins::PyWeak,
compiler::{self, Mode},
function::ArgMapping,
scope::Scope,
vm::VmCompileError,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@@ -21,6 +26,25 @@ pub(crate) struct StoredVirtualMachine {
held_objects: RefCell<Vec<PyObjectRef>>, 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] #[pymodule]
mod _window { mod _window {
use super::{js_module, wasm_builtins}; use super::{js_module, wasm_builtins};
@@ -263,8 +287,8 @@ impl WASMVirtualMachine {
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
self.with_vm(|vm, _| { self.with_vm(|vm, _| {
let code = vm let code = vm
.compile(source, Mode::Exec, &name) .compile(source, Mode::Exec, name.as_str())
.map_err(convert::syntax_err)?; .map_err(|err| compile_err_to_js(vm, err))?;
let attrs = vm.ctx.new_dict(); let attrs = vm.ctx.new_dict();
attrs attrs
.set_item("__name__", vm.new_pyobj(name.as_str()), vm) .set_item("__name__", vm.new_pyobj(name.as_str()), vm)
@@ -327,13 +351,46 @@ impl WASMVirtualMachine {
) -> Result<JsValue, JsValue> { ) -> Result<JsValue, JsValue> {
self.with_vm(|vm, StoredVirtualMachine { scope, .. }| { self.with_vm(|vm, StoredVirtualMachine { scope, .. }| {
let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned()); let source_path = source_path.unwrap_or_else(|| "<wasm>".to_owned());
let code = vm.compile(source, mode, &source_path); let code = vm.compile(source, mode, source_path.as_str());
let code = code.map_err(convert::syntax_err)?; let code = code.map_err(|err| compile_err_to_js(vm, err))?;
let result = vm.run_code_obj(code, scope.clone()); let result = vm.run_code_obj(code, scope.clone());
convert::pyresult_to_js_result(vm, result) 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> { pub fn exec(&self, source: &str, source_path: Option<String>) -> Result<JsValue, JsValue> {
self.run(source, Mode::Exec, source_path) self.run(source, Mode::Exec, source_path)
} }
@@ -348,6 +405,6 @@ impl WASMVirtualMachine {
source: &str, source: &str,
source_path: Option<String>, source_path: Option<String>,
) -> Result<JsValue, JsValue> { ) -> 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 source = r#"print("Hello World!")"#;
let code_obj = vm let code_obj = vm
.compile(source, vm::compiler::Mode::Exec, "<embedded>") .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)?; 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) // (note that this is only the case when compiler::Mode::Single is passed to vm.compile)
match vm match vm
.compile(&input, vm::compiler::Mode::Single, "<embedded>") .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())) .and_then(|code_obj| vm.run_code_obj(code_obj, scope.clone()))
{ {
Ok(output) => { Ok(output) => {
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more