diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 22f0a9a8a01..00000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "matcher": "", - "hooks": [ - { - "type": "command", - "command": "bash .claude/scripts/setup-env.sh" - } - ] - } - ] - } -} diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 84265bda609..da85c312898 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -4,6 +4,7 @@ argdefs argtypes asdl asname +ASNATIVEBYTES atopen atext attro @@ -139,6 +140,8 @@ metavars miscompiles mult multibytecodec +mystricmp +mystrnicmp nameobj nameop nargsf @@ -169,6 +172,7 @@ nvars opname opnames orelse +osmodule outparam outparm paramfunc @@ -189,11 +193,14 @@ pycore pyinner pydecimal pyerrors +pyframe Pyfunc pylifecycle pymain +pymem pyrepl pystate +pystrcmp PYTHONTRACEMALLOC PYTHONUTF8 pythonw diff --git a/.cspell.dict/rust-more.txt b/.cspell.dict/rust-more.txt index c4457723c6c..b53639c3b41 100644 --- a/.cspell.dict/rust-more.txt +++ b/.cspell.dict/rust-more.txt @@ -50,6 +50,7 @@ modpow msvc muldiv nanos +noalias nonoverlapping objclass peekable diff --git a/.cspell.dict/rustpython.txt b/.cspell.dict/rustpython.txt index 8cd08358019..07099bbb171 100644 --- a/.cspell.dict/rustpython.txt +++ b/.cspell.dict/rustpython.txt @@ -27,6 +27,7 @@ pystr pystruct pystructseq pytype +qsbr rustix struc zelf diff --git a/.cspell.json b/.cspell.json index 21199c0c5f5..57f7baab3f7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -59,6 +59,8 @@ "alnum", "csock", "coro", + "contig", + "Crnl", "dedentations", "dedents", "deduped", @@ -66,8 +68,13 @@ "deoptimize", "emscripten", "excs", + "fdigits", + "flufl", "fnfe", + "fsdefault", "ifexp", + "implicits", + "inity", "interps", "jitted", "jitting", @@ -76,8 +83,12 @@ "mcache", "oparg", "opargs", + "pointee", "pyc", + "qmark", "reborrow", + "reborrows", + "reparenting", "reraises", "reraising", "significand", diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index 7900060fb29..c2f1b20f2d9 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -29,6 +29,10 @@ inputs: description: Install gcc-aarch64-linux-gnu (gcc-aarch64-linux-gnu) required: false default: "false" + gcc-mingw-w64-x86-64: + description: Install gcc-mingw-w64-x86-64 (gcc-mingw-w64-x86-64) + required: false + default: "false" clang: description: Install clang (clang) required: false @@ -39,11 +43,40 @@ runs: - name: Install Linux dependencies shell: bash if: ${{ runner.os == 'Linux' }} - run: > - sudo apt-get update + env: + GCC_MULTILIB: ${{ inputs.gcc-multilib }} + MUSL_TOOLS: ${{ inputs.musl-tools }} + CLANG: ${{ inputs.clang }} + GCC_AARCH64_LINUX_GNU: ${{ inputs.gcc-aarch64-linux-gnu }} + GCC_MINGW_W64_X86_64: ${{ inputs.gcc-mingw-w64-x86-64 }} + 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 + + packages=() + if [[ "$GCC_MULTILIB" == "true" ]]; then + packages+=(gcc-multilib) + fi + if [[ "$MUSL_TOOLS" == "true" ]]; then + packages+=(musl-tools) + fi + if [[ "$CLANG" == "true" ]]; then + packages+=(clang) + fi + if [[ "$GCC_AARCH64_LINUX_GNU" == "true" ]]; then + packages+=(gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross) + fi + if [[ "$GCC_MINGW_W64_X86_64" == "true" ]]; then + packages+=(gcc-mingw-w64-x86-64) + fi - sudo apt-get install --no-install-recommends - ${{ fromJSON(inputs.gcc-multilib) && 'gcc-multilib' || '' }} - ${{ fromJSON(inputs.musl-tools) && 'musl-tools' || '' }} - ${{ fromJSON(inputs.clang) && 'clang' || '' }} - ${{ fromJSON(inputs.gcc-aarch64-linux-gnu) && 'gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross' || '' }} + if ((${#packages[@]})); then + sudo apt-get install --no-install-recommends "${packages[@]}" + fi diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index abc2173c6e7..5a8daae06ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,6 +34,7 @@ env: CARGO_PROFILE_RELEASE_DEBUG: 0 CARGO_TERM_COLOR: always CI: true + FORCE_COLOR: 1 jobs: determine_changes: @@ -43,7 +44,7 @@ jobs: # Flag that is raised when any rust code is changed. rust_code: ${{ steps.check_rust_code.outputs.changed }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -87,14 +88,14 @@ jobs: os: [macos-latest, ubuntu-latest, windows-2025] fail-fast: false steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -197,13 +198,17 @@ jobs: target: aarch64-unknown-linux-gnu dependencies: gcc-aarch64-linux-gnu: true + - os: ubuntu-latest + target: x86_64-pc-windows-gnu + dependencies: + gcc-mingw-w64-x86-64: true - os: macos-latest target: aarch64-apple-ios - os: macos-latest target: x86_64-apple-darwin fail-fast: false steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -215,9 +220,10 @@ jobs: gcc-multilib: ${{ matrix.dependencies.gcc-multilib || false }} musl-tools: ${{ matrix.dependencies.musl-tools || false }} gcc-aarch64-linux-gnu: ${{ matrix.dependencies.gcc-aarch64-linux-gnu || false }} + gcc-mingw-w64-x86-64: ${{ matrix.dependencies.gcc-mingw-w64-x86-64 || false }} - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -294,6 +300,8 @@ jobs: - os: macos-latest extra_test_args: - '-u all' + - '--timeout 600' + - '--dont-add-python-opts' env_polluting_tests: - test_set skips: [] @@ -301,26 +309,31 @@ jobs: - os: ubuntu-latest extra_test_args: - '-u all' + - '--timeout 600' + - '--dont-add-python-opts' env_polluting_tests: - test_set skips: [] timeout: 60 - os: windows-2025 - extra_test_args: [] # TODO: Enable '-u all' + extra_test_args: + - '-u all' + - '--timeout 600' + - '--dont-add-python-opts' env_polluting_tests: - test_set skips: [] timeout: 50 fail-fast: false steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -335,13 +348,14 @@ jobs: # Windows runners randomly crashes, https://github.com/actions/cache/issues/1754 continue-on-error: true - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - name: Install macOS dependencies uses: ./.github/actions/install-macos-deps with: openssl: true + # Keep features in sync with update-caches.yml CARGO_ARGS. - name: build rustpython run: cargo build --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }} @@ -358,7 +372,7 @@ jobs: - name: Run CPython tests run: | - target/release/rustpython -m test -j ${{ steps.cores.outputs.cores }} ${{ join(matrix.extra_test_args, ' ') }} --slowest --fail-env-changed --timeout 600 -v -x ${{ env.FLAKY_MP_TESTS }} ${{ join(matrix.skips, ' ') }} + target/release/rustpython -u -m test --slow-ci -j ${{ steps.cores.outputs.cores }} ${{ join(matrix.extra_test_args, ' ') }} -x ${{ env.FLAKY_MP_TESTS }} ${{ join(matrix.skips, ' ') }} timeout-minutes: ${{ matrix.timeout }} env: RUSTPYTHON_SKIP_ENV_POLLUTERS: true @@ -369,7 +383,7 @@ jobs: echo "::group::Attempt ${attempt}" set +e - target/release/rustpython -m test -j 1 ${{ join(matrix.extra_test_args, ' ') }} --slowest --fail-env-changed --timeout 600 -v ${{ env.FLAKY_MP_TESTS }} + target/release/rustpython -u -m test --slow-ci -j 1 ${{ join(matrix.extra_test_args, ' ') }} ${{ env.FLAKY_MP_TESTS }} status=$? set -e @@ -394,7 +408,7 @@ jobs: for thing in "${target_array[@]}"; do for i in $(seq 1 10); do set +e - target/release/rustpython -m test -j 1 --slowest --fail-env-changed --timeout 600 -v "${thing}" + target/release/rustpython -u -m test --slow-ci -u all -j 1 --timeout 600 --dont-add-python-opts "${thing}" exit_code=$? set -e if [ "${exit_code}" -eq 3 ]; then @@ -429,6 +443,17 @@ jobs: target/release/rustpython -m ensurepip target/release/rustpython -c "import pip" + - if: runner.os == 'Windows' + name: Check pip HTTPS with the Windows trust store + run: >- + target/release/rustpython -m pip download + --disable-pip-version-check + --no-cache-dir + --no-deps + --only-binary=:all: + --dest "$env:RUNNER_TEMP\rustpython-pip-smoke" + six + - if: runner.os != 'Windows' name: Check if pip inside venv is functional run: | @@ -457,7 +482,7 @@ jobs: - ubuntu-latest - windows-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -466,7 +491,7 @@ jobs: components: clippy - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -495,13 +520,13 @@ jobs: needs.determine_changes.outputs.rust_code == 'true' || github.ref == 'refs/heads/main' steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: cargo-bins/cargo-binstall@30b5ca8b54e1dcffd9548bc87ede1531310fdc67 # v1.20.0 + - uses: cargo-bins/cargo-binstall@e00d2c94cc0067b77737821097a62d91c0301baa # v1.21.1 - name: cargo shear run: | @@ -518,31 +543,31 @@ jobs: pull-requests: write security-events: write # for zizmor steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: actionlint - uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0 + uses: reviewdog/action-actionlint@d63ba7532e0942965320cd8d73cbae4c7b3c5283 # v1.73.1 - name: zizmor - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 - name: restore prek cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: prek-${{ hashFiles('.pre-commit-config.yaml') }} path: ~/.cache/prek - name: install prek id: prek - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 + uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 with: cache: false show-verbose-logs: false @@ -558,7 +583,7 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Clone CPython - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: python/cpython path: cpython @@ -572,7 +597,7 @@ jobs: - name: save prek cache if: ${{ github.ref == 'refs/heads/main' }} # only save on main - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: prek-${{ hashFiles('.pre-commit-config.yaml') }} path: ~/.cache/prek @@ -583,7 +608,7 @@ jobs: - name: reviewdog if: ${{ !cancelled() }} - uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # v1.24.0 + uses: reviewdog/action-suggester@2558ba17e65a9039e73764a73009fc05fef28a46 # v1.24.3 with: level: warning fail_level: error @@ -596,7 +621,7 @@ jobs: env: NIGHTLY_CHANNEL: nightly steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -606,7 +631,7 @@ jobs: components: miri - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -623,7 +648,10 @@ jobs: env: # miri-ignore-leaks because the type-object circular reference means that there will always be # a memory leak, at least until we have proper cyclic gc - MIRIFLAGS: "-Zmiri-ignore-leaks" + # miri-permissive-provenance because function pointer identity checks (slot comparisons) + # cast fn pointers to usize, which strips provenance — this is the standard pattern for + # fn pointer comparison in Rust and not a soundness issue + MIRIFLAGS: "-Zmiri-ignore-leaks -Zmiri-permissive-provenance" wasm: if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} @@ -631,7 +659,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -640,7 +668,7 @@ jobs: components: clippy - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -665,12 +693,12 @@ jobs: mkdir geckodriver tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - run: python -m pip install -r requirements.txt working-directory: ./wasm/tests - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: package-manager-cache: false @@ -680,7 +708,7 @@ jobs: run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT" - name: Restore npm cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # don't restore on main or release if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/release' with: @@ -735,7 +763,7 @@ jobs: - name: Save npm cache # Save only on main or release if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ steps.npm-cache-dir.outputs.dir }} key: node-${{ runner.os }}-wasm-demo-${{ hashFiles('wasm/demo/package-lock.json') }} @@ -746,7 +774,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -755,7 +783,7 @@ jobs: target: wasm32-wasip1 - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -781,7 +809,9 @@ jobs: - name: build rustpython run: cargo build --profile wasm-release --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib,stdio,importlib,host_env --verbose - name: run snippets - run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" + run: | + wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" + wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_time.py" - name: run cpython unittest run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/Lib/test/test_int.py" @@ -798,14 +828,14 @@ jobs: name: cargo doc runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable - name: Restore cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ @@ -820,4 +850,3 @@ jobs: - name: cargo doc run: cargo doc --locked - diff --git a/.github/workflows/cron-ci.yaml b/.github/workflows/cron-ci.yaml index 5721a642615..e3557e3e05f 100644 --- a/.github/workflows/cron-ci.yaml +++ b/.github/workflows/cron-ci.yaml @@ -27,17 +27,17 @@ jobs: env: INSTA_WORKSPACE_ROOT: ${{ github.workspace }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154 # v2.81.8 + - uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2.85.11 with: tool: cargo-llvm-cov - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - run: sudo apt-get update && sudo apt-get -y install lcov @@ -67,7 +67,7 @@ jobs: # Disable this scheduled job when running on a fork. if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true @@ -85,7 +85,6 @@ jobs: if: ${{ github.event_name != 'pull_request' }} env: SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }} - GITHUB_ACTOR: ${{ github.actor }} run: | echo "$SSHKEY" >~/github_key chmod 600 ~/github_key @@ -95,7 +94,7 @@ jobs: cd website cp ../extra_tests/cpython_tests_results.json ./_data/regrtests_results.json git add ./_data/regrtests_results.json - if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results" --author="$GITHUB_ACTOR"; then + if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results"; then git push fi @@ -105,13 +104,13 @@ jobs: # Disable this scheduled job when running on a fork. if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - name: build rustpython run: cargo build --release --verbose @@ -127,7 +126,6 @@ jobs: if: ${{ github.event_name != 'pull_request' }} env: SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }} - GITHUB_ACTOR: ${{ github.actor }} run: | echo "$SSHKEY" >~/github_key chmod 600 ~/github_key @@ -158,7 +156,7 @@ jobs: } EOF git add -A - if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update what is left results" --author="$GITHUB_ACTOR"; then + if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update what is left results"; then git push fi @@ -168,13 +166,13 @@ jobs: # Disable this scheduled job when running on a fork. if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true - uses: dtolnay/rust-toolchain@stable - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - run: cargo install cargo-criterion @@ -204,6 +202,8 @@ jobs: if: ${{ github.event_name != 'pull_request' }} env: SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }} + COMMIT_SHA: ${{ github.sha }} + REF_NAME: ${{ github.ref_name }} run: | echo "$SSHKEY" >~/github_key chmod 600 ~/github_key @@ -215,8 +215,8 @@ jobs: cp -r ../target/criterion ./assets/criterion printf '{\n "generated_at": "%s",\n "rustpython_commit": "%s",\n "rustpython_ref": "%s"\n}\n' \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - "${{ github.sha }}" \ - "${{ github.ref_name }}" > ./_data/criterion-metadata.json + "$COMMIT_SHA" \ + "$REF_NAME" > ./_data/criterion-metadata.json git add ./assets/criterion ./_data/criterion-metadata.json if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update benchmark results"; then git push diff --git a/.github/workflows/lib-deps-check.yaml b/.github/workflows/lib-deps-check.yaml index e74dd561bb5..6f147542ee6 100644 --- a/.github/workflows/lib-deps-check.yaml +++ b/.github/workflows/lib-deps-check.yaml @@ -6,6 +6,8 @@ on: paths: - "Lib/**" +permissions: {} + concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -18,7 +20,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout base branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Use base branch for scripts (security: don't run PR code with elevated permissions) ref: ${{ github.event.pull_request.base.ref }} @@ -26,13 +28,17 @@ jobs: persist-credentials: false - name: Fetch PR head + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - git fetch origin ${{ github.event.pull_request.head.sha }} + git fetch origin "$PR_HEAD_SHA" - name: Checkout PR Lib files + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # Checkout only Lib/ directory from PR head for accurate comparison - git checkout ${{ github.event.pull_request.head.sha }} -- Lib/ + git checkout "$PR_HEAD_SHA" -- Lib/ - name: Get target CPython version id: cpython-version @@ -41,7 +47,7 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Checkout CPython - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: python/cpython path: cpython @@ -51,14 +57,17 @@ jobs: - name: Get changed Lib files id: all-changed-files + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # Get the list of changed files under Lib/ { echo 'changed<> "$GITHUB_OUTPUT" - name: Parse changed files @@ -98,7 +107,7 @@ jobs: - name: Setup Python if: steps.changed-files.outputs.modules != '' - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - name: Run deps check if: steps.changed-files.outputs.modules != '' @@ -114,7 +123,7 @@ jobs: - name: Post comment if: steps.deps-check.outputs.deps_output != '' - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: lib-deps-check number: ${{ github.event.pull_request.number }} @@ -131,7 +140,7 @@ jobs: - name: Remove comment if no Lib changes if: steps.changed-files.outputs.modules == '' - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: lib-deps-check number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/oscca-pr.yml b/.github/workflows/oscca-pr.yml new file mode 100644 index 00000000000..38b67625ce0 --- /dev/null +++ b/.github/workflows/oscca-pr.yml @@ -0,0 +1,70 @@ +name: Manage OSCCA pull requests + +on: + pull_request_target: + types: [opened] + +permissions: {} + +jobs: + label-and-assign: + name: Label and assign OSCCA pull request + runs-on: ubuntu-slim + timeout-minutes: 5 + permissions: + pull-requests: write + steps: + - name: Label and assign pull request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const osccaUsers = new Set( + [ + "2jiyong", + "chestnut1717", + "devyubin", + "fregataa", + "hyoinandout", + "HyoJongPark", + "JaceJung-dev", + "jinmay", + "jiwahn", + "kangdora", + "kim-jaedeok", + "kyokuping", + "leehanjeong", + "lms0806", + "lsahn-gh", + "moreal", + "name-of-okja", + "rlaisqls", + "seungje0612", + "shAn-kor", + "sigmaith", + "teddygood", + "widehyo1", + "YangSiJun528", + "zzarbttoo", + ].map((login) => login.toLowerCase()), + ); + const pullRequest = context.payload.pull_request; + const author = pullRequest.user.login; + + if (!osccaUsers.has(author.toLowerCase())) { + core.info(`${author} is not an OSCCA participant; skipping.`); + return; + } + + const issue = { + ...context.repo, + issue_number: pullRequest.number, + }; + + await github.rest.issues.addLabels({ + ...issue, + labels: ["z-ca-2026"], + }); + await github.rest.issues.addAssignees({ + ...issue, + assignees: [author], + }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21a0068cbbf..5957666de38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,7 @@ jobs: # target: aarch64-pc-windows-msvc fail-fast: false steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -91,7 +91,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -114,7 +114,7 @@ jobs: - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: package-manager-cache: false @@ -156,7 +156,7 @@ jobs: permissions: contents: write # for creating a release steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index fc524fa738e..32c8e1a62de 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -19,6 +19,7 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_RELEASE_DEBUG: 0 + # Keep feature list in sync with CI's release build in .github/workflows/ci.yaml. CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher jobs: @@ -39,14 +40,14 @@ jobs: target: "" steps: - name: Checkout RustPython main branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: RustPython/RustPython ref: main persist-credentials: false - name: Setup Rust - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 + uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.toolchain }} target: ${{ matrix.target }} @@ -66,7 +67,7 @@ jobs: run: cargo build --profile release ${{ env.CARGO_ARGS }} - name: Save cache - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cargo/bin/ diff --git a/.github/workflows/update-doc-db.yml b/.github/workflows/update-doc-db.yml index a543f428cb5..c42235c86df 100644 --- a/.github/workflows/update-doc-db.yml +++ b/.github/workflows/update-doc-db.yml @@ -8,7 +8,7 @@ on: python-version: description: Target python version to generate doc db for type: string - default: "3.14.3" + default: "3.14.7" base-ref: description: Base branch to create the update branch from type: string @@ -30,13 +30,13 @@ jobs: - windows-latest - macos-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | crates/doc - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ inputs.python-version }} @@ -58,7 +58,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true ref: ${{ inputs.base-ref }} diff --git a/.github/workflows/update-libs-status.yaml b/.github/workflows/update-libs-status.yaml index 837586913c8..c1c656d2068 100644 --- a/.github/workflows/update-libs-status.yaml +++ b/.github/workflows/update-libs-status.yaml @@ -21,7 +21,7 @@ jobs: if: ${{ github.repository == 'RustPython/RustPython' }} steps: - name: Clone RustPython - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: rustpython persist-credentials: false @@ -37,7 +37,7 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Clone CPython - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: python/cpython path: cpython diff --git a/.github/workflows/upgrade-pylib.lock.yml b/.github/workflows/upgrade-pylib.lock.yml index 1a028f728b1..3ac44585943 100644 --- a/.github/workflows/upgrade-pylib.lock.yml +++ b/.github/workflows/upgrade-pylib.lock.yml @@ -58,7 +58,7 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@f990bbb7eb83981a203d4b5eccdc24f677e950c7 # v0.77.5 + uses: github/gh-aw/actions/setup@53258938b59e0797fefeed05ec0c681514b2a827 # v0.84.3 with: destination: /opt/gh-aw/actions - name: Check workflow file timestamps @@ -99,22 +99,22 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@f990bbb7eb83981a203d4b5eccdc24f677e950c7 # v0.77.5 + uses: github/gh-aw/actions/setup@53258938b59e0797fefeed05ec0c681514b2a827 # v0.84.3 with: destination: /opt/gh-aw/actions - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh # Cache configuration from frontmatter processed below - name: Cache (cpython-lib-${{ env.PYTHON_VERSION }}) - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: cpython-lib-${{ env.PYTHON_VERSION }} path: cpython @@ -806,7 +806,7 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@f990bbb7eb83981a203d4b5eccdc24f677e950c7 # v0.77.5 + uses: github/gh-aw/actions/setup@53258938b59e0797fefeed05ec0c681514b2a827 # v0.84.3 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -927,7 +927,7 @@ jobs: success: ${{ steps.parse_results.outputs.success }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@f990bbb7eb83981a203d4b5eccdc24f677e950c7 # v0.77.5 + uses: github/gh-aw/actions/setup@53258938b59e0797fefeed05ec0c681514b2a827 # v0.84.3 with: destination: /opt/gh-aw/actions - name: Download agent artifacts @@ -1039,7 +1039,7 @@ jobs: process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@f990bbb7eb83981a203d4b5eccdc24f677e950c7 # v0.77.5 + uses: github/gh-aw/actions/setup@53258938b59e0797fefeed05ec0c681514b2a827 # v0.84.3 with: destination: /opt/gh-aw/actions - name: Download agent output artifact @@ -1061,7 +1061,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ github.token }} persist-credentials: false diff --git a/.github/workflows/upgrade-pylib.md b/.github/workflows/upgrade-pylib.md index ac71f3d7244..cd05eb77734 100644 --- a/.github/workflows/upgrade-pylib.md +++ b/.github/workflows/upgrade-pylib.md @@ -52,7 +52,7 @@ cache: - cpython-lib- env: - PYTHON_VERSION: "v3.14.6" + PYTHON_VERSION: "v3.14.7" ISSUE_ID: "6839" --- diff --git a/.github/zizmor.yml b/.github/zizmor.yml index f22f76b70d8..02ceb805c2c 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,4 +1,14 @@ rules: + dangerous-triggers: + ignore: + # pull_request_target is needed to label and assign PRs from forks with issues: write. + # The workflow does not check out or execute pull request code. + - oscca-pr.yml:3 + excessive-permissions: + ignore: + # pull_request_target is needed to post PR comments with pull-requests: write. + # Workflow-level permissions: {} restricts defaults; only the job has write access. + - lib-deps-check.yaml:3 unpinned-uses: config: policies: diff --git a/.gitignore b/.gitignore index b5887be53b5..cb8548877c2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,9 @@ __pycache__/ .repl_history.txt .vscode/ wasm-pack.log -.idea/ +.idea/* +!.idea/icon.svg +!.idea/vcs.xml .envrc flame-graph.html @@ -27,4 +29,5 @@ Lib/site-packages/* Lib/test/data/* !Lib/test/data/README cpython/ -.claude/scheduled_tasks.lock \ No newline at end of file +.claude/ +docs/superpowers/ \ No newline at end of file diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 00000000000..84e5f593a6d --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000000..82bc0911775 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39481edaa9f..750f649403e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: priority: 0 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.16 + rev: v0.16.2 hooks: - id: ruff-format priority: 0 @@ -42,7 +42,7 @@ repos: - id: generate-rs-opcode-metadata name: generate rust opcode metadata - entry: python tools/opcode_metadata/generate_rs_opcode_metadata.py + entry: python3 tools/opcode_metadata/generate_rs_opcode_metadata.py files: '^(crates/compiler-core/src/bytecode/instruction\.rs|tools/opcode_metadata/*)$' pass_filenames: false language: system @@ -53,7 +53,7 @@ repos: - id: generate-py-opcode-metadata name: generate python opcode metadata - entry: python tools/opcode_metadata/generate_py_opcode_metadata.py + entry: python3 tools/opcode_metadata/generate_py_opcode_metadata.py files: '^(crates/compiler-core/src/bytecode/instruction\.rs|tools/opcode_metadata/*)$' pass_filenames: false language: system @@ -63,7 +63,7 @@ repos: - manual - repo: https://github.com/streetsidesoftware/cspell-cli - rev: v10.0.0 + rev: v10.0.1 hooks: - id: cspell types: [rust] @@ -77,7 +77,7 @@ repos: priority: 0 - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.8.3 + rev: v3.9.6 hooks: - id: prettier files: '^wasm/.*$' diff --git a/.python-version b/.python-version index 3f0a10fda70..a128d5c0d97 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14.6 +3.14.7 diff --git a/AGENTS.md b/AGENTS.md index c89b2a4d3a4..694fc7be65c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# GitHub Copilot Instructions for RustPython +# AI Agent Instructions for RustPython -This document provides guidelines for working with GitHub Copilot when contributing to the RustPython project. +This document provides guidelines for AI coding agents (GitHub Copilot, Claude Code, Gemini, etc.) contributing to the RustPython project. ## Project Overview @@ -13,36 +13,26 @@ RustPython is a Python 3 interpreter written in Rust, implementing Python 3.14.0 ## Repository Structure -- `src/` - Top-level code for the RustPython binary -- `vm/` - The Python virtual machine implementation - - `builtins/` - Python built-in types and functions - - `stdlib/` - Essential standard library modules implemented in Rust, required to run the Python core -- `compiler/` - Python compiler components - - `parser/` - Parser for converting Python source to AST - - `core/` - Bytecode representation in Rust structures - - `codegen/` - AST to bytecode compiler -- `Lib/` - CPython's standard library in Python (copied from CPython). **IMPORTANT**: Do not edit this directory directly; The only allowed operation is copying files from CPython. -- `derive/` - Rust macros for RustPython -- `common/` - Common utilities -- `extra_tests/` - Integration tests and snippets -- `stdlib/` - Non-essential Python standard library modules implemented in Rust (useful but not required for core functionality) -- `wasm/` - WebAssembly support -- `jit/` - Experimental JIT compiler implementation -- `pylib/` - Python standard library packaging (do not modify this directory directly - its contents are generated automatically) +See the "Code organization" section in [CONTRIBUTING.md](CONTRIBUTING.md#code-organization) for the current directory layout. ## AI Agent Rules +**CRITICAL: AI Policy** + +- Follow RustPython's [AI Policy](https://github.com/RustPython/.github/blob/main/AI_POLICY.md) for every AI-assisted contribution. +- Disclose AI assistance in commit messages with an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer. Use one trailer per AI tool, and never use `Co-authored-by` for an AI assistant. + **CRITICAL: Git Operations** - NEVER create pull requests directly without explicit user permission - NEVER push commits to remote without explicit user permission - Always ask the user before performing any git operations that affect the remote repository - Commits can be created locally when requested, but pushing and PR creation require explicit approval -**CRITICAL: Pre-commit Checks** -- Before creating ANY commit, you MUST run `prek run --all-files` (or `pre-commit run --all-files`) AND the full test suite. Both must pass — do not commit if either fails. -- Test commands are documented in the [Testing](#testing) section below. At minimum run `cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher`; if the change touches `extra_tests/snippets/` run `pytest -v` there too, and if it touches `Lib/` or interpreter behavior, run the relevant `cargo run --release -- -m test ` modules. -- If a hook auto-fixes files (e.g. `ruff-format`, `rustfmt`), re-stage the fixes, re-run `prek` until it reports a clean pass, then re-run the tests, then commit. -- NEVER bypass these checks with `--no-verify`, `--no-gpg-sign`, or by skipping tests "because the change is small". If a hook or test fails, fix the underlying issue and create a new commit — do not amend or force the failing commit through. +**CRITICAL: Commit Hooks and Validation** +- Install the repository's pre-commit hook with `prek install` (or `pre-commit install`) after cloning the repository. +- Every commit must run the configured pre-commit hook. NEVER bypass it with `--no-verify`. Automated workflows that use a normal `git commit`, such as `scripts/update_lib quick`, should be allowed to create local commits through the hook. +- If a hook auto-fixes files (e.g. `ruff-format`, `rustfmt`), re-stage the fixes and retry the commit. Do not amend or force a failing commit through. +- Before completing a task, run the tests appropriate for the change. Test commands are documented in the [Testing](#testing) section below. At minimum run `cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi`, then run `cargo test` from `crates/capi`; if the change touches `extra_tests/snippets/` run `pytest -v` there too, and if it touches `Lib/` or interpreter behavior, run the relevant `cargo run --release -- -m test ` modules. ## Important Development Notes @@ -128,7 +118,10 @@ rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r ```bash # Run Rust unit tests -cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher +cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi + +# Run C-API tests from their directory so their separate Cargo config applies +(cd crates/capi && cargo test) # Run Python snippets tests (debug mode recommended for faster compilation) cargo run -- extra_tests/snippets/builtin_bytes.py @@ -265,12 +258,10 @@ cargo run --features jit ### Linux Build and Debug on macOS -See the "Testing on Linux from macOS" section in [DEVELOPMENT.md](DEVELOPMENT.md#testing-on-linux-from-macos). +See the "Testing on Linux from macOS" section in [CONTRIBUTING.md](CONTRIBUTING.md#testing-on-linux-from-macos). ### Building venvlauncher (Windows) -See DEVELOPMENT.md "CPython Version Upgrade Checklist" section. - **IMPORTANT**: All 4 venvlauncher binaries use the same source code. Do NOT add multiple `[[bin]]` entries to Cargo.toml. Build once and copy with different names. ## Test Code Modification Rules @@ -301,7 +292,7 @@ If you modify any file under `.github/workflows/`, the change must pass a [zizmo ## Documentation - Check the [architecture document](/architecture/architecture.md) for a high-level overview -- Read the [development guide](/DEVELOPMENT.md) for detailed setup instructions +- Read the [development guide](/CONTRIBUTING.md) for detailed setup instructions - Generate documentation with `cargo doc --no-deps --all` - Online documentation is available at [docs.rs/rustpython](https://docs.rs/rustpython/) - [How to update test files](https://github.com/RustPython/RustPython/wiki/How-to-update-test-files#checkout-cpython-source-code-initial-setup) — guide for syncing test cases from upstream CPython into the `Lib/` directory diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58954486eaf..637ecf0993a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,7 +89,15 @@ $ pytest -v Rust unit tests can be run with `cargo`: ```shell -$ cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher +$ cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi +``` + +`rustpython-capi` needs to be tested from inside its own directory, since it has a +separate `cargo` config that only applies there: + +```shell +$ cd crates/capi +$ cargo test ``` Python unit tests can be run by compiling RustPython and running the test module: diff --git a/Cargo.lock b/Cargo.lock index 2f916ef8cdb..fa5e7b52236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,15 +14,39 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", ] [[package]] @@ -49,15 +73,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anes" version = "0.1.6" @@ -116,9 +131,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "approx" @@ -131,9 +146,9 @@ dependencies = [ [[package]] name = "ar_archive_writer" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" dependencies = [ "object", ] @@ -152,9 +167,9 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -174,7 +189,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -186,7 +201,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -209,7 +224,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -225,34 +240,35 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-fips-sys" -version = "0.13.14" +version = "0.13.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d619165468401dec3caa3366ebffbcb83f2f31883e5b3932f8e2dec2ddc568" +checksum = "6c0e6249c249b8916c98ebae7bc06216c8dcab3002f32872b4abe642d17063b1" dependencies = [ "bindgen 0.72.1", "cc", "cmake", "dunce", "fs_extra", + "pkg-config", "regex", ] [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -261,14 +277,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -289,7 +306,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -299,8 +316,8 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", - "syn", + "shlex 1.3.0", + "syn 2.0.119", ] [[package]] @@ -309,7 +326,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -319,8 +336,8 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", - "syn", + "shlex 1.3.0", + "syn 2.0.119", ] [[package]] @@ -331,9 +348,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitflagset" @@ -353,23 +370,14 @@ version = "0.11.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" dependencies = [ - "digest 0.11.3", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", + "digest", ] [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -385,20 +393,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ "allocator-api2", ] @@ -411,9 +419,9 @@ checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "bzip2" @@ -450,14 +458,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -483,28 +491,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.1", ] -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -538,8 +533,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.0", - "crypto-common 0.2.2", + "block-buffer", + "crypto-common", "inout", ] @@ -599,9 +594,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "collection_literals" @@ -627,9 +622,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -641,9 +636,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -660,12 +655,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -704,32 +693,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-models" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" -dependencies = [ - "hax-lib", - "pastey", - "rand 0.9.4", -] - [[package]] name = "cpubits" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -741,9 +710,9 @@ dependencies = [ [[package]] name = "cranelift" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5c702984722ad27d12c532df8467890f922bf5b1932286906dd2bb6779982c" +checksum = "69c8702ad42c0aac8d585f1c3ffe8039bcd996898615c8948a1943e0c9661232" dependencies = [ "cranelift-codegen", "cranelift-frontend", @@ -752,27 +721,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c4ebb31662e2051dcc49b7342d222405a99e951720756cc4b93315972abd67" +checksum = "3d521bdbc6098937af83ef4ab6d5c07398126bc71878f7ef4ea9499977978ef5" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dfc2ec96ec1c3a8a250602e936712e00a381df032f7a8ad175c8f768c03bb" +checksum = "3dde0b83164d4a497860af4236271178bc640512b067101e0853e4f74eaa4df5" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5694aa8a2eb2571a15b3feee38d16ccaf2712200e7b5c9ae0479069bdfb46949" +checksum = "0111d110b72b4efad69a372e29e21628652fd0bcab66967e5c8350ab679affd5" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -780,18 +749,18 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab349d30a5fad9699440ee7ccb435374e8a8735dcca26696a4245bcefcc47e" +checksum = "cf01ecc92fc5499789d79c3b817299d5a8ddd828d31dcf0f9cc3cc66f38dbb36" dependencies = [ "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e95970bdb51d145c828a114a1084cb8b63e65569a51600ad398cb49fa78b062" +checksum = "6cd2563bead0090c3879a7ff7327f7550c9d59bb5d05ecc8cd7886977aa3125a" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -803,7 +772,7 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libm", "log", "regalloc2", @@ -816,9 +785,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8414b8ecc81f89f8a3f2c5cbc785b9ed690200cd6f9d780e96a92f88879704" +checksum = "9640d250d26f9381a73dc7f9862b27928d4809119aec73be0e556612e67b6a99" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -828,24 +797,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631c4e5db42e6a0f9e7a68f18f7faba3692862114870c7c598aee7e0e5677e59" +checksum = "a07f156b90efc94371ddb3536f76e4ab671ad2e093bfa5511e10198cadbb0c47" [[package]] name = "cranelift-control" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c6e92c825abfbb739a4beaa5db3988f98a96a68d6ea656f562098efc142976" +checksum = "d75a76fd9dd37dcbc3d2d2e00abe3281a7e88adc035fba3b8114cc69981576ec" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e57cd185782abada9ab2606bfe88d0abc0d42d83a7d432dcf69991e17fe76e" +checksum = "2eea22522144ba08c7e7ef94bfc25d474ef016c2771974b8ab1986734ac85965" dependencies = [ "cranelift-bitset", "wasmtime-internal-core", @@ -853,9 +822,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0f17e48d15e29552e2f264d302c31a661a830196d879bbdaf4d7b2bf2f7011" +checksum = "efa2826c80dff1d93b19b3cfbcaf9fae44c78d739a98d146dd5bd89c51e14367" dependencies = [ "cranelift-codegen", "log", @@ -865,15 +834,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "407b80b46934c9dce9a6581f0e80d079b7a69e11372fb03d7dce4c7ba3fee4e3" +checksum = "e238a69b95c5415456f22189494a12db94f313bb72b6ec9cc88ddf2f1056e28e" [[package]] name = "cranelift-jit" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea72887b62db0e7c387f8c0b7af600772fd9190c6f7205fd2182b0ce10159c80" +checksum = "a7def01f2b14f97421558d1db33e396b97b97ab2ed40dedc8165ba00d13088e6" dependencies = [ "anyhow", "cranelift-codegen", @@ -892,9 +861,9 @@ dependencies = [ [[package]] name = "cranelift-module" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd3b5369ba0f409b9b218e2356a71285c6e2815e187329a59db09228fd927c4" +checksum = "985df7eceefc91cb75bf7f51b32b7f1739d0fc0e25ddf023b1de407cb3c93d77" dependencies = [ "anyhow", "cranelift-codegen", @@ -903,9 +872,9 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a40e056e421d9a6c757983f2184f765ae1c28a51555d3877e98afc97ce5705b" +checksum = "eceb0ebd8d6aef6bb287e8d532a164b0db1c0062961211e9c22a91f67521c098" dependencies = [ "cranelift-codegen", "libc", @@ -914,9 +883,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.132.1" +version = "0.132.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cdbadda21e49798825a1ec795dab30bcb03235891f662b5ccf23fa45b39682f" +checksum = "004643f39a7bec553de5263d650db30e5b9caec1d5cbe065fd732b7ec9ae40d0" [[package]] name = "crc32fast" @@ -964,9 +933,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -974,18 +943,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -993,16 +962,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -1021,6 +980,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -1037,26 +1005,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] -name = "der" -version = "0.7.10" +name = "defmt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" dependencies = [ - "const-oid 0.9.6", - "der_derive", - "flagset", - "pem-rfc7468 0.7.0", - "zeroize", + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", ] [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", "zeroize", ] @@ -1076,13 +1064,13 @@ dependencies = [ [[package]] name = "der_derive" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1090,9 +1078,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive-where" @@ -1102,17 +1087,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", + "syn 2.0.119", ] [[package]] @@ -1121,9 +1096,9 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", - "const-oid 0.10.2", - "crypto-common 0.2.2", + "block-buffer", + "const-oid", + "crypto-common", "ctutils", ] @@ -1150,13 +1125,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1185,9 +1160,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -1203,9 +1178,9 @@ checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" [[package]] name = "env_filter" -version = "1.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -1213,9 +1188,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -1291,7 +1266,7 @@ checksum = "7693d9dd1ec1c54f52195dfe255b627f7cec7da33b679cd56de949e662b3db10" dependencies = [ "flame", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1322,12 +1297,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1379,16 +1348,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "get-size-derive2" version = "0.7.4" @@ -1397,7 +1356,7 @@ checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1457,20 +1416,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + [[package]] name = "gimli" version = "0.33.0" @@ -1491,9 +1457,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "graviola" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4387e0458389da24c6fe732531e65595c7c4a32b027f98f4789e512e28224465" +checksum = "e8596c4fa98466aae2fcf4c72a665bc0e021c0aaab1e47d82044d3dc3e309a76" dependencies = [ "cfg-if", "getrandom 0.3.4", @@ -1510,68 +1476,22 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.2.0", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hax-lib" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" -dependencies = [ - "hax-lib-macros", - "num-bigint", - "num-traits", -] - -[[package]] -name = "hax-lib-macros" -version = "0.3.6" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "hax-lib-macros-types", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "hax-lib-macros-types" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_json", - "uuid", + "foldhash", ] [[package]] @@ -1604,7 +1524,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -1618,37 +1538,13 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_casemap" version = "2.2.0" @@ -1781,12 +1677,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "indexmap" version = "2.14.0" @@ -1794,9 +1684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", + "hashbrown 0.17.1", ] [[package]] @@ -1811,9 +1699,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", @@ -1836,7 +1724,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1863,6 +1751,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1871,26 +1768,57 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ + "defmt", + "jiff-core", "jiff-static", + "jiff-tzdb-platform", + "js-sys", "log", "portable-atomic", "portable-atomic-util", "serde_core", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", ] [[package]] @@ -1920,7 +1848,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -1939,28 +1867,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1981,7 +1908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", ] [[package]] @@ -1996,12 +1923,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lexical-parse-float" version = "1.0.6" @@ -2035,85 +1956,21 @@ checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413" [[package]] name = "libbz2-rs-sys" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libcrux-intrinsics" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5" -dependencies = [ - "core-models", - "hax-lib", -] - -[[package]] -name = "libcrux-ml-kem" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d" -dependencies = [ - "hax-lib", - "libcrux-intrinsics", - "libcrux-platform", - "libcrux-secrets", - "libcrux-sha3", - "libcrux-traits", -] - -[[package]] -name = "libcrux-platform" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" -dependencies = [ - "libc", -] - -[[package]] -name = "libcrux-secrets" -version = "0.0.5" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" -dependencies = [ - "hax-lib", -] - -[[package]] -name = "libcrux-sha3" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7" -dependencies = [ - "hax-lib", - "libcrux-intrinsics", - "libcrux-platform", - "libcrux-traits", -] - -[[package]] -name = "libcrux-traits" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f" -dependencies = [ - "libcrux-secrets", - "rand 0.9.4", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libffi" -version = "5.1.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0498fe5655f857803e156523e644dcdcdc3b3c7edda42ea2afdae2e09b2db87b" +checksum = "ed185dbb87539a100c1b36c219e16e71572c6d4d4fed3ded898140f755adeaaf" dependencies = [ "libc", "libffi-sys", @@ -2121,9 +1978,9 @@ dependencies = [ [[package]] name = "libffi-sys" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d4f1d4ce15091955144350b75db16a96d4a63728500122706fb4d29a26afbb" +checksum = "25831b230b6a90bdea9f28339c1d00d59773a1c492e8ca09b1ad80e56394c261" dependencies = [ "cc", ] @@ -2156,9 +2013,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ "libc", ] @@ -2176,9 +2033,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.6.3" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1116a951fd9d5110720bb2ae66f72ce5d7b10bd8aa8744924677157089ce13f" +checksum = "03dcace986b149f29509af6ca70e6182bccce916b644424ecf484faa8ddc899a" dependencies = [ "zlib-rs", ] @@ -2206,9 +2063,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lz4_flex" @@ -2240,9 +2097,9 @@ dependencies = [ [[package]] name = "malachite-base" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8b6f86fdbb1eb9955946be91775239dfcb0acdb1a51bb07d5fc9b8c854f5ccd" +checksum = "c6b9d4679f346f85a8f466d0171478304dab8b0e944dd38086411ab5f6100a17" dependencies = [ "hashbrown 0.16.1", "itertools 0.14.0", @@ -2252,9 +2109,9 @@ dependencies = [ [[package]] name = "malachite-bigint" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fcd6e504ffc67db2b3c6d5e90e08054646e2b04f42115a5460bf1c1e37d3bc" +checksum = "5064cf3abe01ff3b80b0349936ebad6c52f7c793182d9c7992bf79ece18c0d22" dependencies = [ "malachite-base", "malachite-nz", @@ -2265,9 +2122,9 @@ dependencies = [ [[package]] name = "malachite-nz" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0197a2f5cfee19d59178e282985c6ca79a9233e26a2adcf40acb693896aa09f6" +checksum = "8a6821ab988221c35d421ba16c4f8dca101efe5ae1cbfa9831f1fdb1596c755e" dependencies = [ "itertools 0.14.0", "libm", @@ -2277,11 +2134,12 @@ dependencies = [ [[package]] name = "malachite-q" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2add95162aede090c48f0ee51bea7d328847ce3180aa44588111f846cc116b" +checksum = "3cf7894cd9617e43ef5d9824633f7dfc1bffd0298880dd668f20bdffb7a6e8ea" dependencies = [ "itertools 0.14.0", + "libm", "malachite-base", "malachite-nz", ] @@ -2295,7 +2153,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2316,14 +2174,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest", ] [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -2336,9 +2194,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -2392,7 +2250,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2405,7 +2263,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2424,9 +2282,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2443,15 +2301,15 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2493,7 +2351,7 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2538,7 +2396,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -2554,7 +2412,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2565,9 +2423,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.6.0+3.6.2" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] @@ -2629,7 +2487,7 @@ dependencies = [ [[package]] name = "parking_lot_core" version = "0.9.12" -source = "git+https://github.com/youknowone/parking_lot?branch=rustpython#4392edbe879acc9c0dd94eda53d2205d3ab912c9" +source = "git+https://github.com/youknowone/parking_lot?branch=rustpython#f4ee53a7b803354a8f0a6de2f28e93fc141240bf" dependencies = [ "cfg-if", "libc", @@ -2644,31 +2502,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pastey" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" - [[package]] name = "pbkdf2" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.3", + "digest", "hmac", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -2689,12 +2532,12 @@ dependencies = [ [[package]] name = "phf" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" dependencies = [ "phf_macros", - "phf_shared 0.13.1", + "phf_shared 0.14.0", "serde", ] @@ -2720,25 +2563,25 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared 0.14.0", ] [[package]] name = "phf_macros" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator 0.14.0", + "phf_shared 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2752,9 +2595,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" dependencies = [ "siphasher", ] @@ -2767,18 +2610,19 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" dependencies = [ "aes", + "aes-gcm", "cbc", - "der 0.8.0", + "der", "pbkdf2", "rand_core 0.10.1", "scrypt", "sha2", - "spki 0.8.0", + "spki", ] [[package]] @@ -2787,10 +2631,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.0", + "der", "pkcs5", "rand_core 0.10.1", - "spki 0.8.0", + "spki", ] [[package]] @@ -2835,7 +2679,18 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures", + "universal-hash", ] [[package]] @@ -2886,29 +2741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2924,18 +2757,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" dependencies = [ "ar_archive_writer", "cc", @@ -2957,9 +2790,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ "libc", "once_cell", @@ -2971,18 +2804,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -2990,33 +2823,33 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "pyo3-macros-backend" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3040,7 +2873,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3081,49 +2914,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] [[package]] name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core 0.6.4", ] [[package]] @@ -3135,15 +2948,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rand_core" version = "0.10.1" @@ -3152,9 +2956,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" dependencies = [ "rustversion", ] @@ -3191,7 +2995,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3222,7 +3026,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3233,7 +3037,7 @@ checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "log", "rustc-hash", "smallvec", @@ -3241,9 +3045,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -3264,9 +3068,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "region" @@ -3298,7 +3102,7 @@ dependencies = [ "pmutil", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3317,9 +3121,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3345,7 +3149,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -3354,9 +3158,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -3368,12 +3172,11 @@ dependencies = [ [[package]] name = "rustls-graviola" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323c712e50c59ceb2ba9ad4d79dcfd3e0046a082d61efa87fcdf8f59af04473c" +checksum = "bf5d0a370be690f7f1aa4e1b912fde3f5f9a5c53285062fd2d6ac1a52ab3e883" dependencies = [ "graviola", - "libcrux-ml-kem", "rustls", ] @@ -3400,9 +3203,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "zeroize", ] @@ -3475,10 +3278,13 @@ dependencies = [ name = "rustpython-capi" version = "0.5.0" dependencies = [ - "bitflags 2.13.0", - "itertools 0.14.0", + "bitflags 2.13.1", + "itertools 0.15.0", + "libc", + "malachite-bigint", "num-complex", "pyo3", + "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", ] @@ -3487,9 +3293,9 @@ dependencies = [ name = "rustpython-codegen" version = "0.5.0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "indexmap", - "itertools 0.14.0", + "itertools 0.15.0", "log", "malachite-bigint", "memchr", @@ -3501,9 +3307,9 @@ dependencies = [ "rustpython-ruff_python_ast", "rustpython-ruff_python_parser", "rustpython-ruff_text_size", + "rustpython-unicode", "rustpython-wtf8", "thiserror", - "unicode_names2 2.0.0", ] [[package]] @@ -3511,9 +3317,9 @@ name = "rustpython-common" version = "0.5.0" dependencies = [ "ascii", - "bitflags 2.13.0", - "getrandom 0.4.2", - "itertools 0.14.0", + "bitflags 2.13.1", + "getrandom 0.4.3", + "itertools 0.15.0", "libc", "lock_api", "malachite-base", @@ -3524,9 +3330,9 @@ dependencies = [ "parking_lot", "radium", "rustpython-literal", + "rustpython-unicode", "rustpython-wtf8", "siphasher", - "unicode_names2 2.0.0", ] [[package]] @@ -3546,12 +3352,13 @@ dependencies = [ name = "rustpython-compiler-core" version = "0.5.0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bitflagset", - "itertools 0.14.0", + "itertools 0.15.0", "lz4_flex", "malachite-bigint", "num-complex", + "num-traits", "rustpython-ruff_source_file", "rustpython-wtf8", ] @@ -3570,19 +3377,19 @@ version = "0.5.0" dependencies = [ "rustpython-compiler", "rustpython-derive-impl", - "syn", + "syn 2.0.119", ] [[package]] name = "rustpython-derive-impl" version = "0.5.0" dependencies = [ - "itertools 0.14.0", + "itertools 0.15.0", "proc-macro2", "quote", "rustpython-compiler-core", "rustpython-doc", - "syn", + "syn 2.0.119", "syn-ext", "textwrap", ] @@ -3591,20 +3398,25 @@ dependencies = [ name = "rustpython-doc" version = "0.5.0" dependencies = [ - "phf 0.13.1", + "phf 0.14.0", ] [[package]] name = "rustpython-host_env" version = "0.5.0" dependencies = [ - "bitflags 2.13.0", - "getrandom 0.4.2", + "bitflags 2.13.1", + "cc", + "dns-lookup", + "gethostname", + "getrandom 0.4.3", "junction", "libc", "libffi", "libloading 0.9.0", - "memmap2 0.9.10", + "mac_address", + "memchr", + "memmap2 0.9.11", "nix 0.31.3", "num-traits", "num_cpus", @@ -3612,8 +3424,12 @@ dependencies = [ "paste", "rustix", "rustpython-wtf8", + "rustyline", "schannel", + "socket2", + "system-configuration", "termios", + "which", "widestring", "windows-sys 0.61.2", ] @@ -3639,11 +3455,11 @@ name = "rustpython-literal" version = "0.5.0" dependencies = [ "hexf-parse", - "icu_properties", "is-macro", "lexical-parse-float", "num-traits", - "rand 0.10.1", + "rand 0.10.2", + "rustpython-unicode", "rustpython-wtf8", ] @@ -3658,12 +3474,11 @@ dependencies = [ [[package]] name = "rustpython-ruff_python_ast" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f021ff72cabf5e2cd6d8ec8813d376a8445a228dc610ab56c27bd9054cda70d4" +version = "0.15.9" +source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af" dependencies = [ "aho-corasick", - "bitflags 2.13.0", + "bitflags 2.13.1", "compact_str", "get-size2", "is-macro", @@ -3677,11 +3492,10 @@ dependencies = [ [[package]] name = "rustpython-ruff_python_parser" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e6ee78bd9671fb5766664b2695fe1f2a92a961f4d9101646c570d8acdb1e0b" +version = "0.15.9" +source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", "compact_str", "get-size2", @@ -3698,9 +3512,8 @@ dependencies = [ [[package]] name = "rustpython-ruff_python_trivia" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e7cfd1056f3a02ff0d2d0e4474286ca963260782f878b7b81c1dd87432e682" +version = "0.15.9" +source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af" dependencies = [ "itertools 0.14.0", "rustpython-ruff_source_file", @@ -3710,9 +3523,8 @@ dependencies = [ [[package]] name = "rustpython-ruff_source_file" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "948107aad62ddb12a11fc7bf68a49e52a0b0a3737d415a2505e54f5a9edac737" +version = "0.15.9" +source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af" dependencies = [ "memchr", "rustpython-ruff_text_size", @@ -3720,9 +3532,8 @@ dependencies = [ [[package]] name = "rustpython-ruff_text_size" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8291ee0f5a779e54ccd4e0151a0c426f8b49a123f99b5b6545db17ccdd4277aa" +version = "0.15.9" +source = "git+https://github.com/RustPython/ruff.git?tag=0.15.19-rustpython#3c1ab2dd9987f190f3df94d28452ed5e65e106af" dependencies = [ "get-size2", ] @@ -3731,11 +3542,11 @@ dependencies = [ name = "rustpython-sre_engine" version = "0.5.0" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "criterion", - "icu_properties", "num_enum", "optional", + "rustpython-unicode", "rustpython-wtf8", ] @@ -3748,30 +3559,25 @@ dependencies = [ "base64", "blake2", "bzip2", - "chrono", "constant_time_eq", "crc32fast", "crossbeam-utils", "csv-core", - "der 0.8.0", - "digest 0.11.3", - "dns-lookup", + "der", + "digest", "dyn-clone", "flame", "flate2", "foreign-types-shared", - "gethostname", "hex", "hmac", - "icu_normalizer", - "icu_properties", "indexmap", "insta", - "itertools 0.14.0", + "itertools 0.15.0", + "jiff", "libc", "libsqlite3-sys", "libz-rs-sys", - "mac_address", "malachite-bigint", "md-5", "memchr", @@ -3786,11 +3592,11 @@ dependencies = [ "parking_lot", "paste", "pbkdf2", - "pem-rfc7468 1.0.0", - "phf 0.13.1", + "pem-rfc7468", + "phf 0.14.0", "pkcs8", "pymath", - "rand 0.10.1", + "rand 0.10.2", "rapidhash", "rustls", "rustls-native-certs", @@ -3804,16 +3610,15 @@ dependencies = [ "rustpython-ruff_python_parser", "rustpython-ruff_source_file", "rustpython-ruff_text_size", + "rustpython-unicode", "rustpython-vm", - "sha1 0.11.0", + "scopeguard", + "sha1", "sha2", "sha3", "shake", - "socket2", - "system-configuration", "tcl-sys", "tk-sys", - "unicode_names2 2.0.0", "uuid", "webpki-roots", "widestring", @@ -3824,6 +3629,19 @@ dependencies = [ "xz-sys", ] +[[package]] +name = "rustpython-unicode" +version = "0.5.0" +dependencies = [ + "icu_casemap", + "icu_locale", + "icu_normalizer", + "icu_properties", + "rustpython-wtf8", + "unicode_names2 3.1.0", + "writeable", +] + [[package]] name = "rustpython-venvlauncher" version = "0.5.0" @@ -3833,9 +3651,8 @@ name = "rustpython-vm" version = "0.5.0" dependencies = [ "ascii", - "bitflags 2.13.0", + "bitflags 2.13.1", "bstr", - "chrono", "constant_time_eq", "crossbeam-utils", "exitcode", @@ -3844,12 +3661,11 @@ dependencies = [ "glob", "half", "hex", - "icu_casemap", - "icu_locale", - "icu_properties", "indexmap", "is-macro", - "itertools 0.14.0", + "itertools 0.15.0", + "itoa", + "jiff", "libc", "log", "malachite-bigint", @@ -3876,18 +3692,16 @@ dependencies = [ "rustpython-ruff_python_parser", "rustpython-ruff_text_size", "rustpython-sre_engine", - "rustyline", + "rustpython-unicode", "scopeguard", "serde_core", "static_assertions", "strum", "strum_macros", + "thin-vec", "thiserror", - "timsort", "wasm-bindgen", - "which", "widestring", - "writeable", ] [[package]] @@ -3896,7 +3710,7 @@ version = "0.5.0" dependencies = [ "ascii", "bstr", - "itertools 0.14.0", + "itertools 0.15.0", "memchr", ] @@ -3908,6 +3722,7 @@ dependencies = [ "js-sys", "rustpython-common", "rustpython-pylib", + "rustpython-ruff_text_size", "rustpython-stdlib", "rustpython-vm", "serde-wasm-bindgen", @@ -3919,17 +3734,17 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rustyline" -version = "18.0.0" +version = "18.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "clipboard-win", "home", @@ -4011,7 +3826,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4072,14 +3887,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4097,17 +3912,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.11.0" @@ -4115,8 +3919,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4126,8 +3930,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4136,7 +3940,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest 0.11.3", + "digest", "keccak", "sponge-cursor", ] @@ -4147,7 +3951,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ - "digest 0.11.3", + "digest", "keccak", "sponge-cursor", ] @@ -4166,13 +3970,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signature" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", ] [[package]] @@ -4217,9 +4027,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" @@ -4231,16 +4041,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0" @@ -4248,7 +4048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der", ] [[package]] @@ -4284,7 +4084,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4295,9 +4095,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -4312,7 +4123,7 @@ checksum = "b126de4ef6c2a628a68609dd00733766c3b015894698a438ebdf374933fc31d1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4323,7 +4134,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4332,7 +4143,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4369,7 +4180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4390,24 +4201,30 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" + [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] @@ -4423,12 +4240,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -4438,26 +4254,20 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", ] -[[package]] -name = "timsort" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639ce8ef6d2ba56be0383a94dd13b92138d58de44c62618303bb798fa92bdc00" - [[package]] name = "tinystr" version = "0.8.3" @@ -4521,7 +4331,7 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4571,9 +4381,9 @@ checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -4592,9 +4402,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -4602,12 +4412,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unicode_names2" version = "1.3.0" @@ -4620,12 +4424,12 @@ dependencies = [ [[package]] name = "unicode_names2" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0" +checksum = "82c3e18d850bb6ebd57735e5654f0af65e572b05c2397e7b2b1a7c6a792cc29c" dependencies = [ "phf 0.11.3", - "unicode_names2_generator 2.0.0", + "unicode_names2_generator 3.1.0", ] [[package]] @@ -4642,14 +4446,24 @@ dependencies = [ [[package]] name = "unicode_names2_generator" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1262662dc96937c71115228ce2e1d30f41db71a7a45d3459e98783ef94052214" +checksum = "849744a58c479122ff24910d7ca57f312b62613ef0412dc327776ae6b235d16a" dependencies = [ "phf_codegen", "rand 0.8.6", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4676,12 +4490,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", - "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -4716,27 +4529,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4747,9 +4551,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4757,9 +4561,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4767,75 +4571,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wasmtime-internal-core" -version = "45.0.1" +version = "45.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110bf85122cd451d3b9ff67f8911d428ec9b729208abe950a0333c3244660e88" +checksum = "3073c03f97f871fe6400e68621c863b93ba79296f8e570285231952d23fcc804" dependencies = [ - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libm", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "45.0.1" +version = "45.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6818a4864719772680694f4e4649a8600bb5efcf71111ebaf7419b266463e8" +checksum = "37dee05e8c35759826f6b926cd949c51f771b6a58619d8e60c63c3f3d3e5e59b" dependencies = [ "cfg-if", "libc", @@ -4845,9 +4615,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4855,36 +4625,36 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] [[package]] name = "which" -version = "8.0.3" +version = "8.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c789537cf2f7f55be8e6192f92e464174ee55f91af622777f7f1ceb0dbccd03e" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" dependencies = [ "libc", ] [[package]] name = "wide" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9479f84a757f819cfab37295955906479181395de83add28f74975fde083141" +checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89" dependencies = [ "bytemuck", "safe_arch", @@ -4927,65 +4697,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -5144,9 +4861,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "winresource" @@ -5158,100 +4875,12 @@ dependencies = [ "version_check", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "write16" version = "1.0.0" @@ -5266,15 +4895,15 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-cert" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" dependencies = [ - "const-oid 0.9.6", - "der 0.7.10", - "sha1 0.10.6", + "const-oid", + "der", + "sha1", "signature", - "spki 0.7.3", + "spki", "tls_codec", ] @@ -5333,9 +4962,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5350,35 +4979,35 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -5391,28 +5020,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -5447,14 +5076,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zlib-rs" -version = "0.6.3" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" diff --git a/Cargo.toml b/Cargo.toml index 280b64f01e5..3b489a88687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ env_logger = "0.11" flamescope = { version = "0.1.2", optional = true } rustls = { workspace = true, optional = true } -rustls-graviola = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] libc = { workspace = true } @@ -60,6 +59,7 @@ rustyline = { workspace = true } [dev-dependencies] criterion = { workspace = true } pyo3 = { workspace = true, features = ["auto-initialize"] } +rustls-graviola = { workspace = true } rustpython-stdlib = { workspace = true } ruff_python_parser = { workspace = true } @@ -79,7 +79,6 @@ path = "src/main.rs" name = "custom_tls_providers" path = "examples/custom_tls_providers.rs" required-features = [ - "rustls-graviola", "rustls/ring", "rustpython-pylib/freeze-stdlib", "rustpython-stdlib/ssl-rustls", @@ -180,35 +179,27 @@ rustpython-vm = { path = "crates/vm", default-features = false, version = "0.5.0 rustpython-pylib = { path = "crates/pylib", version = "0.5.0" } rustpython-stdlib = { path = "crates/stdlib", default-features = false, version = "0.5.0" } rustpython-sre_engine = { path = "crates/sre_engine", version = "0.5.0" } +rustpython-unicode = { path = "crates/unicode", version = "0.5.0" } rustpython-wtf8 = { path = "crates/wtf8", version = "0.5.0" } rustpython-doc = { path = "crates/doc", version = "0.5.0" } -# Use RustPython-packaged Ruff crates from the published fork while keeping -# existing crate names in the codebase. -ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.15.8" } -ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.15.8" } -ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.15.8" } -ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8" } -# To update ruff crates, comment out the above lines and uncomment the following lines to pull directly from the Ruff repository at the specified commit hash. -# Ruff tag 0.15.8 is based on commit c2a8815842f9dc5d24ec19385eae0f1a7188b0d9 -# at the time of this capture. We use the commit hash to ensure reproducible builds. -# ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } -# ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } -# ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } -# ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } +# Use the RustPython Ruff fork for RustPython public `_ast` metadata. +ruff_python_parser = { package = "rustpython-ruff_python_parser", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" } +ruff_python_ast = { package = "rustpython-ruff_python_ast", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" } +ruff_text_size = { package = "rustpython-ruff_text_size", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" } +ruff_source_file = { package = "rustpython-ruff_source_file", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" } der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] } -phf = { version = "0.13.1", default-features = false, features = ["macros"]} +phf = { version = "0.14.0", default-features = false, features = ["macros"]} adler32 = "1.2.0" approx = "0.5.1" -ascii = "1.1" +ascii = { version = "1.1", default-features = false } base64 = "0.22" blake2 = "0.11.0-rc.6" bitflags = "2.11.0" bitflagset = "0.0.3" -bstr = "1" +bstr = { version = "1", default-features = false, features = ["unicode"] } bzip2 = "0.6" -chrono = { version = "0.4.44", default-features = false, features = ["clock", "std"] } console_error_panic_hook = "0.1" constant_time_eq = "0.5" cranelift = "0.132.0" @@ -236,8 +227,10 @@ hexf-parse = "0.2.1" hmac = "0.13" indexmap = { version = "2.14.0", features = ["std"] } insta = "1.47" -itertools = "0.14.0" +itertools = { version = "0.15.0", default-features = false, features = ["use_alloc"] } +itoa = "1" is-macro = "0.3.7" +jiff = "0.2" js-sys = "0.3" junction = "2.0.0" lexical-parse-float = "1.0.6" @@ -253,11 +246,11 @@ log = "0.4.30" lz4_flex = "0.13" nix = { version = "0.31", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] } mac_address = "1.1.3" -malachite-bigint = "0.9.1" -malachite-q = "0.9.1" -malachite-base = "0.9.1" +malachite-bigint = "0.10.0" +malachite-q = "0.10.0" +malachite-base = "0.10.0" md-5 = "0.11" -memchr = "2.8.1" +memchr = { version = "2.8.1", default-features = false, features = ["alloc"] } memmap2 = "0.9.10" mt19937 = "3.3" num-complex = "0.4.6" @@ -286,7 +279,7 @@ rapidhash = "4.4.1" result-like = "0.5.0" rustix = { version = "1.1", features = ["event", "fs", "param", "system"] } rustls = { version = "0.23.39", default-features = false } -rustls-graviola = "0.3" +rustls-graviola = "0.4" rustls-native-certs = "0.8" rustls-pemfile = "2.2" rustls-platform-verifier = "0.7" @@ -311,14 +304,14 @@ tcl-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" textwrap = { version = "0.16.2", default-features = false } termios = "0.3.3" thiserror = "2.0" -timsort = "0.1.2" +thin-vec = "0.2.14" tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" } icu_casemap = "2" icu_locale = "2" icu_properties = "2" icu_normalizer = "2" uuid = "1.23.2" -unicode_names2 = "2.0.0" +unicode_names2 = { version = "3", default-features = false, features = ["no_std"] } widestring = "1.2.0" windows-sys = "0.61.2" wasm-bindgen = "0.2.106" @@ -326,7 +319,7 @@ wasm-bindgen-futures = "0.4" web-sys = "0.3" webpki-roots = "1.0" which = "8" -x509-cert = "0.2.5" +x509-cert = "0.3.0" x509-parser = "0.18" xml = "1.3" writeable = "0.6" @@ -357,12 +350,14 @@ similar_names = "allow" # restriction lints alloc_instead_of_core = "warn" cfg_not_test = "warn" +iter_over_hash_type = "warn" redundant_test_prefix = "warn" std_instead_of_alloc = "warn" std_instead_of_core = "warn" tests_outside_test_module = "warn" # nursery lints to enforce gradually +collection_is_never_read = "warn" debug_assert_with_mut_call = "warn" derive_partial_eq_without_eq = "warn" imprecise_flops = "warn" @@ -376,19 +371,26 @@ search_is_some = "warn" significant_drop_in_scrutinee = "warn" single_option_map = "warn" trait_duplication_in_bounds = "warn" +tuple_array_conversions = "warn" +type_repetition_in_bounds = "warn" +unnecessary_struct_initialization = "warn" unused_peekable = "warn" unused_rounding = "warn" use_self = "warn" useless_let_if_seq = "warn" +while_float = "warn" # pedantic lints to enforce gradually +assigning_clones = "warn" bool_to_int_with_if = "warn" checked_conversions = "warn" cloned_instead_of_copied = "warn" collapsible_else_if = "warn" comparison_chain = "warn" +copy_iterator = "warn" doc_link_with_quotes = "warn" duration_suboptimal_units = "warn" +elidable_lifetime_names = "warn" enum_glob_use = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" @@ -404,6 +406,8 @@ ip_constant = "warn" iter_filter_is_ok = "warn" iter_filter_is_some = "warn" large_futures = "warn" +large_types_passed_by_value = "warn" +manual_assert = "warn" manual_instant_elapsed = "warn" manual_is_variant_and = "warn" map_unwrap_or = "warn" @@ -421,7 +425,9 @@ range_plus_one = "warn" redundant_else = "warn" ref_option = "warn" return_self_not_must_use = "warn" +same_functions_in_if_condition = "warn" single_char_pattern = "warn" +trivially_copy_pass_by_ref = "warn" unchecked_time_subtraction = "warn" uninlined_format_args = "warn" unnecessary_box_returns = "warn" diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 3d3bbd7a39a..803de0c6792 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -57,8 +57,7 @@ try: from _collections import defaultdict except ImportError: - # TODO: RUSTPYTHON - implement defaultdict in Rust - from ._defaultdict import defaultdict + pass heapq = None # Lazily imported diff --git a/Lib/collections/_defaultdict.py b/Lib/collections/_defaultdict.py deleted file mode 100644 index cb9d403c8ad..00000000000 --- a/Lib/collections/_defaultdict.py +++ /dev/null @@ -1,62 +0,0 @@ -from reprlib import recursive_repr as _recursive_repr - -class defaultdict(dict): - def __init__(self, *args, **kwargs): - if len(args) >= 1: - default_factory = args[0] - if default_factory is not None and not callable(default_factory): - raise TypeError("first argument must be callable or None") - args = args[1:] - else: - default_factory = None - super().__init__(*args, **kwargs) - self.default_factory = default_factory - - def __missing__(self, key): - if self.default_factory is not None: - val = self.default_factory() - else: - raise KeyError(key) - # CPython parity: a recursive __missing__ via factory() may have - # already populated key; preserve that value instead of overwriting. - if key in self: - return self[key] - self[key] = val - return val - - @_recursive_repr() - def __repr_factory(factory): - return repr(factory) - - def __repr__(self): - return f"{type(self).__name__}({defaultdict.__repr_factory(self.default_factory)}, {dict.__repr__(self)})" - - def copy(self): - return type(self)(self.default_factory, self) - - __copy__ = copy - - def __reduce__(self): - if self.default_factory is not None: - args = self.default_factory, - else: - args = () - return type(self), args, None, None, iter(self.items()) - - def __or__(self, other): - if not isinstance(other, dict): - return NotImplemented - - new = defaultdict(self.default_factory, self) - new.update(other) - return new - - def __ror__(self, other): - if not isinstance(other, dict): - return NotImplemented - - new = defaultdict(self.default_factory, other) - new.update(self) - return new - -defaultdict.__module__ = 'collections' diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index a8040457abf..7d13414ff82 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -10,7 +10,7 @@ __all__ = ["version", "bootstrap"] -_PIP_VERSION = "26.1.1" +_PIP_VERSION = "26.1.2" # Directory of system wheel packages. Some Linux distribution packaging # policies recommend against bundling dependencies. For example, Fedora diff --git a/Lib/ensurepip/_bundled/pip-26.1.1-py3-none-any.whl b/Lib/ensurepip/_bundled/pip-26.1.2-py3-none-any.whl similarity index 93% rename from Lib/ensurepip/_bundled/pip-26.1.1-py3-none-any.whl rename to Lib/ensurepip/_bundled/pip-26.1.2-py3-none-any.whl index ab0307c7716..24b5cc90ace 100644 Binary files a/Lib/ensurepip/_bundled/pip-26.1.1-py3-none-any.whl and b/Lib/ensurepip/_bundled/pip-26.1.2-py3-none-any.whl differ diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index c3a9805988f..9cbe6dc641e 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6791,7 +6791,6 @@ def _test_dict(cls, obj): obj.clear() case.assertEqual(len(obj), 0) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_dict(self): o = self.manager.dict() o['foo'] = 5 diff --git a/Lib/test/signalinterproctester.py b/Lib/test/signalinterproctester.py index 168b5da0f2c..073c078f45f 100644 --- a/Lib/test/signalinterproctester.py +++ b/Lib/test/signalinterproctester.py @@ -1,9 +1,11 @@ +import gc import os import signal import subprocess import sys import time import unittest +from test import support class SIGUSR1Exception(Exception): @@ -27,16 +29,15 @@ def wait_signal(self, child, signame): # (if set) child.wait() - timeout = 10.0 - deadline = time.monotonic() + timeout - - while time.monotonic() < deadline: + start_time = time.monotonic() + for _ in support.busy_retry(support.SHORT_TIMEOUT, error=False): if self.got_signals[signame]: return signal.pause() - - self.fail('signal %s not received after %s seconds' - % (signame, timeout)) + else: + dt = time.monotonic() - start_time + self.fail('signal %s not received after %.1f seconds' + % (signame, dt)) def subprocess_send_signal(self, pid, signame): code = 'import os, signal; os.kill(%s, signal.%s)' % (pid, signame) @@ -59,6 +60,13 @@ def test_interprocess_signal(self): self.assertEqual(self.got_signals, {'SIGHUP': 1, 'SIGUSR1': 0, 'SIGALRM': 0}) + # gh-110033: Make sure that the subprocess.Popen is deleted before + # the next test which raises an exception. Otherwise, the exception + # may be raised when Popen.__del__() is executed and so be logged + # as "Exception ignored in: ". + child = None + gc.collect() + with self.assertRaises(SIGUSR1Exception): with self.subprocess_send_signal(pid, "SIGUSR1") as child: self.wait_signal(child, 'SIGUSR1') diff --git a/Lib/test/ssltests.py b/Lib/test/ssltests.py new file mode 100644 index 00000000000..ee03aed5cca --- /dev/null +++ b/Lib/test/ssltests.py @@ -0,0 +1,37 @@ +# Convenience test module to run all of the OpenSSL-related tests in the +# standard library. + +import ssl +import sys +import subprocess + +TESTS = [ + 'test_asyncio', 'test_ensurepip.py', 'test_ftplib', 'test_hashlib', + 'test_hmac', 'test_httplib', 'test_imaplib', + 'test_poplib', 'test_ssl', 'test_smtplib', 'test_smtpnet', + 'test_urllib2_localnet', 'test_venv', 'test_xmlrpc' +] + +def run_regrtests(*extra_args): + print(ssl.OPENSSL_VERSION) + args = [ + sys.executable, + '-Werror', '-bb', # turn warnings into exceptions + '-m', 'test', + ] + if not extra_args: + args.extend([ + '-r', # randomize + '-w', # re-run failed tests with -v + '-u', 'network', # use network + '-u', 'urlfetch', # download test vectors + '-j', '0' # use multiple CPUs + ]) + else: + args.extend(extra_args) + args.extend(TESTS) + result = subprocess.call(args) + sys.exit(result) + +if __name__ == '__main__': + run_regrtests(*sys.argv[1:]) diff --git a/Lib/test/string_tests.py b/Lib/test/string_tests.py index 08926bf88f8..0c159e02fb9 100644 --- a/Lib/test/string_tests.py +++ b/Lib/test/string_tests.py @@ -482,11 +482,8 @@ def test_expandtabs(self): self.checkequal(' a\n b', ' \ta\n\tb', 'expandtabs', 1) self.checkraises(TypeError, 'hello', 'expandtabs', 42, 42) - # TODO: RUSTPYTHON; expandtabs overflow checks - # if sys.maxsize < (1 << 32) and struct.calcsize('P') == 4: - # # This test is only valid when sizeof(int) == sizeof(void*) == 4. - if sys.maxsize < (1 << 32) and struct.calcsize('P') == 4 and False: + if sys.maxsize < (1 << 32) and struct.calcsize('P') == 4: self.checkraises(OverflowError, '\ta\n\tb', 'expandtabs', sys.maxsize) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index d300337b915..13df6134882 100644 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -189,7 +189,6 @@ def test_numbers(self): self.assertEqual(a, b, msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' def test_unicode(self): teststr = "Bonne Journ\xe9e \U0002030a\U00020347" testcases = ( @@ -1180,7 +1179,6 @@ def test_sizeof_without_buffer(self): basesize = support.calcvobjsize('Pn2Pi') support.check_sizeof(self, a, basesize) - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' def test_initialize_with_unicode(self): if self.typecode not in ('u', 'w'): with self.assertRaises(TypeError) as cm: @@ -1267,207 +1265,11 @@ def test_empty_string_mem_leak_gh140474(self): self.assertEqual(len(a), 0) self.assertEqual(a.typecode, 'u') - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_add(self): - return super().test_add() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_extend(self): - return super().test_extend() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_iadd(self): - return super().test_iadd() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_setiadd(self): - return super().test_setiadd() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_setslice(self): - return super().test_setslice() - class UCS4Test(UnicodeTest): typecode = 'w' minitemsize = 4 - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_buffer(self): - return super().test_buffer() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_buffer_info(self): - return super().test_buffer_info() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_byteswap(self): - return super().test_byteswap() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_clear(self): - return super().test_clear() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_cmp(self): - return super().test_cmp() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_constructor(self): - return super().test_constructor() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_constructor_with_iterable_argument(self): - return super().test_constructor_with_iterable_argument() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_copy(self): - return super().test_copy() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_count(self): - return super().test_count() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_coveritertraverse(self): - return super().test_coveritertraverse() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_deepcopy(self): - return super().test_deepcopy() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_delitem(self): - return super().test_delitem() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_exhausted_iterator(self): - return super().test_exhausted_iterator() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_exhausted_reverse_iterator(self): - return super().test_exhausted_reverse_iterator() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_extended_getslice(self): - return super().test_extended_getslice() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_extended_set_del_slice(self): - return super().test_extended_set_del_slice() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_filewrite(self): - return super().test_filewrite() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_fromarray(self): - return super().test_fromarray() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_fromfile_ioerror(self): - return super().test_fromfile_ioerror() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_getitem(self): - return super().test_getitem() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_getslice(self): - return super().test_getslice() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_imul(self): - return super().test_imul() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_index(self): - return super().test_index() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_insert(self): - return super().test_insert() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_issue17223(self): - return super().test_issue17223() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_iterator_pickle(self): - return super().test_iterator_pickle() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_len(self): - return super().test_len() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_mul(self): - return super().test_mul() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_pickle(self): - return super().test_pickle() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_pickle_for_empty_array(self): - return super().test_pickle_for_empty_array() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_pop(self): - return super().test_pop() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_reduce_ex(self): - return super().test_reduce_ex() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_remove(self): - return super().test_remove() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_repr(self): - return super().test_repr() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_reverse(self): - return super().test_reverse() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_reverse_iterator(self): - return super().test_reverse_iterator() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_reverse_iterator_picking(self): - return super().test_reverse_iterator_picking() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_setitem(self): - return super().test_setitem() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_str(self): - return super().test_str() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_tofrombytes(self): - return super().test_tofrombytes() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_tofromfile(self): - return super().test_tofromfile() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_tofromlist(self): - return super().test_tofromlist() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_unicode(self): - return super().test_unicode() - - @unittest.expectedFailure # TODO: RUSTPYTHON; Add support for 'w' - def test_weakref(self): - return super().test_weakref() - class NumberTest(BaseTest): diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 00283ca05a0..699a5ec0b04 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -150,7 +150,6 @@ def test_parse_invalid_ast(self): self.assertRaises(TypeError, ast.parse, ast.Constant(42), optimize=optval) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags def test_optimization_levels__debug__(self): cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)] for (optval, expected) in cases: @@ -586,7 +585,6 @@ def test_invalid_sum(self): compile(m, "", "exec") self.assertIn("but got expr()", str(cm.exception)) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: expected str for name def test_invalid_identifier(self): m = ast.Module([ast.Expr(ast.Name(42, ast.Load()))], []) ast.fix_missing_locations(m) @@ -1333,7 +1331,6 @@ class MyNode(ast.AST): self.assertEqual(repl.x, 0) self.assertEqual(repl.y, y) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'x' is not 'x' def test_replace_ignore_known_custom_instance_fields(self): node = ast.parse('x').body[0].value node.extra = extra = object() # add instance 'extra' field @@ -1365,7 +1362,6 @@ def test_replace_ignore_known_custom_instance_fields(self): self.assertIs(repl.ctx, context) self.assertRaises(AttributeError, getattr, repl, 'extra') - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "Name\.__replace__\ missing\ 1\ keyword\ argument:\ 'id'\." does not match "replace() does not support Name objects" def test_replace_reject_missing_field(self): # case: warn if deleted field is not replaced node = ast.parse('x').body[0].value @@ -1404,7 +1400,6 @@ def test_replace_accept_missing_field_with_default(self): self.assertIs(node2.returns, None) self.assertEqual(node2.decorator_list, []) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "Name\.__replace__\ got\ an\ unexpected\ keyword\ argument\ 'extra'\." does not match "replace() does not support Name objects" def test_replace_reject_known_custom_instance_fields_commits(self): node = ast.parse('x').body[0].value node.extra = extra = object() # add instance 'extra' field @@ -1420,7 +1415,6 @@ def test_replace_reject_known_custom_instance_fields_commits(self): self.assertIs(node.ctx, context) self.assertIs(node.extra, extra) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "Name\.__replace__\ got\ an\ unexpected\ keyword\ argument\ 'unknown'\." does not match "replace() does not support Name objects" def test_replace_reject_unknown_instance_fields(self): node = ast.parse('x').body[0].value context = node.ctx @@ -1700,7 +1694,6 @@ def check_text(code, empty, full, **kwargs): full="Module(body=[Import(names=[alias(name='_ast', asname='ast')]), ImportFrom(module='module', names=[alias(name='sub')], level=0)], type_ignores=[])", ) - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^ def test_copy_location(self): src = ast.parse('1 + 1', mode='eval') src.body.right = ast.copy_location(ast.Constant(2), src.body.right) @@ -1737,7 +1730,6 @@ def test_fix_missing_locations(self): "end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)])" ) - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ def test_increment_lineno(self): src = ast.parse('1 + 1', mode='eval') self.assertEqual(ast.increment_lineno(src, n=3), src) @@ -1896,7 +1888,6 @@ def test_literal_eval(self): self.assertRaises(ValueError, ast.literal_eval, '+True') self.assertRaises(ValueError, ast.literal_eval, '2+3') - @unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError not raised def test_literal_eval_str_int_limit(self): with support.adjust_int_max_str_digits(4000): ast.literal_eval('3'*4000) # no error @@ -1959,7 +1950,6 @@ def test_literal_eval_syntax_errors(self): (\ \ ''') - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: required field "lineno" missing from alias def test_bad_integer(self): # issue13436: Bad error message with invalid numeric values body = [ast.ImportFrom(module='time', @@ -2064,7 +2054,6 @@ def arguments(args=None, posonlyargs=None, vararg=None, kw_defaults=[None, ast.Name("x", ast.Store())]), "must have Load context") - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError not raised def test_funcdef(self): a = ast.arguments([], [], None, [], [], None, []) f = ast.FunctionDef("x", a, [], [], None, None, []) @@ -2276,7 +2265,6 @@ def test_unaryop(self): u = ast.UnaryOp(ast.Not(), ast.Name("x", ast.Store())) self.expr(u, "must have Load context") - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError not raised def test_lambda(self): a = ast.arguments([], [], None, [], [], None, []) self.expr(ast.Lambda(a, ast.Name("x", ast.Store())), @@ -3259,7 +3247,6 @@ class MyAttrs(ast.AST): r"MyAttrs.__init__ got an unexpected keyword argument 'c'."): obj = MyAttrs(c=3) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_fields_and_types_no_default(self): class FieldsAndTypesNoDefault(ast.AST): _fields = ('a',) @@ -3273,7 +3260,6 @@ class FieldsAndTypesNoDefault(ast.AST): obj = FieldsAndTypesNoDefault(a=1) self.assertEqual(obj.a, 1) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_incomplete_field_types(self): class MoreFieldsThanTypes(ast.AST): _fields = ('a', 'b') @@ -3293,7 +3279,6 @@ class MoreFieldsThanTypes(ast.AST): self.assertEqual(obj.a, 1) self.assertEqual(obj.b, 2) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'str' but 'bytes' found. def test_malformed_fields_with_bytes(self): class BadFields(ast.AST): _fields = (b'\xff'*64,) @@ -3713,7 +3698,6 @@ def assert_ast(self, code, non_optimized_target, optimized_target): f"{ast.dump(optimized_tree)}", ) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: compile() unrecognized flags def test_folding_format(self): code = "'%s' % (a,)" diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 92895bbb420..1b727f3b1fe 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1019,7 +1019,6 @@ async def iter_one(): asyncio.create_task(iter_one()) return status - @unittest.expectedFailure # TODO: RUSTPYTHON; - GC doesn't finalize async generators def test_asyncgen_finalization_by_gc(self): # Async generators should be finalized when garbage collected. self.loop._process_events = mock.Mock() @@ -1035,7 +1034,6 @@ def test_asyncgen_finalization_by_gc(self): test_utils.run_briefly(self.loop) self.assertTrue(status['finalized']) - @unittest.expectedFailure # TODO: RUSTPYTHON; - GC doesn't finalize async generators def test_asyncgen_finalization_by_gc_in_other_thread(self): # Python issue 34769: If garbage collector runs in another # thread, async generators will not finalize in debug diff --git a/Lib/test/test_asyncio/test_ssl.py b/Lib/test/test_asyncio/test_ssl.py index 932b1dace4f..ca15fc3bdd4 100644 --- a/Lib/test/test_asyncio/test_ssl.py +++ b/Lib/test/test_asyncio/test_ssl.py @@ -738,7 +738,6 @@ async def client(addr): asyncio.wait_for(client(srv.addr), timeout=support.SHORT_TIMEOUT)) - @unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly def test_create_connection_memory_leak(self): HELLO_MSG = b'1' * self.PAYLOAD_SIZE @@ -1617,7 +1616,6 @@ async def test(): else: self.fail('Unexpected ResourceWarning: {}'.format(cm.warning)) - @unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly def test_handshake_timeout_handler_leak(self): s = socket.socket(socket.AF_INET) s.bind(('127.0.0.1', 0)) diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index d01d36ad3db..690a6e7434e 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -77,7 +77,6 @@ def test_monkeypatch(self): def test_open(self): self.do_test("test_open", os_helper.TESTFN) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_cantrace(self): self.do_test("test_cantrace") diff --git a/Lib/test/test_baseexception.py b/Lib/test/test_baseexception.py index 5870dc7f9da..0c206c4e3bd 100644 --- a/Lib/test/test_baseexception.py +++ b/Lib/test/test_baseexception.py @@ -79,10 +79,8 @@ def test_inheritance(self): # Underscore-prefixed (private) exceptions don't need to be documented exc_set = set(e for e in exc_set if not e.startswith('_')) - # RUSTPYTHON specific - exc_set.discard("JitError") - # XXX: RUSTPYTHON; IncompleteInputError will be officially introduced in Python 3.15 - exc_set.discard("IncompleteInputError") + exc_set.discard("JitError") # XXX: RUSTPYTHON specific + exc_set.discard("IncompleteInputError") # XXX: RUSTPYTHON; IncompleteInputError will be officially introduced in Python 3.15 self.assertEqual(len(exc_set), 0, "%s not accounted for" % exc_set) interface_tests = ("length", "args", "str", "repr") diff --git a/Lib/test/test_bigmem.py b/Lib/test/test_bigmem.py index 8f528812e35..12b221a66e3 100644 --- a/Lib/test/test_bigmem.py +++ b/Lib/test/test_bigmem.py @@ -9,7 +9,12 @@ """ from test import support -from test.support import bigmemtest, _1G, _2G, _4G +from test.support import bigmemtest, _1G, _2G, _4G, import_helper +# _testcapi = import_helper.import_module('_testcapi') +try: # TODO: RUSTPYTHON + import _testcapi +except ImportError: + _testcapi = None import unittest import operator @@ -784,17 +789,6 @@ def test_title(self, size): def test_swapcase(self, size): self._test_swapcase(size) - # TODO: RUSTPYTHON - @unittest.expectedFailure - @bigmemtest(size=_2G, memuse=2) - def test_isspace(self, size): - super().test_isspace(size) - - # TODO: RUSTPYTHON - @unittest.expectedFailure - @bigmemtest(size=_2G, memuse=2) - def test_istitle(self, size): - super().test_istitle(size) class BytearrayTest(unittest.TestCase, BaseStrTest): @@ -821,18 +815,6 @@ def test_swapcase(self, size): test_hash = None test_split_large = None - # TODO: RUSTPYTHON - @unittest.expectedFailure - @bigmemtest(size=_2G, memuse=2) - def test_isspace(self, size): - super().test_isspace(size) - - # TODO: RUSTPYTHON - @unittest.expectedFailure - @bigmemtest(size=_2G, memuse=2) - def test_istitle(self, size): - super().test_istitle(size) - class TupleTest(unittest.TestCase): # Tuples have a small, fixed-sized head and an array of pointers to @@ -1280,6 +1262,27 @@ def test_dict(self, size): d[size] = 1 +class ImmortalityTest(unittest.TestCase): + + @bigmemtest(size=_2G, memuse=pointer_size * 9/8) + def test_stickiness(self, size): + """Check that immortality is "sticky", so that + once an object is immortal it remains so.""" + if size < _2G: + # Not enough memory to cause immortality on overflow + return + o1 = o2 = o3 = o4 = o5 = o6 = o7 = o8 = object() + l = [o1] * (size-20) + self.assertFalse(_testcapi.is_immortal(o1)) + for _ in range(30): + l.append(l[0]) + self.assertTrue(_testcapi.is_immortal(o1)) + del o2, o3, o4, o5, o6, o7, o8 + self.assertTrue(_testcapi.is_immortal(o1)) + del l + self.assertTrue(_testcapi.is_immortal(o1)) + + if __name__ == '__main__': if len(sys.argv) > 1: support.set_memlimit(sys.argv[1]) diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index cd8acb4c2cb..48631cecec7 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -222,7 +222,6 @@ def assertInvalidLength(data): assertInvalidLength(b'a' * (4 * 87 + 1)) assertInvalidLength(b'A\tB\nC ??DE') # only 5 valid characters - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Error not raised by a2b_uu def test_uu(self): MAX_UU = 45 for backtick in (True, False): @@ -445,7 +444,6 @@ def test_b2a_qp_a2b_qp_round_trip(self, binary, quotetabs, istext, header): self.assertConversion(binary, converted, restored, quotetabs=quotetabs, istext=istext, header=header) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Error not raised by a2b_uu def test_empty_string(self): # A test for SF bug #1022953. Make sure SystemError is not raised. empty = self.type2test(b'') diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index bc09329e6de..19582e75716 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -4471,7 +4471,6 @@ def test_flags_overflow(self): class TestPythonBufferProtocol(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_basic(self): class MyBuffer: def __buffer__(self, flags): @@ -4500,7 +4499,6 @@ def __buffer__(self): self.assertRaises(TypeError, memoryview, WrongArity()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_release_buffer(self): class WhatToRelease: def __init__(self): @@ -4523,7 +4521,6 @@ def __release_buffer__(self, buffer): self.assertEqual(mv.tobytes(), b"hello") self.assertFalse(wr.held) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_same_buffer_returned(self): class WhatToRelease: def __init__(self): @@ -4549,7 +4546,6 @@ def __release_buffer__(self, buffer): self.assertEqual(mv.tobytes(), b"hello") self.assertFalse(wr.held) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_buffer_flags(self): class PossiblyMutable: def __init__(self, data, mutable) -> None: @@ -4589,7 +4585,6 @@ def __buffer__(self, flags): mv[0] = ord(b'x') self.assertEqual(mv.tobytes(), b"hello") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_call_builtins(self): ba = bytearray(b"hello") mv = ba.__buffer__(0) @@ -4651,7 +4646,6 @@ def __buffer__(self, flags): mv = memoryview(a) self.assertEqual(mv.tobytes(), b"hello") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_inheritance_releasebuffer(self): rb_call_count = 0 class B(bytearray): @@ -4668,7 +4662,6 @@ def __release_buffer__(self, view): self.assertEqual(rb_call_count, 0) self.assertEqual(rb_call_count, 1) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_inherit_but_return_something_else(self): class A(bytearray): def __buffer__(self, flags): @@ -4708,7 +4701,6 @@ def __release_buffer__(self, buffer): with memoryview(c) as mv: self.assertEqual(mv.tobytes(), b"hello") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_release_saves_reference(self): smuggled_buffer = None @@ -4736,7 +4728,6 @@ def __release_buffer__(s, buffer: memoryview): with self.assertRaises(ValueError): smuggled_buffer.tobytes() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_release_saves_reference_no_subclassing(self): ba = bytearray(b"hello") @@ -4757,7 +4748,6 @@ def __release_buffer__(self, buffer): c.buffer.release() ba.clear() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_multiple_inheritance_buffer_last(self): class A: def __buffer__(self, flags): @@ -4817,7 +4807,6 @@ def __buffer__(self, flags): c.clear() self.assertIs(c.buffer, None) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_release_buffer_with_exception_set(self): class A: def __buffer__(self, flags): diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 163ebcfb5bd..10783cf33e2 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -486,7 +486,6 @@ def test_compile_top_level_await_no_coro(self): msg=f"source={source} mode={mode}") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_compile_top_level_await(self): """Test whether code with top level await can be compiled. @@ -627,7 +626,6 @@ def test_compile_async_generator(self): exec(co, glob) self.assertEqual(type(glob['ticker']()), AsyncGeneratorType) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <_ast.Name object at 0xb40000731e3d1360> is not an instance of def test_compile_ast(self): args = ("a*__debug__", "f.py", "exec") raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0] @@ -1020,7 +1018,6 @@ def test_exec_redirected(self): finally: sys.stdout = savestdout - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument closure def test_exec_closure(self): def function_without_closures(): return 3 * 5 diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 70af9af466d..32a9ca7df87 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -518,7 +518,6 @@ def test_hex(self): self.assertEqual(self.type2test(b"\x1a\x2b\x30").hex(), '1a2b30') self.assertEqual(memoryview(b"\x1a\x2b\x30").hex(), '1a2b30') - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument sep def test_hex_separator_basics(self): three_bytes = self.type2test(b'\xb9\x01\xef') self.assertEqual(three_bytes.hex(), 'b901ef') @@ -2102,7 +2101,6 @@ def test_bytes_repr(self, f=repr): self.assertEqual(f(b"'\"'"), r"""b'\'"\''""") # '\'"\'' self.assertEqual(f(BytesSubclass(b"abc")), "b'abc'") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bytearray_repr(self, f=repr): self.assertEqual(f(bytearray()), "bytearray(b'')") self.assertEqual(f(bytearray(b'abc')), "bytearray(b'abc')") @@ -2124,7 +2122,6 @@ def test_bytearray_repr(self, f=repr): def test_bytes_str(self): self.test_bytes_repr(str) - @unittest.expectedFailure # TODO: RUSTPYTHON @check_bytes_warnings def test_bytearray_str(self): self.test_bytearray_repr(str) diff --git a/Lib/test/test_bz2.py b/Lib/test/test_bz2.py index a7e152fb7e7..bcd4e033b59 100644 --- a/Lib/test/test_bz2.py +++ b/Lib/test/test_bz2.py @@ -936,7 +936,6 @@ def testPickle(self): with self.assertRaises(TypeError): pickle.dumps(BZ2Decompressor(), proto) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 100 def testDecompressorChunksMaxsize(self): bzd = BZ2Decompressor() max_length = 100 diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 8b8c452f676..16df318ae8e 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -645,7 +645,6 @@ def test_syntaxerror_indented_caret_position(self): self.assertNotIn("\f", text) self.assertIn("\n 1 + 1 = 2\n ^^^^^\n", text) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_syntaxerror_multi_line_fstring(self): script = 'foo = f"""{}\nfoo"""\n' with os_helper.temp_dir() as script_dir: diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index 39d85d46274..fb519878cd8 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -128,7 +128,6 @@ def test_indentation_error(self): self.assertIsNone(self.sysmod.last_value.__traceback__) self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'UnicodeDecodeError: invalid utf-8 sequence of 1 bytes from index 1\n\nnow exiti [truncated]... doesn't start with 'UnicodeEncodeError: ' def test_unicode_error(self): self.infunc.side_effect = ["'\ud800'", EOFError('Finished')] self.console.interact() diff --git a/Lib/test/test_codeccallbacks.py b/Lib/test/test_codeccallbacks.py index 763146c94fc..28ddf0a63b0 100644 --- a/Lib/test/test_codeccallbacks.py +++ b/Lib/test/test_codeccallbacks.py @@ -1067,8 +1067,7 @@ def test_decodehelper_bug36819(self): decoded = input.decode(enc, "test.bug36819") self.assertEqual(decoded, 'abcdx' * 51) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailureIf(sys.platform != "win32", "TODO: RUSTPYTHON") def test_encodehelper_bug36819(self): handler = RepeatedPosReturn() codecs.register_error("test.bug36819", handler.handle) diff --git a/Lib/test/test_codecmaps_kr.py b/Lib/test/test_codecmaps_kr.py index b8376d36615..a6409239dc5 100644 --- a/Lib/test/test_codecmaps_kr.py +++ b/Lib/test/test_codecmaps_kr.py @@ -11,7 +11,7 @@ class TestCP949Map(multibytecodec_support.TestBase_Mapping, encoding = 'cp949' mapfileurl = 'http://www.pythontest.net/unicode/CP949.TXT' - @unittest.expectedFailure # TODO: RUSTPYTHON; LookupError: unknown encoding: cp949 + @unittest.expectedFailureIf(__import__("sys").platform in ("android", "darwin", "linux"), "TODO: RUSTPYTHON; LookupError: unknown encoding: cp949") def test_mapping_file(self): return super().test_mapping_file() diff --git a/Lib/test/test_codecmaps_tw.py b/Lib/test/test_codecmaps_tw.py index 4a1359ce7be..2fcf59c9f6d 100644 --- a/Lib/test/test_codecmaps_tw.py +++ b/Lib/test/test_codecmaps_tw.py @@ -27,7 +27,7 @@ class TestCP950Map(multibytecodec_support.TestBase_Mapping, (b"\xFFxy", "replace", "\ufffdxy"), ) - @unittest.expectedFailure # TODO: RUSTPYTHON; LookupError: unknown encoding: cp950 + @unittest.expectedFailureIf(__import__("sys").platform in ("android", "darwin", "linux"), "TODO: RUSTPYTHON; LookupError: unknown encoding: cp950") def test_errorhandle(self): return super().test_errorhandle() diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index 12976122241..2e1568d5ea2 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -279,7 +279,6 @@ def test_filename(self): self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename, compile("a = 1\n", "def", 'single').co_filename) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 2 def test_warning(self): # Test that the warning is only returned once. with warnings_helper.check_warnings( diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index b5d3411c71a..c1dadc4e274 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -1956,7 +1956,6 @@ class X(ByteString): pass # No metaclass conflict class Z(ByteString, Awaitable): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; Need to implement __buffer__ and __release_buffer__ (https://docs.python.org/3.13/reference/datamodel.html#emulating-buffer-types) def test_Buffer(self): for sample in [bytes, bytearray, memoryview]: self.assertIsInstance(sample(b"x"), Buffer) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 4d117be1b88..a6542b396cc 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -209,7 +209,6 @@ def test_literals_with_leading_zeroes(self): self.assertEqual(eval("0o777"), 511) self.assertEqual(eval("-0o0000010"), -8) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised def test_int_literals_too_long(self): n = 3000 source = f"a = 1\nb = 2\nc = {'3'*n}\nd = 4" @@ -283,7 +282,6 @@ def test_none_assignment(self): self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'single') self.assertRaises(SyntaxError, compile, stmt, 'tmp', 'exec') - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile def test_import(self): succeed = [ 'import sys', @@ -348,7 +346,6 @@ def test_lambda_consts(self): l = lambda: "this is the only const" self.assertEqual(l.__code__.co_consts, ("this is the only const",)) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile def test_encoding(self): code = b'# -*- coding: badencoding -*-\npass\n' self.assertRaises(SyntaxError, compile, code, 'tmp', 'exec') @@ -465,7 +462,6 @@ def test_condition_expression_with_dead_blocks_compiles(self): # See gh-113054 compile('if (5 if 5 else T): 0', '', 'exec') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_condition_expression_with_redundant_comparisons_compiles(self): # See gh-113054, gh-114083 exprs = [ @@ -580,7 +576,6 @@ def test_compile_redundant_jump_after_convert_pseudo_ops(self): compile(ast.fix_missing_locations(tree), "", "exec") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: at 0xb77555080 file "1", line 1> != at 0xb77554f00 file "3", line 1> def test_compile_ast(self): fname = __file__ if fname.lower().endswith('pyc'): @@ -696,7 +691,6 @@ def test_single_statement(self): self.compile_single("class T:\n pass") self.compile_single("c = '''\na=1\nb=2\nc=3\n'''") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised by compile_single def test_bad_single_statement(self): self.assertInvalidSingle('1\n2') self.assertInvalidSingle('def f(): pass') @@ -708,7 +702,6 @@ def test_bad_single_statement(self): self.assertInvalidSingle('x = 5 # comment\nx = 6\n') self.assertInvalidSingle("c = '''\nd=1\n'''\na = 1\n\nb = 2\n") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'source code cannot contain null bytes' not found in b'OSError: stream did not contain valid UTF-8\n' def test_particularly_evil_undecodable(self): # Issue 24022 src = b'0000\x00\n00000000000\n\x00\n\x9e\n' @@ -719,7 +712,6 @@ def test_particularly_evil_undecodable(self): res = script_helper.run_python_until_end(fn)[0] self.assertIn(b"source code cannot contain null bytes", res.err) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'source code cannot contain null bytes' not found in b'OSError: stream did not contain valid UTF-8\n' def test_yet_more_evil_still_undecodable(self): # Issue #25388 src = b"#\x00\n#\xfd\n" @@ -756,7 +748,6 @@ def check_limit(prefix, repeated, mode="single"): # check_limit("a", " if a else a") # check_limit("if a: pass", "\nelif a: pass", mode="exec") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot contain null" does not match "invalid syntax (, line 1)" def test_null_terminated(self): # The source code is null-terminated internally, but bytes-like # objects are accepted, which could be not terminated. @@ -1673,7 +1664,6 @@ class WeirdDict(dict): self.assertRaises(NameError, ns['foo']) - @unittest.expectedFailure # TODO: RUSTPYTHON; + [3, 5, 3, 5] def test_compile_warnings(self): # Each invocation of compile() emits compiler warnings, even if they # have the same message and line number. @@ -1691,7 +1681,6 @@ def test_compile_warnings(self): self.assertEqual([wm.lineno for wm in caught], [3, 5] * 2) - @unittest.expectedFailure # TODO: RUSTPYTHON; + [5, 9] def test_compile_warning_in_finally(self): # Ensure that warnings inside finally blocks are # only emitted once despite the block being @@ -1742,7 +1731,6 @@ def test_compile_warning_in_finally(self): self.assertEqual(wm.category, SyntaxWarning) self.assertIn("\"is\" with 'int' literal", str(wm.message)) - @unittest.expectedFailure # TODO: RUSTPYTHON @support.subTests('src', [ textwrap.dedent(""" def f(): diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 0e1f020d389..65093dc70c1 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -213,7 +213,6 @@ def test_write_bigfield(self): self._write_test([bigstring,bigstring], '%s,%s' % \ (bigstring, bigstring)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_write_quoting(self): self._write_test(['a',1,'p,q'], 'a,1,"p,q"') self._write_error_test(csv.Error, ['a',1,'p,q'], @@ -263,7 +262,6 @@ def test_write_escape(self): self._write_test(['C\\', '6', '7', 'X"'], 'C\\\\,6,7,"X"""', escapechar='\\', quoting=csv.QUOTE_MINIMAL) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_write_lineterminator(self): for lineterminator in '\r\n', '\n', '\r', '!@#', '\0': with self.subTest(lineterminator=lineterminator): @@ -277,7 +275,6 @@ def test_write_lineterminator(self): f'1,2{lineterminator}' f'"\r","\n"{lineterminator}') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_write_iterable(self): self._write_test(iter(['a', 1, 'p,q']), 'a,1,"p,q"') self._write_test(iter(['a', 1, None]), 'a,1,') @@ -320,7 +317,6 @@ def test_writerows_with_none(self): self.assertEqual(fileobj.read(), 'a\r\n""\r\n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_write_empty_fields(self): self._write_test((), '') self._write_test([''], '""') @@ -375,7 +371,6 @@ def _read_test(self, input, expect, **kwargs): result = list(reader) self.assertEqual(result, expect) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_oddinputs(self): self._read_test([], []) self._read_test([''], [[]]) @@ -386,7 +381,6 @@ def test_read_oddinputs(self): self.assertRaises(csv.Error, self._read_test, [b'abc'], None) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_eol(self): self._read_test(['a,b', 'c,d'], [['a','b'], ['c','d']]) self._read_test(['a,b\n', 'c,d\n'], [['a','b'], ['c','d']]) @@ -401,7 +395,6 @@ def test_read_eol(self): with self.assertRaisesRegex(csv.Error, errmsg): next(csv.reader(['a,b\r\nc,d'])) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_eof(self): self._read_test(['a,"'], [['a', '']]) self._read_test(['"a'], [['a']]) @@ -411,7 +404,6 @@ def test_read_eof(self): self.assertRaises(csv.Error, self._read_test, ['^'], [], escapechar='^', strict=True) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_nul(self): self._read_test(['\0'], [['\0']]) self._read_test(['a,\0b,c'], [['a', '\0b', 'c']]) @@ -424,7 +416,6 @@ def test_read_delimiter(self): self._read_test(['a;b;c'], [['a', 'b', 'c']], delimiter=';') self._read_test(['a\0b\0c'], [['a', 'b', 'c']], delimiter='\0') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_escape(self): self._read_test(['a,\\b,c'], [['a', 'b', 'c']], escapechar='\\') self._read_test(['a,b\\,c'], [['a', 'b,c']], escapechar='\\') @@ -437,7 +428,6 @@ def test_read_escape(self): self._read_test(['a,\\b,c'], [['a', '\\b', 'c']], escapechar=None) self._read_test(['a,\\b,c'], [['a', '\\b', 'c']]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_quoting(self): self._read_test(['1,",3,",5'], [['1', ',3,', '5']]) self._read_test(['1,",3,",5'], [['1', '"', '3', '"', '5']], @@ -474,7 +464,6 @@ def test_read_quoting(self): self._read_test(['1\\.5,\\.5,"\\.5"'], [[1.5, 0.5, ".5"]], quoting=csv.QUOTE_STRINGS, escapechar='\\') - @unittest.skip("TODO: RUSTPYTHON; slice index starts at 1 but ends at 0") def test_read_skipinitialspace(self): self._read_test(['no space, space, spaces,\ttab'], [['no space', 'space', 'spaces', '\ttab']], @@ -489,7 +478,6 @@ def test_read_skipinitialspace(self): [[None, None, None]], skipinitialspace=True, quoting=csv.QUOTE_STRINGS) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_space_delimiter(self): self._read_test(['a b', ' a ', ' ', ''], [['a', '', '', 'b'], ['', '', 'a', '', ''], ['', '', ''], []], @@ -529,7 +517,6 @@ def test_read_linenum(self): self.assertRaises(StopIteration, next, r) self.assertEqual(r.line_num, 3) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_roundtrip_quoteed_newlines(self): rows = [ ['\na', 'b\nc', 'd\n'], @@ -548,7 +535,6 @@ def test_roundtrip_quoteed_newlines(self): for i, row in enumerate(csv.reader(fileobj)): self.assertEqual(row, rows[i]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_roundtrip_escaped_unquoted_newlines(self): rows = [ ['\na', 'b\nc', 'd\n'], @@ -571,7 +557,6 @@ def test_roundtrip_escaped_unquoted_newlines(self): self.assertEqual(row, rows[i]) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Error not raised def test_reader_reentrant_iterator(self): # gh-145105: re-entering the reader from the iterator must not crash. class ReentrantIter: @@ -813,7 +798,6 @@ def test_quoted_quote(self): '"I see," said the blind man', 'as he picked up his hammer and saw']]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_quoted_nl(self): input = '''\ 1,2,3,"""I see,"" @@ -854,18 +838,15 @@ class EscapedExcel(csv.excel): class TestEscapedExcel(TestCsvBase): dialect = EscapedExcel() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_escape_fieldsep(self): self.writerAssertEqual([['abc,def']], 'abc\\,def\r\n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_escape_fieldsep(self): self.readerAssertEqual('abc\\,def\r\n', [['abc,def']]) class TestDialectUnix(TestCsvBase): dialect = 'unix' - @unittest.expectedFailure # TODO: RUSTPYTHON def test_simple_writer(self): self.writerAssertEqual([[1, 'abc def', 'abc']], '"1","abc def","abc"\n') @@ -882,7 +863,6 @@ class TestQuotedEscapedExcel(TestCsvBase): def test_write_escape_fieldsep(self): self.writerAssertEqual([['abc,def']], '"abc,def"\r\n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_escape_fieldsep(self): self.readerAssertEqual('"abc\\,def"\r\n', [['abc,def']]) @@ -1088,7 +1068,6 @@ def test_read_multi(self): "s1": 'abc', "s2": 'def'}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_read_with_blanks(self): reader = csv.DictReader(["1,2,abc,4,5,6\r\n","\r\n", "1,2,abc,4,5,6\r\n"], @@ -1140,7 +1119,6 @@ def test_float_write(self): fileobj.seek(0) self.assertEqual(fileobj.read(), expected) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_char_write(self): import array, string a = array.array('w', string.ascii_letters) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 1a6dc850ab9..96f42183296 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -1795,7 +1795,6 @@ class C: self.assertIsNot(d['f'], t) self.assertEqual(d['f'].my_a(), 6) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_helper_asdict_defaultdict(self): # Ensure asdict() does not throw exceptions when a # defaultdict is a member of a dataclass @@ -1938,7 +1937,6 @@ class C: t = astuple(c, tuple_factory=list) self.assertEqual(t, ['outer', T(1, ['inner', T(11, 12, 13)], 2)]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_helper_astuple_defaultdict(self): # Ensure astuple() does not throw exceptions when a # defaultdict is a member of a dataclass diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 92bf7998d75..0b19496ec4b 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4154,7 +4154,6 @@ class E(D): else: self.fail("shouldn't be able to create inheritance cycles") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_builtin_bases(self): # Make sure all the builtin types can have their base queried without # segfaulting. See issue #5787. @@ -4199,7 +4198,6 @@ class D(C): else: self.fail("best_base calculation found wanting") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_unsubclassable_types(self): with self.assertRaises(TypeError): class X(type(None)): diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 79c975946f7..e2a73773cc2 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -275,7 +275,6 @@ def __next__(self): self.assertRaises(ValueError, {}.update, [(1, 2, 3)]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_update_type_error(self): with self.assertRaises(TypeError) as cm: {}.update([object() for _ in range(3)]) diff --git a/Lib/test/test_dtrace.py b/Lib/test/test_dtrace.py index a63978fd1bd..ba2fa99707c 100644 --- a/Lib/test/test_dtrace.py +++ b/Lib/test/test_dtrace.py @@ -8,7 +8,7 @@ import unittest from test import support -from test.support import findfile +from test.support import findfile, MS_WINDOWS if not support.has_subprocess_support: @@ -103,6 +103,7 @@ class SystemTapBackend(TraceBackend): COMMAND = ["stap", "-g"] +@unittest.skipIf(MS_WINDOWS, "Tests not compliant with trace on Windows.") class TraceTests: # unittest.TestCase options maxDiff = None @@ -159,43 +160,11 @@ class DTraceNormalTests(TraceTests, unittest.TestCase): backend = DTraceBackend() optimize_python = 0 - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_function_entry_return(self): - return super().test_function_entry_return() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_verify_call_opcodes(self): - return super().test_verify_call_opcodes() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_gc(self): - return super().test_gc() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_line(self): - return super().test_line() - class DTraceOptimizedTests(TraceTests, unittest.TestCase): backend = DTraceBackend() optimize_python = 2 - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_function_entry_return(self): - return super().test_function_entry_return() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_verify_call_opcodes(self): - return super().test_verify_call_opcodes() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_gc(self): - return super().test_gc() - - @unittest.expectedFailureIfWindows('TODO: RUSTPYTHON') - def test_line(self): - return super().test_line() - class SystemTapNormalTests(TraceTests, unittest.TestCase): backend = SystemTapBackend() diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 49cdc95021a..671bc487bbf 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -3812,7 +3812,6 @@ def test_typed_subpart_iterator_default_type(self): -Me """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_pushCR_LF(self): '''FeedParser BufferedSubFile.push() assumed it received complete line endings. A CR ending one push() followed by a LF starting @@ -3843,7 +3842,6 @@ def test_pushCR_LF(self): self.assertEqual(len(om), nt) self.assertEqual(''.join([il for il, n in imt]), ''.join(om)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_push_random(self): from email.feedparser import BufferedSubFile, NeedMoreData @@ -3877,7 +3875,6 @@ def test_empty_header_name_handled(self): self.assertEqual(msg['First'], 'val') self.assertEqual(msg['Second'], 'val') - @unittest.expectedFailure # TODO: RUSTPYTHON; Feedparser.feed -> Feedparser._input.push, Feedparser._call_parse -> Feedparser._parse does not keep _input state between calls def test_newlines(self): m = self.parse(['a:\nb:\rc:\r\nd:\n']) self.assertEqual(m.keys(), ['a', 'b', 'c', 'd']) @@ -3896,7 +3893,6 @@ def test_newlines(self): m = self.parse(['a:\r', 'b:\x85', 'c:\n']) self.assertEqual(m.items(), [('a', ''), ('b', '\x85c:')]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_long_lines(self): # Expected peak memory use on 32-bit platform: 6*N*M bytes. M, N = 1000, 20000 diff --git a/Lib/test/test_eof.py b/Lib/test/test_eof.py index f5a0bc56958..582e5b6de6e 100644 --- a/Lib/test/test_eof.py +++ b/Lib/test/test_eof.py @@ -18,7 +18,6 @@ def test_EOF_single_quote(self): self.assertEqual(str(cm.exception), expect) self.assertEqual(cm.exception.offset, 1) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_EOFS(self): expect = ("unterminated triple-quoted string literal (detected at line 3) (, line 1)") with self.assertRaises(SyntaxError) as cm: @@ -45,7 +44,6 @@ def test_EOFS(self): self.assertEqual(cm.exception.text, "ä = '''thîs is ") self.assertEqual(cm.exception.offset, 5) - @unittest.expectedFailure # TODO: RUSTPYTHON @force_not_colorized def test_EOFS_with_file(self): expect = ("(, line 1)") @@ -86,7 +84,6 @@ def test_EOFS_with_file(self): ' ^', 'SyntaxError: unterminated triple-quoted string literal (detected at line 4)']) - @unittest.expectedFailure # TODO: RUSTPYTHON @warnings_helper.ignore_warnings(category=SyntaxWarning) def test_eof_with_line_continuation(self): expect = "unexpected EOF while parsing (, line 1)" @@ -94,7 +91,6 @@ def test_eof_with_line_continuation(self): compile('"\\Xhh" \\', '', 'exec') self.assertEqual(str(cm.exception), expect) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_line_continuation_EOF(self): """A continuation at the end of input must be an error; bpo2180.""" expect = 'unexpected EOF while parsing (, line 1)' @@ -127,7 +123,6 @@ def test_line_continuation_EOF(self): exec('\\') self.assertEqual(str(cm.exception), expect) - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipIf(not sys.executable, "sys.executable required") @force_not_colorized def test_line_continuation_EOF_from_file_bpo2180(self): diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py index 507bbc2ecbc..1e1c43a6bf4 100644 --- a/Lib/test/test_exception_group.py +++ b/Lib/test/test_exception_group.py @@ -1,4 +1,4 @@ -import collections.abc +import collections import types import unittest from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, exceeds_recursion_limit @@ -194,6 +194,79 @@ class MyEG(ExceptionGroup): "MyEG('flat', [ValueError(1), TypeError(2)]), " "TypeError(2)])")) + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Tuples differ: ('test', (ValueError(1), TypeError(2))) != ('test', []) + def test_exceptions_mutation(self): + class MyEG(ExceptionGroup): + pass + + excs = [ValueError(1), TypeError(2)] + eg = MyEG('test', excs) + + self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])") + excs.clear() + + # Ensure that clearing the exceptions sequence doesn't change the repr. + self.assertEqual(repr(eg), "MyEG('test', [ValueError(1), TypeError(2)])") + + # Ensure that the args are still as passed. + self.assertEqual(eg.args, ('test', [])) + + excs = (ValueError(1), KeyboardInterrupt(2)) + eg = BaseExceptionGroup('test', excs) + + # Ensure that immutable sequences still work fine. + self.assertEqual( + repr(eg), + "BaseExceptionGroup('test', (ValueError(1), KeyboardInterrupt(2)))" + ) + + # Test non-standard custom sequences. + excs = collections.deque([ValueError(1), TypeError(2)]) + eg = ExceptionGroup('test', excs) + + self.assertEqual( + repr(eg), + "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))" + ) + excs.clear() + + # Ensure that clearing the exceptions sequence doesn't change the repr. + self.assertEqual( + repr(eg), + "ExceptionGroup('test', deque([ValueError(1), TypeError(2)]))" + ) + + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised + def test_repr_raises(self): + class MySeq(collections.abc.Sequence): + def __init__(self, raises): + self.raises = raises + + def __len__(self): + return 1 + + def __getitem__(self, index): + if index == 0: + return ValueError(1) + raise IndexError + + def __repr__(self): + if self.raises: + raise self.raises + return None + + seq = MySeq(None) + with self.assertRaisesRegex( + TypeError, + r"__repr__ returned non-string \(type NoneType\)" + ): + ExceptionGroup("test", seq) + + seq = MySeq(ValueError) + with self.assertRaises(ValueError): + BaseExceptionGroup("test", seq) + + def create_simple_eg(): excs = [] diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 7e79732a3b9..7c81c4b3905 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -231,7 +231,6 @@ def check(self, src, lineno, offset, end_lineno=None, end_offset=None, encoding= line = line.removeprefix('\ufeff') self.assertIn(line, cm.exception.text) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_error_offset_continuation_characters(self): check = self.check check('"\\\n"(1 for c in I,\\\n\\', 2, 2) @@ -2146,7 +2145,6 @@ class AssertionErrorTests(unittest.TestCase): def tearDown(self): unlink(TESTFN) - @unittest.expectedFailure # TODO: RUSTPYTHON @force_not_colorized def test_assertion_error_location(self): cases = [ @@ -2528,6 +2526,31 @@ def test_incorrect_constructor(self): args = ("bad.py", 1, 2, "abcdefg", 1) self.assertRaises(TypeError, SyntaxError, "bad bad", args) + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 2 is not None + def test_syntax_error_memory_leak(self): + # gh-146250: memory leak with re-initialization of SyntaxError + e = SyntaxError("msg", ("file.py", 1, 2, "txt", 2, 3)) + e.__init__("new_msg", ("new_file.py", 2, 3, "new_txt", 3, 4)) + self.assertEqual(e.msg, "new_msg") + self.assertEqual(e.args, ("new_msg", ("new_file.py", 2, 3, "new_txt", 3, 4))) + self.assertEqual(e.filename, "new_file.py") + self.assertEqual(e.lineno, 2) + self.assertEqual(e.offset, 3) + self.assertEqual(e.text, "new_txt") + self.assertEqual(e.end_lineno, 3) + self.assertEqual(e.end_offset, 4) + + e = SyntaxError("msg", ("file.py", 1, 2, "txt", 2, 3)) + e.__init__("new_msg", ("new_file.py", 2, 3, "new_txt")) + self.assertEqual(e.msg, "new_msg") + self.assertEqual(e.args, ("new_msg", ("new_file.py", 2, 3, "new_txt"))) + self.assertEqual(e.filename, "new_file.py") + self.assertEqual(e.lineno, 2) + self.assertEqual(e.offset, 3) + self.assertEqual(e.text, "new_txt") + self.assertIsNone(e.end_lineno) + self.assertIsNone(e.end_offset) + class TestInvalidExceptionMatcher(unittest.TestCase): def test_except_star_invalid_exception_type(self): diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py new file mode 100644 index 00000000000..08779bdb008 --- /dev/null +++ b/Lib/test/test_external_inspection.py @@ -0,0 +1,1304 @@ +import unittest +import os +import textwrap +import importlib +import sys +import socket +import threading +import time +from asyncio import staggered, taskgroups, base_events, tasks +from unittest.mock import ANY +from test.support import ( + os_helper, + SHORT_TIMEOUT, + busy_retry, + requires_gil_enabled, +) +from test.support.import_helper import import_module +from test.support.script_helper import make_script +from test.support.socket_helper import find_unused_port + +import subprocess + +PROCESS_VM_READV_SUPPORTED = False + +try: + from _remote_debugging import PROCESS_VM_READV_SUPPORTED + from _remote_debugging import RemoteUnwinder + from _remote_debugging import FrameInfo, CoroInfo, TaskInfo +except ImportError: + raise unittest.SkipTest( + "Test only runs when _remote_debugging is available" + ) + + +def _make_test_script(script_dir, script_basename, source): + to_return = make_script(script_dir, script_basename, source) + importlib.invalidate_caches() + return to_return + + +skip_if_not_supported = unittest.skipIf( + ( + sys.platform != "darwin" + and sys.platform != "linux" + and sys.platform != "win32" + ), + "Test only runs on Linux, Windows and MacOS", +) + + +def get_stack_trace(pid): + unwinder = RemoteUnwinder(pid, all_threads=True, debug=True) + return unwinder.get_stack_trace() + + +def get_async_stack_trace(pid): + unwinder = RemoteUnwinder(pid, debug=True) + return unwinder.get_async_stack_trace() + + +def get_all_awaited_by(pid): + unwinder = RemoteUnwinder(pid, debug=True) + return unwinder.get_all_awaited_by() + + +class TestGetStackTrace(unittest.TestCase): + maxDiff = None + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_remote_stack_trace(self): + # Spawn a process with some realistic Python code + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def bar(): + for x in range(100): + if x == 50: + baz() + + def baz(): + foo() + + def foo(): + sock.sendall(b"ready:thread\\n"); time.sleep(10_000) # same line number + + t = threading.Thread(target=bar) + t.start() + sock.sendall(b"ready:main\\n"); t.join() # same line number + """ + ) + stack_trace = None + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + response = b"" + while ( + b"ready:main" not in response + or b"ready:thread" not in response + ): + response += client_socket.recv(1024) + stack_trace = get_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + thread_expected_stack_trace = [ + FrameInfo([script_name, 15, "foo"]), + FrameInfo([script_name, 12, "baz"]), + FrameInfo([script_name, 9, "bar"]), + FrameInfo([threading.__file__, ANY, "Thread.run"]), + ] + # Is possible that there are more threads, so we check that the + # expected stack traces are in the result (looking at you Windows!) + self.assertIn((ANY, thread_expected_stack_trace), stack_trace) + + # Check that the main thread stack trace is in the result + frame = FrameInfo([script_name, 19, ""]) + for _, stack in stack_trace: + if frame in stack: + break + else: + self.fail("Main thread stack trace not found in result") + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_self_trace_after_ctypes_import(self): + """Test that RemoteUnwinder works on the same process after _ctypes import. + + When _ctypes is imported, it may call dlopen on the libpython shared + library, creating a duplicate mapping in the process address space. + The remote debugging code must skip these uninitialized duplicate + mappings and find the real PyRuntime. See gh-144563. + """ + + # Skip the test if the _ctypes module is missing. + import_module("_ctypes") + + # Run the test in a subprocess to avoid side effects + script = textwrap.dedent("""\ + import os + import _remote_debugging + + # Should work before _ctypes import + unwinder = _remote_debugging.RemoteUnwinder(os.getpid()) + + import _ctypes + + # Should still work after _ctypes import (gh-144563) + unwinder = _remote_debugging.RemoteUnwinder(os.getpid()) + """) + + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_async_remote_stack_trace(self): + # Spawn a process with some realistic Python code + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio + import time + import sys + import socket + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def c5(): + sock.sendall(b"ready"); time.sleep(10_000) # same line number + + async def c4(): + await asyncio.sleep(0) + c5() + + async def c3(): + await c4() + + async def c2(): + await c3() + + async def c1(task): + await task + + async def main(): + async with asyncio.TaskGroup() as tg: + task = tg.create_task(c2(), name="c2_root") + tg.create_task(c1(task), name="sub_main_1") + tg.create_task(c1(task), name="sub_main_2") + + def new_eager_loop(): + loop = asyncio.new_event_loop() + eager_task_factory = asyncio.create_eager_task_factory( + asyncio.Task) + loop.set_task_factory(eager_task_factory) + return loop + + asyncio.run(main(), loop_factory={{TASK_FACTORY}}) + """ + ) + stack_trace = None + for task_factory_variant in "asyncio.new_event_loop", "new_eager_loop": + with ( + self.subTest(task_factory_variant=task_factory_variant), + os_helper.temp_dir() as work_dir, + ): + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + server_socket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM + ) + server_socket.setsockopt( + socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 + ) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + script_name = _make_test_script( + script_dir, + "script", + script.format(TASK_FACTORY=task_factory_variant), + ) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + response = client_socket.recv(1024) + self.assertEqual(response, b"ready") + stack_trace = get_async_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # First check all the tasks are present + tasks_names = [ + task.task_name for task in stack_trace[0].awaited_by + ] + for task_name in ["c2_root", "sub_main_1", "sub_main_2"]: + self.assertIn(task_name, tasks_names) + + # Now ensure that the awaited_by_relationships are correct + id_to_task = { + task.task_id: task for task in stack_trace[0].awaited_by + } + task_name_to_awaited_by = { + task.task_name: set( + id_to_task[awaited.task_name].task_name + for awaited in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + task_name_to_awaited_by, + { + "c2_root": {"Task-1", "sub_main_1", "sub_main_2"}, + "Task-1": set(), + "sub_main_1": {"Task-1"}, + "sub_main_2": {"Task-1"}, + }, + ) + + # Now ensure that the coroutine stacks are correct + coroutine_stacks = { + task.task_name: sorted( + tuple(tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + coroutine_stacks, + { + "Task-1": [ + ( + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + tuple([script_name, 26, "main"]), + ) + ], + "c2_root": [ + ( + tuple([script_name, 10, "c5"]), + tuple([script_name, 14, "c4"]), + tuple([script_name, 17, "c3"]), + tuple([script_name, 20, "c2"]), + ) + ], + "sub_main_1": [(tuple([script_name, 23, "c1"]),)], + "sub_main_2": [(tuple([script_name, 23, "c1"]),)], + }, + ) + + # Now ensure the coroutine stacks for the awaited_by relationships are correct. + awaited_by_coroutine_stacks = { + task.task_name: sorted( + ( + id_to_task[coro.task_name].task_name, + tuple(tuple(frame) for frame in coro.call_stack), + ) + for coro in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "c2_root": [ + ( + "Task-1", + ( + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + tuple([script_name, 26, "main"]), + ), + ), + ("sub_main_1", (tuple([script_name, 23, "c1"]),)), + ("sub_main_2", (tuple([script_name, 23, "c1"]),)), + ], + "sub_main_1": [ + ( + "Task-1", + ( + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + tuple([script_name, 26, "main"]), + ), + ) + ], + "sub_main_2": [ + ( + "Task-1", + ( + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + tuple( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + tuple([script_name, 26, "main"]), + ), + ) + ], + }, + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_asyncgen_remote_stack_trace(self): + # Spawn a process with some realistic Python code + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio + import time + import sys + import socket + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + async def gen_nested_call(): + sock.sendall(b"ready"); time.sleep(10_000) # same line number + + async def gen(): + for num in range(2): + yield num + if num == 1: + await gen_nested_call() + + async def main(): + async for el in gen(): + pass + + asyncio.run(main()) + """ + ) + stack_trace = None + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + response = client_socket.recv(1024) + self.assertEqual(response, b"ready") + stack_trace = get_async_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # For this simple asyncgen test, we only expect one task with the full coroutine stack + self.assertEqual(len(stack_trace[0].awaited_by), 1) + task = stack_trace[0].awaited_by[0] + self.assertEqual(task.task_name, "Task-1") + + # Check the coroutine stack - based on actual output, only shows main + coroutine_stack = sorted( + tuple(tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + self.assertEqual( + coroutine_stack, + [ + ( + tuple([script_name, 10, "gen_nested_call"]), + tuple([script_name, 16, "gen"]), + tuple([script_name, 19, "main"]), + ) + ], + ) + + # No awaited_by relationships expected for this simple case + self.assertEqual(task.awaited_by, []) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_async_gather_remote_stack_trace(self): + # Spawn a process with some realistic Python code + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio + import time + import sys + import socket + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + async def deep(): + await asyncio.sleep(0) + sock.sendall(b"ready"); time.sleep(10_000) # same line number + + async def c1(): + await asyncio.sleep(0) + await deep() + + async def c2(): + await asyncio.sleep(0) + + async def main(): + await asyncio.gather(c1(), c2()) + + asyncio.run(main()) + """ + ) + stack_trace = None + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + response = client_socket.recv(1024) + self.assertEqual(response, b"ready") + stack_trace = get_async_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # First check all the tasks are present + tasks_names = [ + task.task_name for task in stack_trace[0].awaited_by + ] + for task_name in ["Task-1", "Task-2"]: + self.assertIn(task_name, tasks_names) + + # Now ensure that the awaited_by_relationships are correct + id_to_task = { + task.task_id: task for task in stack_trace[0].awaited_by + } + task_name_to_awaited_by = { + task.task_name: set( + id_to_task[awaited.task_name].task_name + for awaited in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + task_name_to_awaited_by, + { + "Task-1": set(), + "Task-2": {"Task-1"}, + }, + ) + + # Now ensure that the coroutine stacks are correct + coroutine_stacks = { + task.task_name: sorted( + tuple(tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + coroutine_stacks, + { + "Task-1": [(tuple([script_name, 21, "main"]),)], + "Task-2": [ + ( + tuple([script_name, 11, "deep"]), + tuple([script_name, 15, "c1"]), + ) + ], + }, + ) + + # Now ensure the coroutine stacks for the awaited_by relationships are correct. + awaited_by_coroutine_stacks = { + task.task_name: sorted( + ( + id_to_task[coro.task_name].task_name, + tuple(tuple(frame) for frame in coro.call_stack), + ) + for coro in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "Task-2": [ + ("Task-1", (tuple([script_name, 21, "main"]),)) + ], + }, + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_async_staggered_race_remote_stack_trace(self): + # Spawn a process with some realistic Python code + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio.staggered + import time + import sys + import socket + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + async def deep(): + await asyncio.sleep(0) + sock.sendall(b"ready"); time.sleep(10_000) # same line number + + async def c1(): + await asyncio.sleep(0) + await deep() + + async def c2(): + await asyncio.sleep(10_000) + + async def main(): + await asyncio.staggered.staggered_race( + [c1, c2], + delay=None, + ) + + asyncio.run(main()) + """ + ) + stack_trace = None + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + response = client_socket.recv(1024) + self.assertEqual(response, b"ready") + stack_trace = get_async_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # First check all the tasks are present + tasks_names = [ + task.task_name for task in stack_trace[0].awaited_by + ] + for task_name in ["Task-1", "Task-2"]: + self.assertIn(task_name, tasks_names) + + # Now ensure that the awaited_by_relationships are correct + id_to_task = { + task.task_id: task for task in stack_trace[0].awaited_by + } + task_name_to_awaited_by = { + task.task_name: set( + id_to_task[awaited.task_name].task_name + for awaited in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + task_name_to_awaited_by, + { + "Task-1": set(), + "Task-2": {"Task-1"}, + }, + ) + + # Now ensure that the coroutine stacks are correct + coroutine_stacks = { + task.task_name: sorted( + tuple(tuple(frame) for frame in coro.call_stack) + for coro in task.coroutine_stack + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + coroutine_stacks, + { + "Task-1": [ + ( + tuple([staggered.__file__, ANY, "staggered_race"]), + tuple([script_name, 21, "main"]), + ) + ], + "Task-2": [ + ( + tuple([script_name, 11, "deep"]), + tuple([script_name, 15, "c1"]), + tuple( + [ + staggered.__file__, + ANY, + "staggered_race..run_one_coro", + ] + ), + ) + ], + }, + ) + + # Now ensure the coroutine stacks for the awaited_by relationships are correct. + awaited_by_coroutine_stacks = { + task.task_name: sorted( + ( + id_to_task[coro.task_name].task_name, + tuple(tuple(frame) for frame in coro.call_stack), + ) + for coro in task.awaited_by + ) + for task in stack_trace[0].awaited_by + } + self.assertEqual( + awaited_by_coroutine_stacks, + { + "Task-1": [], + "Task-2": [ + ( + "Task-1", + ( + tuple( + [staggered.__file__, ANY, "staggered_race"] + ), + tuple([script_name, 21, "main"]), + ), + ) + ], + }, + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_async_global_awaited_by(self): + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import asyncio + import os + import random + import sys + import socket + from string import ascii_lowercase, digits + from test.support import socket_helper, SHORT_TIMEOUT + + HOST = '127.0.0.1' + PORT = socket_helper.find_unused_port() + connections = 0 + + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + class EchoServerProtocol(asyncio.Protocol): + def connection_made(self, transport): + global connections + connections += 1 + self.transport = transport + + def data_received(self, data): + self.transport.write(data) + self.transport.close() + + async def echo_client(message): + reader, writer = await asyncio.open_connection(HOST, PORT) + writer.write(message.encode()) + await writer.drain() + + data = await reader.read(100) + assert message == data.decode() + writer.close() + await writer.wait_closed() + # Signal we are ready to sleep + sock.sendall(b"ready") + await asyncio.sleep(SHORT_TIMEOUT) + + async def echo_client_spam(server): + async with asyncio.TaskGroup() as tg: + while connections < 1000: + msg = list(ascii_lowercase + digits) + random.shuffle(msg) + tg.create_task(echo_client("".join(msg))) + await asyncio.sleep(0) + # at least a 1000 tasks created. Each task will signal + # when is ready to avoid the race caused by the fact that + # tasks are waited on tg.__exit__ and we cannot signal when + # that happens otherwise + # at this point all client tasks completed without assertion errors + # let's wrap up the test + server.close() + await server.wait_closed() + + async def main(): + loop = asyncio.get_running_loop() + server = await loop.create_server(EchoServerProtocol, HOST, PORT) + async with server: + async with asyncio.TaskGroup() as tg: + tg.create_task(server.serve_forever(), name="server task") + tg.create_task(echo_client_spam(server), name="echo client spam") + + asyncio.run(main()) + """ + ) + stack_trace = None + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + for _ in range(1000): + expected_response = b"ready" + response = client_socket.recv(len(expected_response)) + self.assertEqual(response, expected_response) + for _ in busy_retry(SHORT_TIMEOUT): + try: + all_awaited_by = get_all_awaited_by(p.pid) + except RuntimeError as re: + # This call reads a linked list in another process with + # no synchronization. That occasionally leads to invalid + # reads. Here we avoid making the test flaky. + msg = str(re) + if msg.startswith("Task list appears corrupted"): + continue + elif msg.startswith( + "Invalid linked list structure reading remote memory" + ): + continue + elif msg.startswith("Unknown error reading memory"): + continue + elif msg.startswith("Unhandled frame owner"): + continue + raise # Unrecognized exception, safest not to ignore it + else: + break + # expected: a list of two elements: 1 thread, 1 interp + self.assertEqual(len(all_awaited_by), 2) + # expected: a tuple with the thread ID and the awaited_by list + self.assertEqual(len(all_awaited_by[0]), 2) + # expected: no tasks in the fallback per-interp task list + self.assertEqual(all_awaited_by[1], (0, [])) + entries = all_awaited_by[0][1] + # expected: at least 1000 pending tasks + self.assertGreaterEqual(len(entries), 1000) + # the first three tasks stem from the code structure + main_stack = [ + FrameInfo([taskgroups.__file__, ANY, "TaskGroup._aexit"]), + FrameInfo( + [taskgroups.__file__, ANY, "TaskGroup.__aexit__"] + ), + FrameInfo([script_name, 60, "main"]), + ] + self.assertIn( + TaskInfo( + [ANY, "Task-1", [CoroInfo([main_stack, ANY])], []] + ), + entries, + ) + self.assertIn( + TaskInfo( + [ + ANY, + "server task", + [ + CoroInfo( + [ + [ + FrameInfo( + [ + base_events.__file__, + ANY, + "Server.serve_forever", + ] + ) + ], + ANY, + ] + ) + ], + [ + CoroInfo( + [ + [ + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + FrameInfo( + [script_name, ANY, "main"] + ), + ], + ANY, + ] + ) + ], + ] + ), + entries, + ) + self.assertIn( + TaskInfo( + [ + ANY, + "Task-4", + [ + CoroInfo( + [ + [ + FrameInfo( + [tasks.__file__, ANY, "sleep"] + ), + FrameInfo( + [ + script_name, + 38, + "echo_client", + ] + ), + ], + ANY, + ] + ) + ], + [ + CoroInfo( + [ + [ + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + FrameInfo( + [ + script_name, + 41, + "echo_client_spam", + ] + ), + ], + ANY, + ] + ) + ], + ] + ), + entries, + ) + + expected_awaited_by = [ + CoroInfo( + [ + [ + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup._aexit", + ] + ), + FrameInfo( + [ + taskgroups.__file__, + ANY, + "TaskGroup.__aexit__", + ] + ), + FrameInfo( + [script_name, 41, "echo_client_spam"] + ), + ], + ANY, + ] + ) + ] + tasks_with_awaited = [ + task + for task in entries + if task.awaited_by == expected_awaited_by + ] + self.assertGreaterEqual(len(tasks_with_awaited), 1000) + + # the final task will have some random number, but it should for + # sure be one of the echo client spam horde (In windows this is not true + # for some reason) + if sys.platform != "win32": + self.assertEqual( + tasks_with_awaited[-1].awaited_by, + entries[-1].awaited_by, + ) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_self_trace(self): + stack_trace = get_stack_trace(os.getpid()) + # Is possible that there are more threads, so we check that the + # expected stack traces are in the result (looking at you Windows!) + this_tread_stack = None + for thread_id, stack in stack_trace: + if thread_id == threading.get_native_id(): + this_tread_stack = stack + break + self.assertIsNotNone(this_tread_stack) + self.assertEqual( + stack[:2], + [ + FrameInfo( + [ + __file__, + get_stack_trace.__code__.co_firstlineno + 2, + "get_stack_trace", + ] + ), + FrameInfo( + [ + __file__, + self.test_self_trace.__code__.co_firstlineno + 6, + "TestGetStackTrace.test_self_trace", + ] + ), + ], + ) + + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + @requires_gil_enabled("Free threaded builds don't have an 'active thread'") + def test_only_active_thread(self): + # Test that only_active_thread parameter works correctly + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + + # Connect to the test process + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def worker_thread(name, barrier, ready_event): + barrier.wait() # Synchronize thread start + ready_event.wait() # Wait for main thread signal + # Sleep to keep thread alive + time.sleep(10_000) + + def main_work(): + # Do busy work to hold the GIL + sock.sendall(b"working\\n") + count = 0 + while count < 100000000: + count += 1 + if count % 10000000 == 0: + pass # Keep main thread busy + sock.sendall(b"done\\n") + + # Create synchronization primitives + num_threads = 3 + barrier = threading.Barrier(num_threads + 1) # +1 for main thread + ready_event = threading.Event() + + # Start worker threads + threads = [] + for i in range(num_threads): + t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event)) + t.start() + threads.append(t) + + # Wait for all threads to be ready + barrier.wait() + + # Signal ready to parent process + sock.sendall(b"ready\\n") + + # Signal threads to start waiting + ready_event.set() + + # Now do busy work to hold the GIL + main_work() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + # Create a socket server to communicate with the target process + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_socket.bind(("localhost", port)) + server_socket.settimeout(SHORT_TIMEOUT) + server_socket.listen(1) + + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + try: + p = subprocess.Popen([sys.executable, script_name]) + client_socket, _ = server_socket.accept() + server_socket.close() + + # Wait for ready signal + response = b"" + while b"ready" not in response: + response += client_socket.recv(1024) + + # Wait for the main thread to start its busy work + while b"working" not in response: + response += client_socket.recv(1024) + + # Get stack trace with all threads + unwinder_all = RemoteUnwinder(p.pid, all_threads=True) + for _ in range(10): + # Wait for the main thread to start its busy work + all_traces = unwinder_all.get_stack_trace() + found = False + for thread_id, stack in all_traces: + if not stack: + continue + current_frame = stack[0] + if ( + current_frame.funcname == "main_work" + and current_frame.lineno > 15 + ): + found = True + + if found: + break + # Give a bit of time to take the next sample + time.sleep(0.1) + else: + self.fail( + "Main thread did not start its busy work on time" + ) + + # Get stack trace with only GIL holder + unwinder_gil = RemoteUnwinder(p.pid, only_active_thread=True) + gil_traces = unwinder_gil.get_stack_trace() + + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + finally: + if client_socket is not None: + client_socket.close() + p.kill() + p.terminate() + p.wait(timeout=SHORT_TIMEOUT) + + # Verify we got multiple threads in all_traces + self.assertGreater( + len(all_traces), 1, "Should have multiple threads" + ) + + # Verify we got exactly one thread in gil_traces + self.assertEqual( + len(gil_traces), 1, "Should have exactly one GIL holder" + ) + + # The GIL holder should be in the all_traces list + gil_thread_id = gil_traces[0][0] + all_thread_ids = [trace[0] for trace in all_traces] + self.assertIn( + gil_thread_id, + all_thread_ids, + "GIL holder should be among all threads", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_fileutils.py b/Lib/test/test_fileutils.py new file mode 100644 index 00000000000..ff13498fbfe --- /dev/null +++ b/Lib/test/test_fileutils.py @@ -0,0 +1,30 @@ +# Run tests for functions in Python/fileutils.c. + +import os +import os.path +import unittest +from test.support import import_helper + +# Skip this test if the _testcapi module isn't available. +_testcapi = import_helper.import_module('_testinternalcapi') + + +class PathTests(unittest.TestCase): + + def test_capi_normalize_path(self): + if os.name == 'nt': + raise unittest.SkipTest('Windows has its own helper for this') + else: + from test.test_posixpath import PosixPathTest as posixdata + tests = posixdata.NORMPATH_CASES + for filename, expected in tests: + if not os.path.isabs(filename): + continue + with self.subTest(filename): + result = _testcapi.normalize_path(filename) + self.assertEqual(result, expected, + msg=f'input: {filename!r} expected output: {expected!r}') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 0938c89cbcb..609a7164fa0 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -725,7 +725,6 @@ def test_serialized_float_rounding(self): class FormatTestCase(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Invalid format specifier def test_format(self): # these should be rewritten to use both format(x, spec) and # x.__format__(spec) @@ -1262,7 +1261,6 @@ def test_whitespace(self): self.identical(got, expected) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: invalid hexadecimal floating-point string def test_from_hex(self): MIN = self.MIN MAX = self.MAX diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 6868c87171d..aa28108312e 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -515,7 +515,6 @@ def test_with_two_underscore_in_format_specifier(self): with self.assertRaisesRegex(ValueError, error_msg): '{:__}'.format(1) - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_with_a_commas_and_an_underscore_in_format_specifier(self): error_msg = re.escape("Cannot specify both ',' and '_'.") with self.assertRaisesRegex(ValueError, error_msg): @@ -523,7 +522,6 @@ def test_with_a_commas_and_an_underscore_in_format_specifier(self): with self.assertRaisesRegex(ValueError, error_msg): '{:.,_f}'.format(1.1) - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_with_an_underscore_and_a_comma_in_format_specifier(self): error_msg = re.escape("Cannot specify both ',' and '_'.") with self.assertRaisesRegex(ValueError, error_msg): @@ -560,7 +558,6 @@ def test_unicode_in_error_message(self): with self.assertRaisesRegex(ValueError, str_err): "{a:%ЫйЯЧ}".format(a='a') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_negative_zero(self): ## default behavior self.assertEqual(f"{-0.:.1f}", "-0.0") diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index ae02e2a59f9..53d42a595b7 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -315,7 +315,6 @@ def inner(): % (file_repr, offset + 5)) class TestFrameLocals(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_scope(self): class A: x = 1 @@ -333,7 +332,6 @@ def f(): self.assertEqual(locals()['y'], 2) f() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 2 def test_closure(self): x = 1 y = 2 @@ -356,7 +354,6 @@ def test_closure_with_inline_comprehension(self): lst = [locals() for k in [0]] self.assertEqual(lst[0]['k'], 0) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 3 != 4 def test_as_dict(self): x = 1 y = 2 @@ -414,7 +411,6 @@ def test_non_string_key(self): d[1] = 2 self.assertEqual(d[1], 2) - @unittest.expectedFailure # TODO: RUSTPYTHON; UnboundLocalError: local variable 'b' referenced before assignment def test_write_with_hidden(self): def f(): f_locals = [sys._getframe().f_locals for b in [0]][0] @@ -426,7 +422,6 @@ def f(): c = 0 f() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: != 'a.b.c' def test_local_objects(self): o = object() k = '.'.join(['a', 'b', 'c']) @@ -457,7 +452,6 @@ def test_repr(self): frame = sys._getframe() self.assertEqual(repr(frame.f_locals), repr(dict(frame.f_locals))) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised def test_delete(self): x = 1 d = sys._getframe().f_locals @@ -501,7 +495,6 @@ def test_sizeof(self): proxy = sys._getframe().f_locals support.check_sizeof(self, proxy, support.calcobjsize("P")) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised def test_unsupport(self): x = 1 d = sys._getframe().f_locals @@ -536,7 +529,6 @@ def __eq__(self, other): return StringSubclass('x'), ImpostorX(), 'x' - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: dict_keys(['obj', 'x']) != ['obj', 'x', 'proxy'] def test_proxy_key_stringlikes_overwrite(self): def f(obj): x = 1 @@ -559,7 +551,6 @@ def f(obj): self.assertEqual(keys_snapshot, expected_keys) self.assertEqual(proxy_snapshot, expected_dict) - @unittest.expectedFailure # TODO: RUSTPYTHON; UnboundLocalError: local variable 'b' referenced before assignment def test_proxy_key_stringlikes_ftrst_write(self): def f(obj): proxy = sys._getframe().f_locals @@ -587,7 +578,6 @@ class ObjectSubclass: with self.assertRaises(TypeError): proxy[obj] = 0 - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'dict' != 'FrameLocalsProxy' def test_constructor(self): FrameLocalsProxy = type([sys._getframe().f_locals for x in range(1)][0]) diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index f4fca1caec7..e35d5118f18 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -701,7 +701,6 @@ def test_double_braces(self): ["f'{ {{}} }'", # dict in a set ]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_compile_time_concat(self): x = 'def' self.assertEqual('abc' f'## {x}ghi', 'abc## defghi') @@ -816,7 +815,6 @@ def build_fstr(n, extra=''): s = "f'{1}' 'x' 'y'" * 1024 self.assertEqual(eval(s), '1xy' * 1024) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_format_specifier_expressions(self): width = 10 precision = 4 @@ -947,7 +945,6 @@ def test_parens_in_expressions(self): ["f'{3)+(4}'", ]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_newlines_before_syntax_error(self): self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'", @@ -1031,7 +1028,6 @@ def test_misformed_unicode_character_name(self): r"'\N{GREEK CAPITAL LETTER DELTA'", ]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_backslashes_in_expression_part(self): self.assertEqual(f"{( 1 + @@ -1732,7 +1728,6 @@ def test_with_an_underscore_and_a_comma_in_format_specifier(self): with self.assertRaisesRegex(ValueError, error_msg): f'{1:_,}' - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "f-string: expecting a valid expression after '{'" does not match "invalid syntax (?, line 1)" def test_syntax_error_for_starred_expressions(self): with self.assertRaisesRegex(SyntaxError, "can't use starred expression here"): compile("f'{*a}'", "?", "exec") diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index ff696c5c153..bb9c88efec6 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -432,7 +432,6 @@ def f(): class CellTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_comparison(self): # These tests are here simply to exercise the comparison code; # their presence should not be interpreted as providing any diff --git a/Lib/test/test_future_stmt/test_future.py b/Lib/test/test_future_stmt/test_future.py index faa5f4cc683..02690919cf3 100644 --- a/Lib/test/test_future_stmt/test_future.py +++ b/Lib/test/test_future_stmt/test_future.py @@ -81,7 +81,6 @@ def test_future_multiple_features(self): ): from test.test_future_stmt import test_future_multiple_features # noqa: F401 - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 24 def test_unknown_future_flag(self): code = """ from __future__ import nested_scopes @@ -135,14 +134,12 @@ def test_multiple_import_statements_on_same_line(self): """ self.assertSyntaxError(code, offset=54) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 24 def test_future_import_star(self): code = """ from __future__ import * """ self.assertSyntaxError(code, message='future feature * is not defined', offset=24) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_future_import_braces(self): code = """ from __future__ import braces @@ -180,7 +177,6 @@ def test_unicode_literals_exec(self): exec("from __future__ import unicode_literals; x = ''", {}, scope) self.assertIsInstance(scope["x"], str) - @unittest.expectedFailure # TODO: RUSTPYTHON; barry_as_FLUFL (<> operator) not supported def test_syntactical_future_repl(self): p = spawn_python('-i') p.stdin.write(b"from __future__ import barry_as_FLUFL\n") @@ -188,7 +184,6 @@ def test_syntactical_future_repl(self): out = kill_python(p) self.assertNotIn(b'SyntaxError: invalid syntax', out) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_future_dotted_import(self): with self.assertRaises(ImportError): exec("from .__future__ import spam") @@ -480,7 +475,6 @@ def bar(): self.assertEqual(foo.__code__.co_cellvars, ()) self.assertEqual(foo().__code__.co_freevars, ()) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised def test_annotations_forbidden(self): with self.assertRaises(SyntaxError): self._exec_future("test: (yield)") diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 8ede6e22fab..07c1decb42c 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -134,7 +134,6 @@ def gen(): self.assertEqual(len(resurrected), 1) self.assertIsInstance(resurrected[0].gi_code, types.CodeType) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: is not None def test_exhausted_generator_frame_cycle(self): def g(): yield @@ -762,7 +761,6 @@ def get_frame(index): self.assertIn('a', frame_locals) self.assertEqual(frame_locals['a'], 42) - @unittest.expectedFailure # TODO: RUSTPYTHON; frame locals don't survive generator deallocation def test_frame_locals_outlive_generator(self): frame_locals1 = None diff --git a/Lib/test/test_genexps.py b/Lib/test/test_genexps.py index fde12f13cdc..17d2d137074 100644 --- a/Lib/test/test_genexps.py +++ b/Lib/test/test_genexps.py @@ -159,7 +159,7 @@ ... SyntaxError: cannot assign to generator expression - >>> (y for y in (1,2)) += 10 # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> (y for y in (1,2)) += 10 Traceback (most recent call last): ... SyntaxError: 'generator expression' is an illegal expression for augmented assignment diff --git a/Lib/test/test_global.py b/Lib/test/test_global.py index 1f55dfbe1ac..11d0bd54e8b 100644 --- a/Lib/test/test_global.py +++ b/Lib/test/test_global.py @@ -28,7 +28,6 @@ def setUp(self): ### Syntax error cases as covered in Python/symtable.c ###################################################### - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5 def test_name_param(self): prog_text = """\ def fn(name_param): @@ -36,7 +35,6 @@ def fn(name_param): """ check_syntax_error(self, prog_text, lineno=2, offset=5) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5 def test_name_after_assign(self): prog_text = """\ def fn(): @@ -45,7 +43,6 @@ def fn(): """ check_syntax_error(self, prog_text, lineno=3, offset=5) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5 def test_name_after_use(self): prog_text = """\ def fn(): @@ -54,7 +51,6 @@ def fn(): """ check_syntax_error(self, prog_text, lineno=3, offset=5) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 12 != 5 def test_name_annot(self): prog_text_3 = """\ def fn(): diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index cf90de7b115..cfb24a5c457 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -114,7 +114,6 @@ def test_underscore_literals(self): # Sanity check: no literal begins with an underscore self.assertRaises(NameError, eval, "_0") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bad_numerical_literals(self): check = self.check_syntax_error check("0b12", "invalid digit '2' in binary literal") @@ -137,7 +136,6 @@ def test_bad_numerical_literals(self): check("1e2_", "invalid decimal literal") check("1e+", "invalid decimal literal") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_end_of_numerical_literals(self): def check(test, error=False): with self.subTest(expr=test): @@ -251,7 +249,6 @@ def test_eof_error(self): compile(s, "", "exec") self.assertIn("was never closed", str(cm.exception)) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised @skip_wasi_stack_overflow() def test_max_level(self): # Macro defined in Parser/lexer/state.h @@ -298,7 +295,6 @@ def one(): my_lst[one()-1]: int = 5 self.assertEqual(my_lst, [5]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_var_annot_syntax_errors(self): # parser pass check_syntax_error(self, "def f: int") @@ -751,7 +747,6 @@ def test_expr_stmt(self): # Check the heuristic for print & exec covers significant cases # As well as placing some limits on false positives - @unittest.expectedFailure # TODO: RUSTPYTHON def test_former_statements_refer_to_builtins(self): keywords = "print", "exec" # Cases where we want the custom error @@ -1165,7 +1160,6 @@ def continue_in_finally_after_return2(x): """, True) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_yield(self): # Allowed as standalone statement def g(): yield 1 @@ -1205,7 +1199,6 @@ def g(): rest = 4, 5, 6; yield 1, 2, 3, *rest # Check annotation refleak on SyntaxError check_syntax_error(self, "def g(a:(yield)): pass") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_yield_in_comprehensions(self): # Check yield in comprehensions def g(): [x for x in [(yield 1)]] @@ -1302,7 +1295,6 @@ def test_assert_failures(self): else: self.fail("AssertionError not raised by 'assert False'") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_assert_syntax_warnings(self): # Ensure that we warn users if they provide a non-zero length tuple as # the assertion test. @@ -1317,7 +1309,6 @@ def test_assert_syntax_warnings(self): compile('assert x, "msg"', '', 'exec') compile('assert False, "msg"', '', 'exec') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_assert_warning_promotes_to_syntax_error(self): # If SyntaxWarning is configured to be an error, it actually raises a # SyntaxError. @@ -1496,7 +1487,6 @@ def test_comparison(self): if 1 not in (): pass if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in x is x is not x: pass - @unittest.expectedFailure # TODO: RUSTPYTHON def test_comparison_is_literal(self): def check(test, msg): self.check_syntax_warning(test, msg) @@ -1526,7 +1516,6 @@ def check(test, msg): compile('True is x', '', 'exec') compile('... is x', '', 'exec') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_warn_missed_comma(self): def check(test): self.check_syntax_warning(test, msg) @@ -1734,8 +1723,7 @@ class G: pass class H: pass @d := class_decorator class I: pass - # TODO: RUSTPYTHON; SyntaxError: the symbol 'class_decorator' must be present in the symbol table - # @lambda c: class_decorator(c) + @lambda c: class_decorator(c) class J: pass @[..., class_decorator, ...][1] class K: pass diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index f7a7c0cc825..d928c878a15 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -588,7 +588,6 @@ def test_frame(self): self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals), '(x=11, y=14)') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'f_code' def test_previous_frame(self): args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back) self.assertEqual(args, ['a', 'b', 'c', 'd', 'e', 'f']) @@ -5988,7 +5987,6 @@ def _strip_non_python_syntax(self, input, self.assertEqual(computed_clean_signature, clean_signature) self.assertEqual(computed_self_parameter, self_parameter) - @unittest.expectedFailure # TODO: RUSTPYTHON; + (module, /, path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) def test_signature_strip_non_python_syntax(self): self._strip_non_python_syntax( "($module, /, path, mode, *, dir_fd=None, " + @@ -6319,7 +6317,6 @@ def test_weakref_module_has_signatures(self): no_signature = {'ReferenceType', 'ref'} self._test_module_has_signatures(weakref, no_signature) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: .func at 0xa4c07a580> builtin has invalid signature def test_python_function_override_signature(self): def func(*args, **kwargs): pass diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index c51b547f31a..99ab6c7ba90 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -4473,7 +4473,6 @@ def test_io_after_close(self): self.assertRaises(ValueError, f.writelines, []) self.assertRaises(ValueError, next, f) - @unittest.expectedFailure # TODO: RUSTPYTHON; cyclic gc def test_blockingioerror(self): # Various BlockingIOError issues class C(str): diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index e7c764815d1..c1695690b72 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -538,7 +538,6 @@ def test_count(self): #check proper internal error handling for large "step' sizes count(1, maxsize+5); sys.exc_info() - @unittest.expectedFailure # TODO: RUSTPYTHON; 'count(10.5)' != 'count(10.5, 1.0)' def test_count_with_step(self): self.assertEqual(lzip('abc',count(2,3)), [('a', 2), ('b', 5), ('c', 8)]) self.assertEqual(lzip('abc',count(start=2,step=3)), @@ -1137,7 +1136,6 @@ def test_repeat_with_negative_times(self): self.assertEqual(repr(repeat('a', times=-1)), "repeat('a', 0)") self.assertEqual(repr(repeat('a', times=-2)), "repeat('a', 0)") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_map(self): self.assertEqual(list(map(operator.pow, range(3), range(1,7))), [0**1, 1**2, 2**3]) @@ -1262,7 +1260,6 @@ def test_takewhile(self): self.assertEqual(list(t), [1, 1, 1]) self.assertRaises(StopIteration, next, t) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_dropwhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(dropwhile(underten, data)), [20, 2, 4, 6, 8]) @@ -1273,7 +1270,6 @@ def test_dropwhile(self): self.assertRaises(TypeError, next, dropwhile(10, [(4,5)])) self.assertRaises(ValueError, next, dropwhile(errfunc, [(4,5)])) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_tee(self): n = 200 diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index e320a2008a8..40cec226183 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -106,7 +106,6 @@ def test_empty_slice(self): x[:] = x self.assertEqual(x, []) - @unittest.skip("TODO: RUSTPYTHON; crash") def test_list_resize_overflow(self): # gh-97616: test new_allocated * sizeof(PyObject*) overflow # check in list_resize() diff --git a/Lib/test/test_listcomps.py b/Lib/test/test_listcomps.py index 5dbc130b4c5..e76169c69df 100644 --- a/Lib/test/test_listcomps.py +++ b/Lib/test/test_listcomps.py @@ -206,11 +206,9 @@ class i: [__classdict__ for x in y] """ self._check_in_scopes(code, raises=NameError) - @unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: compiler_make_closure: cannot find '__classdict__' in parent vars def test_references___classdict___nested(self): class _C: - # res = [(lambda: __classdict__)() for _ in [1]] # TODO: RUSTPYTHON - pass # TODO: RUSTPYTHON + res = [(lambda: __classdict__)() for _ in [1]] self.assertIn("res", _C.res[0]) def test_references___conditional_annotations__(self): @@ -219,7 +217,6 @@ class i: [__conditional_annotations__ for x in y] """ self._check_in_scopes(code, raises=NameError) - @unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: compiler_make_closure: cannot find '__conditional_annotations__' in parent vars def test_references___conditional_annotations___nested(self): code = """ class i: [lambda: __conditional_annotations__ for x in y] diff --git a/Lib/test/test_lzma.py b/Lib/test/test_lzma.py index eebe6370f5f..fff261d890e 100644 --- a/Lib/test/test_lzma.py +++ b/Lib/test/test_lzma.py @@ -656,7 +656,6 @@ def test_init_bad_check(self): with self.assertRaises(ValueError): LZMAFile(BytesIO(COMPRESSED_XZ), check=lzma.CHECK_UNKNOWN) - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u32 def test_init_bad_preset(self): with self.assertRaises(TypeError): LZMAFile(BytesIO(), "w", preset=4.39) diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py index ad4c6095abf..4e5311cd0a2 100644 --- a/Lib/test/test_marshal.py +++ b/Lib/test/test_marshal.py @@ -319,7 +319,6 @@ def test_recursion_limit(self): last.append([0]) self.assertRaises(ValueError, marshal.dumps, head) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_reference_loop_list(self): a = [] a.append(a) @@ -331,7 +330,6 @@ def test_reference_loop_list(self): self.assertIsInstance(b, list) self.assertIs(b[0], b) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_reference_loop_dict(self): a = {} a[None] = a @@ -343,7 +341,6 @@ def test_reference_loop_dict(self): self.assertIsInstance(b, dict) self.assertIs(b[None], b) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_reference_loop_tuple(self): a = ([],) a[0].append(a) @@ -356,7 +353,6 @@ def test_reference_loop_tuple(self): self.assertIsInstance(b[0], list) self.assertIs(b[0][0], b) - @unittest.skip("TODO: RUSTPYTHON; unexpected payload for constant python value") def test_reference_loop_code(self): def f(): return 1234.5 @@ -370,7 +366,6 @@ def f(): for v in range(marshal.version + 1): self.assertRaises(ValueError, marshal.dumps, code, v) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised by dumps def test_reference_loop_slice(self): a = slice([], None) a.start.append(a) @@ -387,21 +382,18 @@ def test_reference_loop_slice(self): for v in range(marshal.version + 1): self.assertRaises(ValueError, marshal.dumps, a, v) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_loads_reference_loop_list(self): data = b'\xdb\x01\x00\x00\x00r\x00\x00\x00\x00' # [] a = marshal.loads(data) self.assertIsInstance(a, list) self.assertIs(a[0], a) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_loads_reference_loop_dict(self): data = b'\xfbNr\x00\x00\x00\x000' # {None: } a = marshal.loads(data) self.assertIsInstance(a, dict) self.assertIs(a[None], a) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: bad marshal data def test_loads_abnormal_reference_loops(self): # Indirect self-references of tuples. data = b'\xa8\x01\x00\x00\x00[\x01\x00\x00\x00r\x00\x00\x00\x00' # ([],) @@ -547,7 +539,6 @@ def test_deterministic_sets(self): _, dump_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1") self.assertEqual(dump_0, dump_1) - @unittest.skip("TODO: RUSTPYTHON; unexpected payload for constant python value") def test_unmarshallable(self): # Check no crash after encountering unmarshallable objects. # See https://github.com/python/cpython/issues/106287. @@ -748,7 +739,6 @@ class InterningTestCase(unittest.TestCase, HelperMixin): strobj = "this is an interned string" strobj = sys.intern(strobj) - @unittest.expectedFailure # TODO: RUSTPYTHON def testIntern(self): s = marshal.loads(marshal.dumps(self.strobj)) self.assertEqual(s, self.strobj) diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index 7214a377067..7ad3aa8a527 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/test_memoryio.py @@ -587,7 +587,6 @@ def test_issue5449(self): self.ioclass(initial_bytes=buf) self.assertRaises(TypeError, self.ioclass, buf, foo=None) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'B' def test_write_concurrent_close(self): class B: def __buffer__(self, flags): @@ -601,7 +600,6 @@ def __buffer__(self, flags): # concurrently mutates (e.g., closes or exports) 'memio'. # See: https://github.com/python/cpython/issues/143378. - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'B' def test_writelines_concurrent_close(self): class B: def __buffer__(self, flags): @@ -611,7 +609,6 @@ def __buffer__(self, flags): memio = self.ioclass() self.assertRaises(ValueError, memio.writelines, [B()]) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'B' def test_write_concurrent_export(self): class B: buf = None @@ -622,7 +619,6 @@ def __buffer__(self, flags): memio = self.ioclass() self.assertRaises(BufferError, memio.write, B()) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'B' def test_writelines_concurrent_export(self): class B: buf = None @@ -633,7 +629,6 @@ def __buffer__(self, flags): memio = self.ioclass() self.assertRaises(BufferError, memio.writelines, [B()]) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'B' def test_write_mutating_buffer(self): # Test that buffer is exported only once during write(). # See: https://github.com/python/cpython/issues/143602. @@ -930,13 +925,6 @@ def test_cow_mutable(self): def test_flags(self): return super().test_flags() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised by write - def test_write(self): - return super().test_write() - - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u64 - def test_seek(self): - return super().test_seek() class CStringIOTest(PyStringIOTest): ioclass = io.StringIO @@ -944,7 +932,6 @@ class CStringIOTest(PyStringIOTest): # XXX: For the Python version of io.StringIO, this is highly # dependent on the encoding used for the underlying buffer. - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 8 != 2 def test_widechar(self): buf = self.buftype("\U0002030a\U00020347") memio = self.ioclass(buf) @@ -969,7 +956,6 @@ def test_getstate(self): memio.close() self.assertRaises(ValueError, memio.__getstate__) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised by __setstate__ def test_setstate(self): # This checks whether __setstate__ does proper input validation. memio = self.ioclass() @@ -1006,47 +992,10 @@ def __str__(self): memio2.write(MyStr("world")) self.assertEqual(memio2.getvalue(), "hello world") - @unittest.expectedFailure # TODO: RUSTPYTHON; + - def test_issue5265(self): - return super().test_issue5265() - - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ++++ - def test_newline_empty(self): - return super().test_newline_empty() - - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^ - def test_newline_none(self): - return super().test_newline_none() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: OSError not raised by seek - def test_relative_seek(self): - return super().test_relative_seek() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised by writable def test_flags(self): return super().test_flags() - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'StringIO' object has no attribute 'detach' - def test_detach(self): - return super().test_detach() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'StringIO' object has no attribute 'newlines'. Did you mean: 'readlines'? - def test_newlines_property(self): - return super().test_newlines_property() - - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u64 - def test_seek(self): - return super().test_seek() - - @unittest.expectedFailure # TODO: RUSTPYTHON; d - def test_newline_cr(self): - return super().test_newline_cr() - - @unittest.expectedFailure # TODO: RUSTPYTHON; d - def test_newline_crlf(self): - return super().test_newline_crlf() - - class CStringIOPickleTest(PyStringIOPickleTest): UnsupportedOperation = io.UnsupportedOperation @@ -1056,34 +1005,5 @@ def __new__(cls, *args, **kwargs): def __init__(self, *args, **kwargs): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; + - def test_issue5265(self): - return super().test_issue5265() - - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ++++ - def test_newline_empty(self): - return super().test_newline_empty() - - @unittest.expectedFailure # TODO: RUSTPYTHON; ? ^^^^^ - def test_newline_none(self): - return super().test_newline_none() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: OSError not raised by seek - def test_relative_seek(self): - return super().test_relative_seek() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'StringIO' object has no attribute 'newlines'. Did you mean: 'readlines'? - def test_newlines_property(self): - return super().test_newlines_property() - - @unittest.expectedFailure # TODO: RUSTPYTHON; d - def test_newline_cr(self): - return super().test_newline_cr() - - @unittest.expectedFailure # TODO: RUSTPYTHON; d - def test_newline_crlf(self): - return super().test_newline_crlf() - - if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 7889fa88d00..707540f299d 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -664,7 +664,6 @@ def test_memoryview_hex(self): m2 = m1[::-1] self.assertEqual(m2.hex(), '30' * 200000) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument sep def test_memoryview_hex_separator(self): x = bytes(range(97, 102)) m1 = memoryview(x) @@ -798,7 +797,6 @@ def __bool__(self): m[0] = MyBool() self.assertEqual(ba[:8], b'\0'*8) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'memoryview' object has no attribute '__buffer__' def test_buffer_reference_loop(self): m = memoryview(b'abc').__buffer__(0) o = MyObject() diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 26fd366355f..c6b52ce331e 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -553,7 +553,6 @@ def testAttributeRepr(self): self.assertEqual(str(node), repr(node)) dom.unlink() - @unittest.expectedFailure # TODO: RUSTPYTHON def testWriteXML(self): str = '' dom = parseString(str) @@ -601,7 +600,6 @@ def test_toxml_quote_attrib(self): 'lflf=" " ' 'ws=" "/>') - @unittest.expectedFailure # TODO: RUSTPYTHON def testAltNewline(self): str = '\n\n' dom = parseString(str) @@ -659,7 +657,6 @@ def testProcessingInstruction(self): self.assertIsNone(pi.localName) self.assertEqual(pi.namespaceURI, xml.dom.EMPTY_NAMESPACE) - @unittest.expectedFailure # TODO: RUSTPYTHON def testProcessingInstructionRepr(self): dom = parseString('') pi = dom.documentElement.firstChild @@ -706,7 +703,6 @@ def testAttrListKeys(self): pass def testAttrListKeysNS(self): pass - @unittest.expectedFailure # TODO: RUSTPYTHON def testRemoveNamedItem(self): doc = parseString("") e = doc.documentElement @@ -716,7 +712,6 @@ def testRemoveNamedItem(self): self.assertTrue(a1.isSameNode(a2)) self.assertRaises(xml.dom.NotFoundErr, attrs.removeNamedItem, "a") - @unittest.expectedFailure # TODO: RUSTPYTHON def testRemoveNamedItemNS(self): doc = parseString("") e = doc.documentElement @@ -789,7 +784,6 @@ def _setupCloneElement(self, deep): root.setAttribute("added", "VALUE") return dom, clone - @unittest.expectedFailure # TODO: RUSTPYTHON def testCloneElementShallow(self): dom, clone = self._setupCloneElement(0) self.assertEqual(len(clone.childNodes), 0) @@ -941,11 +935,9 @@ def check_clone_attribute(self, deep, testName): self.confirm(clone.specified, testName + ": cloned attribute must have specified == True") - @unittest.expectedFailure # TODO: RUSTPYTHON def testCloneAttributeShallow(self): self.check_clone_attribute(0, "testCloneAttributeShallow") - @unittest.expectedFailure # TODO: RUSTPYTHON def testCloneAttributeDeep(self): self.check_clone_attribute(1, "testCloneAttributeDeep") @@ -957,11 +949,9 @@ def check_clone_pi(self, deep, testName): self.confirm(clone.target == pi.target and clone.data == pi.data) - @unittest.expectedFailure # TODO: RUSTPYTHON def testClonePIShallow(self): self.check_clone_pi(0, "testClonePIShallow") - @unittest.expectedFailure # TODO: RUSTPYTHON def testClonePIDeep(self): self.check_clone_pi(1, "testClonePIDeep") @@ -1219,7 +1209,6 @@ def testBug1433694(self): self.assertIsNone(node.childNodes[-1].nextSibling, "Final child's .nextSibling should be None") - @unittest.expectedFailure # TODO: RUSTPYTHON def testSiblings(self): doc = parseString("text?") root = doc.documentElement @@ -1340,7 +1329,6 @@ def checkRenameNodeSharedConstraints(self, doc, node): self.assertRaises(xml.dom.WrongDocumentErr, doc2.renameNode, node, xml.dom.EMPTY_NAMESPACE, "foo") - @unittest.expectedFailure # TODO: RUSTPYTHON def testRenameAttribute(self): doc = parseString("") elem = doc.documentElement @@ -1545,7 +1533,6 @@ def setup(): self.confirm(text is None and len(elem.childNodes) == 2) - @unittest.expectedFailure # TODO: RUSTPYTHON def testSchemaType(self): doc = parseString( "") e = doc.documentElement @@ -1611,7 +1597,6 @@ def testSetIdAttribute(self): self.confirm(e.isSameNode(doc.getElementById("w")) and a2.isId) - @unittest.expectedFailure # TODO: RUSTPYTHON def testSetIdAttributeNS(self): NS1 = "http://xml.python.org/ns1" NS2 = "http://xml.python.org/ns2" @@ -1648,7 +1633,6 @@ def testSetIdAttributeNS(self): self.confirm(e.isSameNode(doc.getElementById("w")) and a2.isId) - @unittest.expectedFailure # TODO: RUSTPYTHON def testSetIdAttributeNode(self): NS1 = "http://xml.python.org/ns1" NS2 = "http://xml.python.org/ns2" @@ -1770,7 +1754,6 @@ def testProcessingInstructionNameError(self): pi = doc.createProcessingInstruction("y", "z") pi.nodeValue = "crash" - @unittest.expectedFailure # TODO: RUSTPYTHON def test_minidom_attribute_order(self): xml_str = '' doc = parseString(xml_str) @@ -1778,13 +1761,11 @@ def test_minidom_attribute_order(self): doc.writexml(output) self.assertEqual(output.getvalue(), xml_str) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_toxml_with_attributes_ordered(self): xml_str = '' doc = parseString(xml_str) self.assertEqual(doc.toxml(), xml_str) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_toprettyxml_with_attributes_ordered(self): xml_str = '' doc = parseString(xml_str) @@ -1792,7 +1773,6 @@ def test_toprettyxml_with_attributes_ordered(self): '\n' '\n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_toprettyxml_with_cdata(self): xml_str = ']]>' doc = parseString(xml_str) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index 6b67c9e5074..a1c46706fee 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -33,13 +33,10 @@ def random_tagname(length=10): raise unittest.SkipTest("incompatible with Emscripten's mmap emulation.") +@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON; Errors on setUp") class MmapTests(unittest.TestCase): def setUp(self): - # TODO: RUSTPYTHON; Remove this once windows doesn't get errored on setup:/ - if os.name == "nt": - raise unittest.SkipTest("TODO: RUSTPYTHON; Error during class setUp") - if os.path.exists(TESTFN): os.unlink(TESTFN) diff --git a/Lib/test/test_module/__init__.py b/Lib/test/test_module/__init__.py index d4ed61648dd..22132b01c8a 100644 --- a/Lib/test/test_module/__init__.py +++ b/Lib/test/test_module/__init__.py @@ -151,13 +151,11 @@ def test_module_getattr_errors(self): if 'test.test_module.bad_getattr2' in sys.modules: del sys.modules['test.test_module.bad_getattr2'] - @unittest.expectedFailure # TODO: RUSTPYTHON def test_module_dir(self): import test.test_module.good_getattr as gga self.assertEqual(dir(gga), ['a', 'b', 'c']) del sys.modules['test.test_module.good_getattr'] - @unittest.expectedFailure # TODO: RUSTPYTHON def test_module_dir_errors(self): import test.test_module.bad_getattr as bga from test.test_module import bad_getattr2 diff --git a/Lib/test/test_modulefinder.py b/Lib/test/test_modulefinder.py index 51f7fd257e0..b64e684f805 100644 --- a/Lib/test/test_modulefinder.py +++ b/Lib/test/test_modulefinder.py @@ -390,8 +390,6 @@ def test_bytecode(self): os.remove(source_path) self._do_test(bytecode_test) - # TODO: RUSTPYTHON; panics at code.rs with 'called Option::unwrap() on a None value' - @unittest.skip("TODO: RUSTPYTHON; panics in co_filename replacement") def test_replace_paths(self): old_path = os.path.join(self.test_dir, 'a', 'module.py') new_path = os.path.join(self.test_dir, 'a', 'spam.py') diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 1f72b552c6c..30eee65dc12 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -1984,7 +1984,6 @@ def f(): ] return d["f"], expected - @unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls def test_method_call_error(self): nonopt_func, nonopt_expected = self._super_method_call_error(optimized=False) opt_func, opt_expected = self._super_method_call_error(optimized=True) @@ -2022,7 +2021,6 @@ def f(): ] return d["f"], expected - @unittest.expectedFailure # TODO: RUSTPYTHON; line number differences in multi-line super() calls def test_attr(self): nonopt_func, nonopt_expected = self._super_attr(optimized=False) opt_func, opt_expected = self._super_attr(optimized=True) diff --git a/Lib/test/test_msvcrt.py b/Lib/test/test_msvcrt.py index 1c6905bd1ee..fef86ce323e 100644 --- a/Lib/test/test_msvcrt.py +++ b/Lib/test/test_msvcrt.py @@ -4,6 +4,7 @@ import unittest from textwrap import dedent +from test import support from test.support import os_helper, requires_resource from test.support.os_helper import TESTFN, TESTFN_ASCII @@ -67,8 +68,12 @@ def run_in_separated_process(self, code): # Run test in a separated process to avoid stdin conflicts. # See: gh-110147 cmd = [sys.executable, '-c', code] - subprocess.run(cmd, check=True, capture_output=True, - creationflags=subprocess.CREATE_NEW_CONSOLE) + try: + subprocess.run(cmd, check=True, capture_output=True, + creationflags=subprocess.CREATE_NEW_CONSOLE) + except subprocess.CalledProcessError as exc: + support.skip_on_low_desktop_heap_memory_subprocess(exc.returncode) + raise def test_kbhit(self): code = dedent(''' diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index 4f92176b301..a859e051de2 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -4,35 +4,30 @@ class NamedExpressionInvalidTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_01(self): code = """x := 0""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_02(self): code = """x = y := 0""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_03(self): code = """y := f(x)""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_04(self): code = """y0 = y1 := f(x)""" with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_06(self): code = """((a, b) := (1, 2))""" @@ -370,7 +365,6 @@ def test_named_expression_invalid_dict_comprehension_iterable_expression(self): with self.assertRaisesRegex(SyntaxError, msg): exec(f"lambda: {code}", {}) # Function scope - @unittest.expectedFailure # TODO: RUSTPYTHON; wrong error message def test_named_expression_invalid_mangled_class_variables(self): code = """class Foo: def bar(self): diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index 8d359a646d9..5d0857b059e 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -6,6 +6,7 @@ import inspect import sys import unittest +from test import support @dataclasses.dataclass @@ -82,7 +83,6 @@ class S4(collections.UserList, dict, C): self.assertEqual(self.check_mapping_then_sequence(S3()), "seq") self.assertEqual(self.check_mapping_then_sequence(S4()), "seq") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_late_registration_mapping(self): class Parent: pass @@ -106,7 +106,6 @@ class GrandchildPost(ChildPost): self.assertEqual(self.check_mapping_then_sequence(ChildPost()), "map") self.assertEqual(self.check_mapping_then_sequence(GrandchildPost()), "map") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_late_registration_sequence(self): class Parent: pass @@ -2246,7 +2245,6 @@ def f(w): self.assertEqual(f(None), {}) self.assertEqual(f((1, 2)), {}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_patma_210(self): def f(w): match w: @@ -2566,15 +2564,14 @@ def test_patma_240(self): self.assertEqual(y, 0) self.assertEqual(z, {0: 1}) - # TODO: RUSTPYTHON - # def test_patma_241(self): - # x = [[{0: 0}]] - # match x: - # case list([({-0-0j: int(real=0+0j, imag=0-0j) | (1) as z},)]): - # y = 0 - # self.assertEqual(x, [[{0: 0}]]) - # self.assertEqual(y, 0) - # self.assertEqual(z, 0) + def test_patma_241(self): + x = [[{0: 0}]] + match x: + case list([({-0-0j: int(real=0+0j, imag=0-0j) | (1) as z},)]): + y = 0 + self.assertEqual(x, [[{0: 0}]]) + self.assertEqual(y, 0) + self.assertEqual(z, 0) def test_patma_242(self): x = range(3) @@ -2955,7 +2952,6 @@ def test_invalid_syntax_2(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_syntax_3(self): self.assert_syntax_error(""" match ...: @@ -3020,6 +3016,13 @@ def test_multiple_assignments_to_name_in_pattern_5(self): pass """) + def test_multiple_assignments_to_name_in_pattern_6(self): + self.assert_syntax_error(""" + match ...: + case a as a + 1: # NAME and expression with no () + pass + """) + def test_multiple_starred_names_in_sequence_pattern_0(self): self.assert_syntax_error(""" match ...: @@ -3075,7 +3078,6 @@ def test_name_capture_makes_remaining_patterns_unreachable_4(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_patterns_may_only_match_literals_and_attribute_lookups_0(self): self.assert_syntax_error(""" match ...: @@ -3083,7 +3085,6 @@ def test_patterns_may_only_match_literals_and_attribute_lookups_0(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_patterns_may_only_match_literals_and_attribute_lookups_1(self): self.assert_syntax_error(""" match ...: @@ -3126,7 +3127,6 @@ def test_real_number_multiple_ops(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_real_number_wrong_ops(self): for op in ["*", "/", "@", "**", "%", "//"]: with self.subTest(op=op): @@ -3202,7 +3202,6 @@ def test_mapping_pattern_duplicate_key(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_mapping_pattern_duplicate_key_edge_case0(self): self.assert_syntax_error(""" match ...: @@ -3210,7 +3209,6 @@ def test_mapping_pattern_duplicate_key_edge_case0(self): pass """) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_mapping_pattern_duplicate_key_edge_case1(self): self.assert_syntax_error(""" match ...: @@ -3225,8 +3223,6 @@ def test_mapping_pattern_duplicate_key_edge_case2(self): pass """) - - @unittest.expectedFailure # TODO: RUSTPYTHON def test_mapping_pattern_duplicate_key_edge_case3(self): self.assert_syntax_error(""" match ...: @@ -3258,7 +3254,6 @@ def test_accepts_positional_subpatterns_1(self): self.assertEqual(x, range(10)) self.assertIs(y, None) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_got_multiple_subpatterns_for_attribute_0(self): class Class: __match_args__ = ("a", "a") @@ -3273,7 +3268,6 @@ class Class: self.assertIs(y, None) self.assertIs(z, None) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_got_multiple_subpatterns_for_attribute_1(self): class Class: __match_args__ = ("a",) @@ -3379,7 +3373,6 @@ class A: class TestValueErrors(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_mapping_pattern_checks_duplicate_key_1(self): class Keys: KEY = "a" @@ -3506,6 +3499,7 @@ def f(command): # 0 self.assertListEqual(self._trace(f, 1), [1, 2, 3]) self.assertListEqual(self._trace(f, 0), [1, 2, 5, 6]) + @support.skip_wasi_stack_overflow() def test_parser_deeply_nested_patterns(self): # Deeply nested patterns can cause exponential backtracking when parsing. # See gh-93671 for more information. diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 8b2806781af..5eea014ccde 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1290,7 +1290,7 @@ def test_post_mortem_chained(): ... except Exception as e: ... pdb._post_mortem(e, instance) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE ... 'exceptions', ... 'exceptions 0', ... '$_exception', @@ -2133,7 +2133,7 @@ def test_pdb_asynctask(): >>> def test_function(): ... asyncio.run(test(), loop_factory=asyncio.EventLoop) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE + >>> with PdbTestInput([ # doctest: +ELLIPSIS ... '$_asynctask', ... 'continue', ... ]): @@ -2165,7 +2165,7 @@ def test_pdb_await_support(): >>> def test_function(): ... asyncio.run(main(), loop_factory=asyncio.EventLoop) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE + >>> with PdbTestInput([ # doctest: +ELLIPSIS ... 'x = await task', ... 'p x', ... 'x = await test()', @@ -2280,7 +2280,7 @@ def test_pdb_await_contextvar(): >>> def test_function(): ... asyncio.run(main(), loop_factory=asyncio.EventLoop) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> with PdbTestInput([ ... 'p var.get()', ... 'print(await get_var())', ... 'print(await asyncio.create_task(set_var(100)))', @@ -2768,7 +2768,7 @@ def test_pdb_multiline_statement(): >>> def test_function(): ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE ... 'def f(x):', ... ' return x * 2', ... '', @@ -3122,7 +3122,7 @@ def test_pdb_issue_gh_101673(): ... a = 1 ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE ... '!a = 2', ... 'll', ... 'p a', @@ -4185,7 +4185,6 @@ def test_blocks_at_first_code_line(self): self.assertTrue(any("__main__.py(4)()" in l for l in stdout.splitlines()), stdout) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_modified_after_execution(self): script = """ print("hello") @@ -4259,7 +4258,6 @@ def test_file_modified_after_execution_with_multiple_instances(self): self.assertIn("WARNING:", stdout) self.assertIn("was edited", stdout) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_modified_after_execution_with_restart(self): script = """ import random diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index eb251568767..14657dd2e77 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -157,7 +157,6 @@ def test_pack_unpack(self): self.assertNotInBytecode(code, 'UNPACK_SEQUENCE') self.check_lnotab(code) - @unittest.expectedFailure # TODO: RUSTPYTHON; LOAD_CONST count mismatch in long-tuple branch def test_constant_folding_tuples_of_constants(self): for line, elem in ( ('a = 1,2,3', (1, 2, 3)), diff --git a/Lib/test/test_pep646_syntax.py b/Lib/test/test_pep646_syntax.py index 8034bb9e935..aac089b190b 100644 --- a/Lib/test/test_pep646_syntax.py +++ b/Lib/test/test_pep646_syntax.py @@ -312,7 +312,7 @@ >>> f4.__annotations__ {'args': StarredB, 'arg1': } - >>> def f5(*args: *b = (1,)): pass # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> def f5(*args: *b = (1,)): pass Traceback (most recent call last): ... SyntaxError: invalid syntax @@ -321,8 +321,7 @@ __test__ = {'doctests' : doctests} def load_tests(loader, tests, pattern): - from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON - tests.addTest(doctest.DocTestSuite(checker=DocTestChecker())) # TODO: RUSTPYTHON + tests.addTest(doctest.DocTestSuite()) return tests diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py index f5444409593..d4faaaeca00 100644 --- a/Lib/test/test_pkgutil.py +++ b/Lib/test/test_pkgutil.py @@ -231,7 +231,6 @@ def test_walk_packages_raises_on_string_or_bytes_input(self): with self.assertRaises((TypeError, ValueError)): list(pkgutil.walk_packages(bytes_input)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_name_resolution(self): import logging import logging.handlers diff --git a/Lib/test/test_pty.py b/Lib/test/test_pty.py index 1126404fee7..c68162c94ca 100644 --- a/Lib/test/test_pty.py +++ b/Lib/test/test_pty.py @@ -195,6 +195,11 @@ def test_openpty(self): s2 = _readline(master_fd) self.assertEqual(b'For my pet fish, Eric.\n', normalize_output(s2)) + # skip (not expectedFailure) because the test still forks a real child + # process, which crashes on missing os.login_tty() inside the parallel + # test runner's worker process, corrupting its JSON reporting channel + # ("worker bug", reproducible under --slow-ci -j N; not under plain -m test). + @unittest.skip("TODO: RUSTPYTHON; pty.fork() calls os.login_tty(), which is not implemented") def test_fork(self): debug("calling pty.fork()") pid, master_fd = pty.fork() @@ -296,6 +301,11 @@ def test_master_read(self): self.assertEqual(data, b"") + # skip (not expectedFailure) because the test still forks a real child + # process, which crashes on missing os.login_tty() inside the parallel + # test runner's worker process, corrupting its JSON reporting channel + # ("worker bug", reproducible under --slow-ci -j N; not under plain -m test). + @unittest.skip("TODO: RUSTPYTHON; pty.fork() calls os.login_tty(), which is not implemented") def test_spawn_doesnt_hang(self): # gh-140482: Do the test in a pty.fork() child to avoid messing # with the interactive test runner's terminal settings. diff --git a/Lib/test/test_pulldom.py b/Lib/test/test_pulldom.py index f91fa1f8a0f..435b52d33eb 100644 --- a/Lib/test/test_pulldom.py +++ b/Lib/test/test_pulldom.py @@ -24,7 +24,6 @@ class PullDOMTestCase(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory (os error 2): 'xmltestdata/test.xml' -> 'None' def test_parse(self): """Minimal test of DOMEventStream.parse()""" diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 46f8ba60f8b..d206e5a910d 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -932,7 +932,6 @@ def test_synopsis(self): synopsis = pydoc.synopsis(TESTFN, {}) self.assertEqual(synopsis, 'line 1: h\xe9') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_source_synopsis(self): def check(source, expected, encoding=None): if isinstance(source, str): @@ -1709,7 +1708,6 @@ def test_bound_builtin_classmethod_unrepresentable_default(self): "classmeth(a, b=) class method of " "_testcapi.DocStringUnrepresentableSignatureTest") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_overridden_text_signature(self): class C: def meth(*args, **kwargs): @@ -1798,7 +1796,6 @@ def test_getset_descriptor(self): self.assertEqual(self._get_summary_line(Exception.args), "args") self.assertEqual(self._get_summary_line(memoryview.obj), "obj") - @unittest.expectedFailure # TODO: RUSTPYTHON @requires_docstrings def test_member_descriptor(self): # Currently these attributes are implemented as member descriptors diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 682abc520b3..e7046b79f7e 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -560,7 +560,6 @@ def test6(self): ["", "1", "", "", "2", "", "", "345", ""], "buffered text not properly split") - @unittest.expectedFailure # TODO: RUSTPYTHON def test7(self): self.setHandlers(["CommentHandler", "EndElementHandler", "StartElementHandler"]) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index e4f90db3304..65b1eed5bdd 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -117,7 +117,6 @@ def f(x, x): ... SyntaxError: duplicate argument 'x' in function definition""" self.assertIn(r, f.getvalue()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_runsource_shows_syntax_error_for_failed_compilation(self): console = InteractiveColoredConsole() source = "print('Hello, world!'" @@ -133,7 +132,6 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): console.runsource(source) mock_showsyntaxerror.assert_called_once() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_runsource_survives_null_bytes(self): console = InteractiveColoredConsole() source = "\x00\n" @@ -155,7 +153,6 @@ def test_no_active_future(self): self.assertFalse(result) self.assertEqual(f.getvalue(), "{'x': }\n") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_future_annotations(self): console = InteractiveColoredConsole() source = dedent("""\ @@ -210,7 +207,6 @@ def test_multiline_single_assignment(self): console = InteractiveColoredConsole(namespace, filename="") self.assertFalse(_more_lines(console, code)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_multiline_single_block(self): namespace = {} code = dedent("""\ @@ -227,7 +223,6 @@ def test_multiple_statements_single_line(self): console = InteractiveColoredConsole(namespace, filename="") self.assertFalse(_more_lines(console, code)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_multiple_statements(self): namespace = {} code = dedent("""\ @@ -237,7 +232,6 @@ def test_multiple_statements(self): console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_multiple_blocks(self): namespace = {} code = dedent("""\ @@ -285,7 +279,6 @@ def test_incomplete_statement(self): class TestWarnings(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_pep_765_warning(self): """ Test that a SyntaxWarning emitted from the diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 74735ef3c84..1bf3f9715b4 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -466,7 +466,6 @@ def prepare_reader(self, events): reader = ReadlineAlikeReader(console=console, config=config) return reader - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_default(self): # fmt: off input_code = ( @@ -486,7 +485,6 @@ def test_auto_indent_default(self): output = multiline_input(reader) self.assertEqual(output, output_code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_continuation(self): # auto indenting according to previous user indentation # fmt: off @@ -514,7 +512,6 @@ def test_auto_indent_continuation(self): output = multiline_input(reader) self.assertEqual(output, output_code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_prev_block(self): # auto indenting according to indentation in different block # fmt: off @@ -546,7 +543,6 @@ def test_auto_indent_prev_block(self): output2 = multiline_input(reader) self.assertEqual(output2, output_code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_multiline(self): # fmt: off events = itertools.chain( @@ -586,7 +582,6 @@ def test_auto_indent_multiline(self): output = multiline_input(reader) self.assertEqual(output, output_code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_with_comment(self): # fmt: off events = code_to_events( @@ -605,7 +600,6 @@ def test_auto_indent_with_comment(self): output = multiline_input(reader) self.assertEqual(output, output_code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_auto_indent_with_multicomment(self): # fmt: off events = code_to_events( @@ -680,7 +674,6 @@ def test_get_line_buffer_returns_str(self): wrapper = _ReadlineWrapper(f_in=None, f_out=None, reader=reader) self.assertIs(type(wrapper.get_line_buffer()), str) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_multiline_edit(self): events = itertools.chain( code_to_events("def f():\n...\n\n"), @@ -744,7 +737,6 @@ def test_history_navigation_with_up_arrow(self): self.assertEqual(output, "1+1") self.assert_screen_equal(reader, "1+1", clean=True) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_history_with_multiline_entries(self): code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n" events = list(itertools.chain( @@ -1426,7 +1418,6 @@ def test_paste_mid_newlines(self): output = multiline_input(reader) self.assertEqual(output, code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_paste_mid_newlines_not_in_paste_mode(self): # fmt: off code = ( @@ -1448,7 +1439,6 @@ def test_paste_mid_newlines_not_in_paste_mode(self): output = multiline_input(reader) self.assertEqual(output, expected) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_paste_not_in_paste_mode(self): # fmt: off input_code = ( diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index 33ef95accbd..51644ec7ce4 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -180,7 +180,6 @@ def test_up_arrow_after_ctrl_r(self): reader, _ = handle_all_events(events) self.assert_screen_equal(reader, "") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Lists differ def test_newline_within_block_trailing_whitespace(self): # fmt: off code = ( diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 8ac6daecc32..1cfddb8e19c 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -726,7 +726,6 @@ def test_groupdict(self): 'first second').groupdict(), {'first':'first', 'second':'second'}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expand(self): self.assertEqual(re.match("(?Pfirst) (?Psecond)", "first second") @@ -891,7 +890,6 @@ def test_named_unicode_escapes(self): self.checkPatternError(br'\N{LESS-THAN SIGN}', r'bad escape \N', 0) self.checkPatternError(br'[\N{LESS-THAN SIGN}]', r'bad escape \N', 1) - @unittest.expectedFailure # TODO: RUSTPYTHON; re.search(r"\B", "") now returns a match in CPython 3.14 def test_word_boundaries(self): # See http://bugs.python.org/issue10713 self.assertEqual(re.search(r"\b(abc)\b", "abc").group(1), "abc") @@ -1734,7 +1732,6 @@ def test_bug_817234(self): self.assertEqual(next(iter).span(), (4, 4)) self.assertRaises(StopIteration, next, iter) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bug_6561(self): # '\d' should match characters in Unicode category 'Nd' # (Number, Decimal Digit), but not those in 'Nl' (Number, @@ -1756,7 +1753,6 @@ def test_bug_6561(self): for x in not_decimal_digits: self.assertIsNone(re.match(r'^\d$', x)) - @unittest.expectedFailure # TODO: RUSTPYTHON; a = array.array(typecode)\n ValueError: bad typecode (must be b, B, u, h, H, i, I, l, L, q, Q, f or d) @warnings_helper.ignore_warnings(category=DeprecationWarning) # gh-80480 array('u') def test_empty_array(self): # SF buf 1647541 @@ -2496,7 +2492,6 @@ def test_search_anchor_at_beginning(self): # With optimization -- 0.0003 seconds. self.assertLess(stopwatch.seconds, 0.1) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_possessive_quantifiers(self): """Test Possessive Quantifiers Test quantifiers of the form @+ for some repetition operator @, @@ -2650,7 +2645,6 @@ def test_bug_gh100061(self): self.assertEqual(re.match("(?>(?:ab?c){1,3})", "aca").span(), (0, 2)) self.assertEqual(re.match("(?:ab?c){1,3}+", "aca").span(), (0, 2)) - @unittest.expectedFailure # TODO: RUSTPYTHON; self.assertEqual(re.match('((x)|y|z){3}+', 'xyz').groups(), ('z', 'x'))\n AssertionError: Tuples differ: ('x', 'x') != ('z', 'x') def test_bug_gh101955(self): # Possessive quantifier with nested alternative with capture groups self.assertEqual(re.match('((x)|y|z)*+', 'xyz').groups(), ('z', 'x')) diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 85db8e14c10..cb9ae24ccf2 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -2355,7 +2355,6 @@ def test_pass(self): self.check_executed_tests(output, testname, stats=1, parallel=True) self.assertNotIn('SPAM SPAM SPAM', output) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType' def test_xml(self): code = textwrap.dedent(r""" import unittest diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 03bf8d8b548..c80db832387 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -1,14 +1,35 @@ """Test the interactive interpreter.""" -import sys import os -import unittest +import select import subprocess +import sys +import unittest +from contextlib import contextmanager +from functools import partial from textwrap import dedent -from test.support import cpython_only, SuppressCrashReport +from test import support +from test.support import ( + cpython_only, + has_subprocess_support, + os_helper, + SuppressCrashReport, + SHORT_TIMEOUT, +) from test.support.script_helper import kill_python +from test.support.import_helper import import_module + +try: + import pty +except ImportError: + pty = None -def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): + +if not has_subprocess_support: + raise unittest.SkipTest("test module requires subprocess") + + +def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, isolated=True, **kw): """Run the Python REPL with the given arguments. kw is extra keyword args to pass to subprocess.Popen. Returns a Popen @@ -22,7 +43,14 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): # path may be used by Py_GetPath() to build the default module search # path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") - cmd_line = [stdin_fname, '-E', '-i'] + cmd_line = [stdin_fname] + # Isolated mode implies -EPs and ignores PYTHON* variables. + if isolated: + cmd_line.append('-I') + # Don't re-run the built-in REPL from interactive mode + # if we're testing a custom REPL (such as the asyncio REPL). + if not custom: + cmd_line.append('-i') cmd_line.extend(args) # Set TERM=vt100, for the rationale see the comments in spawn_python() of @@ -36,10 +64,47 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw): stdout=stdout, stderr=stderr, **kw) + +spawn_asyncio_repl = partial(spawn_repl, "-m", "asyncio", custom=True) + + +@contextmanager +def temp_pythonstartup(*, source: str, histfile: str = ".pythonhist"): + """Create environment variables for a PYTHONSTARTUP script in a temporary directory.""" + with os_helper.temp_dir() as tmpdir: + filename = os.path.join(tmpdir, "pythonstartup.py") + with open(filename, "w") as f: + f.write(source) + yield { + "PYTHONSTARTUP": filename, + "PYTHON_HISTORY": os.path.join(tmpdir, histfile) + } + + +def run_on_interactive_mode(source): + """Spawn a new Python interpreter, pass the given + input source code from the stdin and return the + result back. If the interpreter exits non-zero, it + raises a ValueError.""" + + process = spawn_repl() + process.stdin.write(source) + output = kill_python(process) + + if process.returncode != 0: + raise ValueError("Process didn't exit properly.") + return output + + +@support.force_not_colorized_test_class class TestInteractiveInterpreter(unittest.TestCase): @cpython_only + # Python built with Py_TRACE_REFS fail with a fatal error in + # _PyRefchain_Trace() on memory allocation error. + @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build') def test_no_memory(self): + import_module("_testcapi") # Issue #30696: Fix the interactive interpreter looping endlessly when # no memory. Check also that the fix does not break the interactive # loop when an exception is raised. @@ -92,6 +157,23 @@ def test_multiline_string_parsing(self): output = kill_python(p) self.assertEqual(p.returncode, 0) + @cpython_only + def test_lexer_buffer_realloc_with_null_start(self): + # gh-144759: NULL pointer arithmetic in the lexer when start and + # multi_line_start are NULL (uninitialized in tok_mode_stack[0]) + # and the lexer buffer is reallocated while parsing long input. + long_value = "a" * 2000 + user_input = dedent(f"""\ + x = f'{{{long_value!r}}}' + print(x) + """) + p = spawn_repl() + p.stdin.write(user_input) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertIn(long_value, output) + + @unittest.expectedFailureIf(sys.platform == "android", "TODO: RUSTPYTHON; AssertionError: 101 != 0") def test_close_stdin(self): user_input = dedent(''' import os @@ -107,6 +189,305 @@ def test_close_stdin(self): self.assertEqual(process.returncode, 0) self.assertIn('before close', output) + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 != 0 + def test_interactive_traceback_reporting(self): + user_input = "1 / 0 / 3 / 4" + p = spawn_repl() + p.stdin.write(user_input) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + + traceback_lines = output.splitlines()[-6:-1] + expected_lines = [ + "Traceback (most recent call last):", + " File \"\", line 1, in ", + " 1 / 0 / 3 / 4", + " ~~^~~", + "ZeroDivisionError: division by zero", + ] + self.assertEqual(traceback_lines, expected_lines) + + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 != 0 + def test_interactive_traceback_reporting_multiple_input(self): + user_input1 = dedent(""" + def foo(x): + 1 / x + + """) + p = spawn_repl() + p.stdin.write(user_input1) + user_input2 = "foo(0)" + p.stdin.write(user_input2) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + + traceback_lines = output.splitlines()[-8:-1] + expected_lines = [ + ' File "", line 1, in ', + ' foo(0)', + ' ~~~^^^', + ' File "", line 2, in foo', + ' 1 / x', + ' ~~^~~', + 'ZeroDivisionError: division by zero' + ] + self.assertEqual(traceback_lines, expected_lines) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_pythonstartup_error_reporting(self): + # errors based on https://github.com/python/cpython/issues/137576 + + def make_repl(env): + return subprocess.Popen( + [os.path.join(os.path.dirname(sys.executable), ''), "-i"], + executable=sys.executable, + text=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, + ) + + # case 1: error in user input, but PYTHONSTARTUP is fine + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('from pythonstartup')\n") + + env = os.environ.copy() + env['PYTHONSTARTUP'] = script + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") + p = make_repl(env) + p.stdin.write("1/0") + output = kill_python(p) + expected = dedent(""" + Traceback (most recent call last): + File "", line 1, in + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) + self.assertIn("from pythonstartup", output) + self.assertIn(expected, output) + + # case 2: error in PYTHONSTARTUP triggered by user input + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("def foo():\n 1/0\n") + + env = os.environ.copy() + env['PYTHONSTARTUP'] = script + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist") + p = make_repl(env) + p.stdin.write('foo()') + output = kill_python(p) + expected = dedent(""" + Traceback (most recent call last): + File "", line 1, in + foo() + ~~~^^ + File "%s", line 2, in foo + 1/0 + ~^~ + ZeroDivisionError: division by zero + """) % script + self.assertIn(expected, output) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_runsource_show_syntax_error_location(self): + user_input = dedent("""def f(x, x): ... + """) + p = spawn_repl() + p.stdin.write(user_input) + output = kill_python(p) + expected_lines = [ + ' def f(x, x): ...', + ' ^', + "SyntaxError: duplicate argument 'x' in function definition" + ] + self.assertEqual(output.splitlines()[4:-1], expected_lines) + + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 101 != 0 + def test_interactive_source_is_in_linecache(self): + user_input = dedent(""" + def foo(x): + return x + 1 + + def bar(x): + return foo(x) + 2 + """) + p = spawn_repl() + p.stdin.write(user_input) + user_input2 = dedent(""" + import linecache + print(linecache._interactive_cache[linecache._make_key(foo.__code__)]) + """) + p.stdin.write(user_input2) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'\')" + self.assertIn(expected, output, expected) + + def test_asyncio_repl_reaches_python_startup_script(self): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('pythonstartup done!')\n") + env = os.environ.copy() + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history") + env["PYTHONSTARTUP"] = script + p = spawn_asyncio_repl(isolated=False, env=env) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertIn("pythonstartup done!", output) + + def test_asyncio_repl_respects_isolated_mode(self): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('should not print')\n") + env = os.environ.copy() + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history") + env["PYTHONSTARTUP"] = script + p = spawn_asyncio_repl(isolated=True, env=env) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertNotIn("should not print", output) + + @unittest.skipUnless(pty, "requires pty") + def test_asyncio_repl_is_ok(self): + m, s = pty.openpty() + cmd = [sys.executable, "-I", "-m", "asyncio"] + env = os.environ.copy() + proc = subprocess.Popen( + cmd, + stdin=s, + stdout=s, + stderr=s, + text=True, + close_fds=True, + env=env, + ) + os.close(s) + os.write(m, b"await asyncio.sleep(0)\n") + os.write(m, b"exit()\n") + output = [] + while select.select([m], [], [], SHORT_TIMEOUT)[0]: + try: + data = os.read(m, 1024).decode("utf-8") + if not data: + break + except OSError: + break + output.append(data) + os.close(m) + try: + exit_code = proc.wait(timeout=SHORT_TIMEOUT) + except subprocess.TimeoutExpired: + proc.kill() + exit_code = proc.wait() + + self.assertEqual(exit_code, 0, "".join(output)) + + +@support.force_not_colorized_test_class +class TestInteractiveModeSyntaxErrors(unittest.TestCase): + + @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Process didn't exit properly. + def test_interactive_syntax_error_correct_line(self): + output = run_on_interactive_mode(dedent("""\ + def f(): + print(0) + return yield 42 + """)) + + traceback_lines = output.splitlines()[-4:-1] + expected_lines = [ + ' return yield 42', + ' ^^^^^', + 'SyntaxError: invalid syntax' + ] + self.assertEqual(traceback_lines, expected_lines) + + +class TestAsyncioREPL(unittest.TestCase): + def test_multiple_statements_fail_early(self): + user_input = "1 / 0; print(f'afterwards: {1+1}')" + p = spawn_asyncio_repl() + p.stdin.write(user_input) + output = kill_python(p) + self.assertIn("ZeroDivisionError", output) + self.assertNotIn("afterwards: 2", output) + + def test_toplevel_contextvars_sync(self): + user_input = dedent("""\ + from contextvars import ContextVar + var = ContextVar("var", default="failed") + var.set("ok") + """) + p = spawn_asyncio_repl() + p.stdin.write(user_input) + user_input2 = dedent(""" + print(f"toplevel contextvar test: {var.get()}") + """) + p.stdin.write(user_input2) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + expected = "toplevel contextvar test: ok" + self.assertIn(expected, output, expected) + + def test_toplevel_contextvars_async(self): + user_input = dedent("""\ + from contextvars import ContextVar + var = ContextVar('var', default='failed') + """) + p = spawn_asyncio_repl() + p.stdin.write(user_input) + user_input2 = "async def set_var(): var.set('ok')\n" + p.stdin.write(user_input2) + user_input3 = "await set_var()\n" + p.stdin.write(user_input3) + user_input4 = "print(f'toplevel contextvar test: {var.get()}')\n" + p.stdin.write(user_input4) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + expected = "toplevel contextvar test: ok" + self.assertIn(expected, output, expected) + + def test_quiet_mode(self): + p = spawn_repl("-q", "-m", "asyncio", custom=True) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertEqual(output[:3], ">>>") + + @support.force_not_colorized + @support.subTests( + ("startup_code", "expected_error"), + [ + ("some invalid syntax\n", "SyntaxError: invalid syntax"), + ("1/0\n", "ZeroDivisionError: division by zero"), + ], + ) + def test_pythonstartup_failure(self, startup_code, expected_error): + startup_env = self.enterContext( + temp_pythonstartup(source=startup_code, histfile=".asyncio_history")) + + p = spawn_repl( + "-qm", "asyncio", + env=os.environ | startup_env, + isolated=False, + custom=True) + p.stdin.write("print('user code', 'executed')\n") + output = kill_python(p) + self.assertEqual(p.returncode, 0) + + tb_hint = f'File "{startup_env["PYTHONSTARTUP"]}", line 1' + self.assertIn(tb_hint, output) + self.assertIn(expected_error, output) + + self.assertIn("user code executed", output) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py index db3d87bd17a..22a55b57c07 100644 --- a/Lib/test/test_reprlib.py +++ b/Lib/test/test_reprlib.py @@ -237,7 +237,6 @@ def test_nesting(self): eq(r([[[[[[{}]]]]]]), "[[[[[[{}]]]]]]") eq(r([[[[[[[{}]]]]]]]), "[[[[[[[...]]]]]]]") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_cell(self): def get_cell(): x = 42 diff --git a/Lib/test/test_resource.py b/Lib/test/test_resource.py index e2226e1a69d..6c7145caa93 100644 --- a/Lib/test/test_resource.py +++ b/Lib/test/test_resource.py @@ -151,7 +151,6 @@ def expected(cur): resource.setrlimit(resource.RLIMIT_FSIZE, (2**64-5, max)) self.assertIn(resource.getrlimit(resource.RLIMIT_FSIZE), expected(2**64-5)) - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u64 @unittest.skipIf(sys.platform == "vxworks", "setting RLIMIT_FSIZE is not supported on VxWorks") @unittest.skipUnless(hasattr(resource, 'RLIMIT_FSIZE'), 'requires resource.RLIMIT_FSIZE') diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py index faaf4dd95b6..e9e6b604d0d 100644 --- a/Lib/test/test_sax.py +++ b/Lib/test/test_sax.py @@ -190,7 +190,6 @@ def test_parse_bytes(self): with self.assertRaises(SAXException): self.check_parse(f) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_parse_path_object(self): make_xml_file(self.data, 'utf-8', None) self.check_parse(FakePath(TESTFN)) @@ -1018,7 +1017,6 @@ def test_expat_external_dtd_enabled(self): resolver.entities, [(None, 'unsupported://non-existing')] ) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_external_dtd_default(self): parser = create_parser() resolver = self.TestEntityRecorder() @@ -1084,7 +1082,6 @@ def startElement(self, name, attrs): def startElementNS(self, name, qname, attrs): self._attrs = attrs - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_attrs_empty(self): parser = create_parser() gather = self.AttrGatherer() @@ -1095,7 +1092,6 @@ def test_expat_attrs_empty(self): self.verify_empty_attrs(gather._attrs) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_attrs_wattr(self): parser = create_parser() gather = self.AttrGatherer() @@ -1106,7 +1102,6 @@ def test_expat_attrs_wattr(self): self.verify_attrs_wattr(gather._attrs) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_nsattrs_empty(self): parser = create_parser(1) gather = self.AttrGatherer() @@ -1300,7 +1295,6 @@ def test_flush_reparse_deferral_disabled(self): # ===== Locator support - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_locator_noinfo(self): result = BytesIO() xmlgen = XMLGenerator(result) @@ -1315,7 +1309,6 @@ def test_expat_locator_noinfo(self): self.assertEqual(parser.getPublicId(), None) self.assertEqual(parser.getLineNumber(), 1) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' def test_expat_locator_withinfo(self): result = BytesIO() xmlgen = XMLGenerator(result) @@ -1326,7 +1319,6 @@ def test_expat_locator_withinfo(self): self.assertEqual(parser.getSystemId(), TEST_XMLFILE) self.assertEqual(parser.getPublicId(), None) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing' @requires_nonascii_filenames def test_expat_locator_withinfo_nonascii(self): fname = os_helper.TESTFN_UNICODE diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index d88f2c598f1..42f11c9eb28 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -330,7 +330,6 @@ def test_cyclical_repr(self): name = repr(s).partition('(')[0] # strip class name self.assertEqual(repr(s), '%s({%s(...)})' % (name, name)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_do_not_rehash_dict_keys(self): n = 10 d = dict.fromkeys(map(HashCountingInt, range(n))) @@ -657,7 +656,6 @@ def test_set_membership(self): self.assertRaises(KeyError, myset.remove, set(range(1))) self.assertRaises(KeyError, myset.remove, set(range(3))) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_unhashable_element(self): myset = {'a'} elem = [1, 2, 3] @@ -829,7 +827,6 @@ class TestFrozenSetSubclass(TestFrozenSet): thetype = FrozenSetSubclass basetype = frozenset - @unittest.expectedFailure # TODO: RUSTPYTHON def test_keywords_in_subclass(self): class subclass(frozenset): pass diff --git a/Lib/test/test_shlex.py b/Lib/test/test_shlex.py index 7c41432b82f..2a355abdeeb 100644 --- a/Lib/test/test_shlex.py +++ b/Lib/test/test_shlex.py @@ -167,12 +167,10 @@ def testSplitNone(self): with self.assertRaises(ValueError): shlex.split(None) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Error Retrieving Value def testSplitPosix(self): """Test data splitting with posix parser""" self.splitTest(self.posix_data, comments=True) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Error Retrieving Value def testCompat(self): """Test compatibility interface""" for i in range(len(self.data)): @@ -313,7 +311,6 @@ def testEmptyStringHandling(self): s = shlex.shlex("'')abc", punctuation_chars=True) self.assertEqual(list(s), expected) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Error Retrieving Value def testUnicodeHandling(self): """Test punctuation_chars and whitespace_split handle unicode.""" ss = "\u2119\u01b4\u2602\u210c\u00f8\u1f24" @@ -356,7 +353,6 @@ def testJoin(self): joined = shlex.join(split_command) self.assertEqual(joined, command) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: Error Retrieving Value def testJoinRoundtrip(self): all_data = self.data + self.posix_data for command, *split_command in all_data: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ccc06cebac8..ddeb868db7b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -1181,7 +1181,6 @@ def testInterfaceNameIndex(self): self.assertIsInstance(_name, str) self.assertEqual(name, _name) - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u32 @unittest.skipUnless(hasattr(socket, 'if_indextoname'), 'socket.if_indextoname() not available.') @support.skip_android_selinux('if_indextoname') @@ -1249,7 +1248,6 @@ def testNtoH(self): self.assertEqual(swapped & mask, mask) self.assertRaises(OverflowError, func, 1<<34) - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust u16 def testNtoHErrors(self): s_good_values = [0, 1, 2, 0xffff] l_good_values = s_good_values + [0xffffffff] diff --git a/Lib/test/test_sqlite3/__init__.py b/Lib/test/test_sqlite3/__init__.py index 78a1e2078a5..145f3b80024 100644 --- a/Lib/test/test_sqlite3/__init__.py +++ b/Lib/test/test_sqlite3/__init__.py @@ -6,9 +6,14 @@ import os import sqlite3 +# make sure only print once +_printed_version = False + # Implement the unittest "load tests" protocol. -def load_tests(*args): - if verbose: +def load_tests(loader, tests, pattern): + global _printed_version + if verbose and not _printed_version: print(f"test_sqlite3: testing with SQLite version {sqlite3.sqlite_version}") + _printed_version = True pkg_dir = os.path.dirname(__file__) - return load_package_tests(pkg_dir, *args) + return load_package_tests(pkg_dir, loader, tests, pattern) diff --git a/Lib/test/test_sqlite3/test_backup.py b/Lib/test/test_sqlite3/test_backup.py index 9d31978b1ad..bc24831a0c7 100644 --- a/Lib/test/test_sqlite3/test_backup.py +++ b/Lib/test/test_sqlite3/test_backup.py @@ -103,7 +103,7 @@ def progress(status, remaining, total): self.assertEqual(len(journal), 1) self.assertEqual(journal[0], 0) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_non_callable_progress(self): with self.assertRaises(TypeError) as cm: with memory_database() as bck: diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 2174e14e7cb..ef8acd0f338 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -364,7 +364,6 @@ def test_use_after_close(self): with self.cx: pass - @unittest.expectedFailure # TODO: RUSTPYTHON def test_exceptions(self): # Optional DB-API extension. self.assertEqual(self.cx.Warning, sqlite.Warning) @@ -401,7 +400,6 @@ def test_in_transaction_ro(self): with self.assertRaises(AttributeError): self.cx.in_transaction = True - @unittest.expectedFailure # TODO: RUSTPYTHON def test_connection_exceptions(self): exceptions = [ "DataError", @@ -527,7 +525,6 @@ def test_connection_bad_reinit(self): cx.executemany, "insert into t values(?)", ((v,) for v in range(3))) - @unittest.expectedFailure # TODO: RUSTPYTHON SQLITE_DBCONFIG constants not implemented def test_connection_config(self): op = sqlite.SQLITE_DBCONFIG_ENABLE_FKEY with memory_database() as cx: @@ -552,7 +549,7 @@ def test_connection_config(self): with self.assertRaisesRegex(sqlite.IntegrityError, "constraint"): cx.execute("insert into u values(0)") - @unittest.expectedFailure # TODO: RUSTPYTHON deprecation warning not emitted for positional args + @unittest.expectedFailure # TODO: RUSTPYTHON; deprecation warning not emitted for positional args def test_connect_positional_arguments(self): regex = ( r"Passing more than 1 positional argument to sqlite3.connect\(\)" @@ -566,14 +563,14 @@ def test_connect_positional_arguments(self): cx.close() self.assertEqual(cm.filename, __file__) - @unittest.expectedFailure # TODO: RUSTPYTHON ResourceWarning not emitted + @unittest.expectedFailure # TODO: RUSTPYTHON; ResourceWarning not emitted def test_connection_resource_warning(self): with self.assertWarns(ResourceWarning): cx = sqlite.connect(":memory:") del cx gc_collect() - @unittest.expectedFailure # TODO: RUSTPYTHON Connection signature inspection not working + @unittest.expectedFailure # TODO: RUSTPYTHON; Connection signature inspection not working def test_connection_signature(self): from inspect import signature sig = signature(self.cx) @@ -584,7 +581,7 @@ class UninitialisedConnectionTests(unittest.TestCase): def setUp(self): self.cx = sqlite.Connection.__new__(sqlite.Connection) - @unittest.skip('TODO: RUSTPYTHON') + @unittest.skip("TODO: RUSTPYTHON") def test_uninit_operations(self): funcs = ( lambda: self.cx.isolation_level, @@ -726,7 +723,7 @@ def test_open_undecodable_uri(self): self.assertTrue(os.path.exists(path)) cx.execute(self._sql) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_factory_database_arg(self): def factory(database, *args, **kwargs): nonlocal database_arg @@ -875,7 +872,6 @@ def __getitem__(slf, x): with self.assertRaises(ZeroDivisionError): self.cu.execute("select name from test where name=?", L()) - @unittest.expectedFailure # TODO: RUSTPYTHON mixed named and positional parameters not validated def test_execute_named_param_and_sequence(self): dataset = ( ("select :a", (1,)), @@ -1396,6 +1392,11 @@ def test_blob_get_slice(self): def test_blob_get_empty_slice(self): self.assertEqual(self.blob[5:5], b"") + def test_blob_get_empty_slice_oob_indices(self): + self.cx.execute("insert into test(b) values (?)", (b"abc",)) + with self.cx.blobopen("test", "b", 2) as blob: + self.assertEqual(blob[5:-5], b"") + def test_blob_get_slice_negative_index(self): self.assertEqual(self.blob[5:-5], self.data[5:-5]) @@ -1412,6 +1413,18 @@ def test_blob_set_empty_slice(self): self.blob[0:0] = b"" self.assertEqual(self.blob[:], self.data) + def test_blob_set_empty_slice_wrong_type(self): + with self.assertRaises(TypeError): + self.blob[5:5] = None + + def test_blob_set_empty_slice_wrong_size(self): + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:5] = b"123" + + def test_blob_set_empty_slice_correct(self): + self.blob[5:5] = b"" + self.assertEqual(self.blob[:], self.data) + def test_blob_set_slice_with_skip(self): self.blob[0:10:2] = b"12345" actual = self.cx.execute("select b from test").fetchone()[0] @@ -1603,7 +1616,7 @@ def test_check_connection_thread(self): with self.subTest(fn=fn): self._run_test(fn) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_check_cursor_thread(self): fns = [ lambda: self.cur.execute("insert into test(name) values('a')"), @@ -1758,29 +1771,23 @@ def setUp(self): self.cur = self.con.cursor() self.con.close() - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_con_cursor(self): self.check(self.con.cursor) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_con_commit(self): self.check(self.con.commit) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_con_rollback(self): self.check(self.con.rollback) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_cur_execute(self): self.check(self.cur.execute, "select 4") - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_create_function(self): def f(x): return 17 self.check(self.con.create_function, "foo", 1, f) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_create_aggregate(self): class Agg: def __init__(self): @@ -1791,19 +1798,16 @@ def finalize(self): return 17 self.check(self.con.create_aggregate, "foo", 1, Agg) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_set_authorizer(self): def authorizer(*args): return sqlite.DENY self.check(self.con.set_authorizer, authorizer) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_set_progress_callback(self): def progress(): pass self.check(self.con.set_progress_handler, progress, 100) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for closed connection def test_closed_call(self): self.check(self.con) @@ -2037,7 +2041,6 @@ def test_row_equality(self): self.assertNotEqual(r1, r3) - @unittest.expectedFailure # TODO: RUSTPYTHON Row with no description fails def test_row_no_description(self): cu = self.cx.cursor() self.assertIsNone(cu.description) diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py index 74aacc05c2b..9ba71a49cfc 100644 --- a/Lib/test/test_sqlite3/test_dump.py +++ b/Lib/test/test_sqlite3/test_dump.py @@ -9,7 +9,7 @@ class DumpTests(MemoryDatabaseMixin, unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_table_dump(self): expected_sqls = [ "PRAGMA foreign_keys=OFF;", @@ -57,7 +57,7 @@ def test_table_dump(self): [self.assertEqual(expected_sqls[i], actual_sqls[i]) for i in range(len(expected_sqls))] - @unittest.expectedFailure # TODO: RUSTPYTHON iterdump filter parameter not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; iterdump filter parameter not implemented def test_table_dump_filter(self): all_table_sqls = [ """CREATE TABLE "some_table_2" ("id_1" INTEGER);""", @@ -128,7 +128,7 @@ def test_table_dump_filter(self): ["BEGIN TRANSACTION;", *all_table_sqls, *all_views_sqls, "COMMIT;"], ) - @unittest.expectedFailure # TODO: RUSTPYTHON _iterdump not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; _iterdump not implemented def test_dump_autoincrement(self): expected = [ 'CREATE TABLE "t1" (id integer primary key autoincrement);', @@ -149,7 +149,7 @@ def test_dump_autoincrement(self): actual = [stmt for stmt in self.cx.iterdump()] self.assertEqual(expected, actual) - @unittest.expectedFailure # TODO: RUSTPYTHON _iterdump not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; _iterdump not implemented def test_dump_autoincrement_create_new_db(self): self.cu.execute("BEGIN TRANSACTION") self.cu.execute("CREATE TABLE t1 (id integer primary key autoincrement)") @@ -175,7 +175,7 @@ def test_dump_autoincrement_create_new_db(self): rows = res.fetchall() self.assertEqual(rows[0][0], seq) - @unittest.expectedFailure # TODO: RUSTPYTHON _iterdump not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; _iterdump not implemented def test_unorderable_row(self): # iterdump() should be able to cope with unorderable row types (issue #15545) class UnorderableRow: @@ -197,7 +197,7 @@ def __getitem__(self, index): got = list(self.cx.iterdump()) self.assertEqual(expected, got) - @unittest.expectedFailure # TODO: RUSTPYTHON _iterdump not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; _iterdump not implemented def test_dump_custom_row_factory(self): # gh-118221: iterdump should be able to cope with custom row factories. def dict_factory(cu, row): @@ -213,7 +213,7 @@ def dict_factory(cu, row): self.assertEqual(expected, actual) self.assertEqual(self.cx.row_factory, dict_factory) - @unittest.expectedFailure # TODO: RUSTPYTHON _iterdump not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; _iterdump not implemented @requires_virtual_table("fts4") def test_dump_virtual_tables(self): # gh-64662 diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py index 2816bd91253..4345df7aef0 100644 --- a/Lib/test/test_sqlite3/test_factory.py +++ b/Lib/test/test_sqlite3/test_factory.py @@ -40,7 +40,7 @@ def __init__(self, *args, **kwargs): self.row_factory = dict_factory class ConnectionFactoryTests(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_connection_factories(self): class DefectFactory(sqlite.Connection): def __init__(self, *args, **kwargs): @@ -56,7 +56,7 @@ def __init__(self, *args, **kwargs): with memory_database(factory=DefectFactory) as con: self.assertIsInstance(con, DefectFactory) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_connection_factory_relayed_call(self): # gh-95132: keyword args must not be passed as positional args class Factory(sqlite.Connection): @@ -68,7 +68,7 @@ def __init__(self, *args, **kwargs): self.assertIsNone(con.isolation_level) self.assertIsInstance(con, Factory) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_connection_factory_as_positional_arg(self): class Factory(sqlite.Connection): def __init__(self, *args, **kwargs): @@ -90,9 +90,6 @@ def __init__(self, *args, **kwargs): class CursorFactoryTests(MemoryDatabaseMixin, unittest.TestCase): - def tearDown(self): - self.con.close() - def test_is_instance(self): cur = self.con.cursor() self.assertIsInstance(cur, sqlite.Cursor) @@ -131,7 +128,7 @@ def test_custom_factory(self): row = self.con.execute("select 1, 2").fetchone() self.assertIsInstance(row, list) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_sqlite_row_index(self): row = self.con.execute("select 1 as a_1, 2 as b").fetchone() self.assertIsInstance(row, sqlite.Row) @@ -162,7 +159,19 @@ def test_sqlite_row_index(self): with self.assertRaises(IndexError): row[complex()] # index must be int or string - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: can't delete attribute + def test_delete_connection_row_factory(self): + # gh-149738: deleting row_factory should raise an exception + with self.assertRaises(AttributeError): + del self.con.row_factory + + @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: can't delete attribute + def test_delete_connection_text_factory(self): + # gh-149738: deleting text_factory should raise an exception + with self.assertRaises(AttributeError): + del self.con.text_factory + + @unittest.expectedFailure # TODO: RUSTPYTHON def test_sqlite_row_index_unicode(self): row = self.con.execute("select 1 as \xff").fetchone() self.assertEqual(row["\xff"], 1) diff --git a/Lib/test/test_sqlite3/test_hooks.py b/Lib/test/test_sqlite3/test_hooks.py index c47cfab180d..e5b946bbaf2 100644 --- a/Lib/test/test_sqlite3/test_hooks.py +++ b/Lib/test/test_sqlite3/test_hooks.py @@ -24,11 +24,15 @@ import sqlite3 as sqlite import unittest +from test.support import import_helper from test.support.os_helper import TESTFN, unlink from .util import memory_database, cx_limit, with_tracebacks from .util import MemoryDatabaseMixin +# TODO(picnixz): increase test coverage for other callbacks +# such as 'func', 'step', 'finalize', and 'collation'. + class CollationTests(MemoryDatabaseMixin, unittest.TestCase): @@ -116,6 +120,21 @@ def test_collation_register_twice(self): self.assertEqual(result[0][0], 'b') self.assertEqual(result[1][0], 'a') + def test_collation_register_when_busy(self): + # See https://github.com/python/cpython/issues/146090. + con = self.con + con.create_collation("mycoll", lambda x, y: (x > y) - (x < y)) + con.execute("CREATE TABLE t(x TEXT)") + con.execute("INSERT INTO t VALUES (?)", ("a",)) + con.execute("INSERT INTO t VALUES (?)", ("b",)) + con.commit() + + cursor = self.con.execute("SELECT x FROM t ORDER BY x COLLATE mycoll") + next(cursor) + # Replace the collation while the statement is active -> SQLITE_BUSY. + with self.assertRaises(sqlite.OperationalError) as cm: + self.con.create_collation("mycoll", lambda a, b: 0) + def test_deregister_collation(self): """ Register a collation, then deregister it. Make sure an error is raised if we try @@ -129,8 +148,56 @@ def test_deregister_collation(self): self.assertEqual(str(cm.exception), 'no such collation sequence: mycoll') +class AuthorizerTests(MemoryDatabaseMixin, unittest.TestCase): + + def assert_not_authorized(self, func, /, *args, **kwargs): + with self.assertRaisesRegex(sqlite.DatabaseError, "not authorized"): + func(*args, **kwargs) + + # When a handler has an invalid signature, the exception raised is + # the same that would be raised if the handler "negatively" replied. + + def test_authorizer_invalid_signature(self): + self.cx.execute("create table if not exists test(a number)") + self.cx.set_authorizer(lambda: None) + self.assert_not_authorized(self.cx.execute, "select * from test") + + # Tests for checking that callback context mutations do not crash. + # Regression tests for https://github.com/python/cpython/issues/142830. + + @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'exc_type' + @with_tracebacks(ZeroDivisionError, regex="hello world") + def test_authorizer_concurrent_mutation_in_call(self): + self.cx.execute("create table if not exists test(a number)") + + def handler(*a, **kw): + self.cx.set_authorizer(None) + raise ZeroDivisionError("hello world") + + self.cx.set_authorizer(handler) + self.assert_not_authorized(self.cx.execute, "select * from test") + + @with_tracebacks(OverflowError) + def test_authorizer_concurrent_mutation_with_overflown_value(self): + _testcapi = import_helper.import_module("_testcapi") + self.cx.execute("create table if not exists test(a number)") + + def handler(*a, **kw): + self.cx.set_authorizer(None) + # We expect 'int' at the C level, so this one will raise + # when converting via PyLong_Int(). + return _testcapi.INT_MAX + 1 + + self.cx.set_authorizer(handler) + self.assert_not_authorized(self.cx.execute, "select * from test") + + class ProgressTests(MemoryDatabaseMixin, unittest.TestCase): + def assert_interrupted(self, func, /, *args, **kwargs): + with self.assertRaisesRegex(sqlite.OperationalError, "interrupted"): + func(*args, **kwargs) + def test_progress_handler_used(self): """ Test that the progress handler is invoked once it is set. @@ -196,7 +263,7 @@ def progress(): con.execute("select 1 union select 2 union select 3").fetchall() self.assertEqual(action, 0, "progress handler was not cleared") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON @with_tracebacks(ZeroDivisionError, msg_regex="bad_progress") def test_error_in_progress_handler(self): def bad_progress(): @@ -207,7 +274,7 @@ def bad_progress(): create table foo(a, b) """) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, msg_regex="bad_progress") def test_error_in_progress_handler_result(self): class BadBool: @@ -221,8 +288,8 @@ def bad_progress(): create table foo(a, b) """) - @unittest.expectedFailure # TODO: RUSTPYTHON keyword-only arguments not supported for set_progress_handler - def test_progress_handler_keyword_args(self): + @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_progress_handler + def test_set_progress_handler_keyword_args(self): regex = ( r"Passing keyword argument 'progress_handler' to " r"_sqlite3.Connection.set_progress_handler\(\) is deprecated. " @@ -234,6 +301,44 @@ def test_progress_handler_keyword_args(self): self.con.set_progress_handler(progress_handler=lambda: None, n=1) self.assertEqual(cm.filename, __file__) + # When a handler has an invalid signature, the exception raised is + # the same that would be raised if the handler "negatively" replied. + + def test_progress_handler_invalid_signature(self): + self.cx.execute("create table if not exists test(a number)") + self.cx.set_progress_handler(lambda x: None, 1) + self.assert_interrupted(self.cx.execute, "select * from test") + + # Tests for checking that callback context mutations do not crash. + # Regression tests for https://github.com/python/cpython/issues/142830. + + @unittest.skip("TODO: RUSTPYTHON; Timeout after 10 minutes") + @with_tracebacks(ZeroDivisionError, regex="hello world") + def test_progress_handler_concurrent_mutation_in_call(self): + self.cx.execute("create table if not exists test(a number)") + + def handler(*a, **kw): + self.cx.set_progress_handler(None, 1) + raise ZeroDivisionError("hello world") + + self.cx.set_progress_handler(handler, 1) + self.assert_interrupted(self.cx.execute, "select * from test") + + def test_progress_handler_concurrent_mutation_in_conversion(self): + self.cx.execute("create table if not exists test(a number)") + + class Handler: + def __bool__(_): + # clear the progress handler + self.cx.set_progress_handler(None, 1) + raise ValueError # force PyObject_True() to fail + + self.cx.set_progress_handler(Handler.__init__, 1) + self.assert_interrupted(self.cx.execute, "select * from test") + + # Running with tracebacks makes the second execution of this + # function raise another exception because of a database change. + class TraceCallbackTests(MemoryDatabaseMixin, unittest.TestCase): @@ -325,7 +430,7 @@ def test_trace_expanded_sql(self): cx.execute("create table t(t)") cx.executemany("insert into t values(?)", ((v,) for v in range(3))) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks( sqlite.DataError, regex="Expanded SQL string exceeds the maximum string length" @@ -350,15 +455,15 @@ def test_trace_too_much_expanded_sql(self): with self.check_stmt_trace(cx, [expanded_query]): cx.execute(unexpanded_query, (ok_param,)) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, regex="division by zero") def test_trace_bad_handler(self): with memory_database() as cx: cx.set_trace_callback(lambda stmt: 5/0) cx.execute("select 1") - @unittest.expectedFailure # TODO: RUSTPYTHON keyword-only arguments not supported for set_trace_callback - def test_trace_keyword_args(self): + @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_trace_callback + def test_set_trace_callback_keyword_args(self): regex = ( r"Passing keyword argument 'trace_callback' to " r"_sqlite3.Connection.set_trace_callback\(\) is deprecated. " @@ -370,6 +475,37 @@ def test_trace_keyword_args(self): self.con.set_trace_callback(trace_callback=lambda: None) self.assertEqual(cm.filename, __file__) + # When a handler has an invalid signature, the exception raised is + # the same that would be raised if the handler "negatively" replied, + # but for the trace handler, exceptions are never re-raised (only + # printed when needed). + + @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'exc_type' + @with_tracebacks( + TypeError, + regex=r".*\(\) missing 6 required positional arguments", + ) + def test_trace_handler_invalid_signature(self): + self.cx.execute("create table if not exists test(a number)") + self.cx.set_trace_callback(lambda x, y, z, t, a, b, c: None) + self.cx.execute("select * from test") + + # Tests for checking that callback context mutations do not crash. + # Regression tests for https://github.com/python/cpython/issues/142830. + + @unittest.skip("TODO: RUSTPYTHON; Timeout after 10 minutes") + @with_tracebacks(ZeroDivisionError, regex="hello world") + def test_trace_callback_concurrent_mutation_in_call(self): + self.cx.execute("create table if not exists test(a number)") + + def handler(statement): + # clear the progress handler + self.cx.set_trace_callback(None) + raise ZeroDivisionError("hello world") + + self.cx.set_trace_callback(handler) + self.cx.execute("select * from test") + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_sqlite3/test_regression.py b/Lib/test/test_sqlite3/test_regression.py index 0ebd6d5e9da..5560365fdc6 100644 --- a/Lib/test/test_sqlite3/test_regression.py +++ b/Lib/test/test_sqlite3/test_regression.py @@ -258,7 +258,7 @@ def collation_cb(a, b): # Lone surrogate cannot be encoded to the default encoding (utf8) "\uDC80", collation_cb) - @unittest.skip('TODO: RUSTPYTHON; recursive cursor use causes lock contention') + @unittest.skip("TODO: RUSTPYTHON; recursive cursor use causes lock contention") def test_recursive_cursor_use(self): """ http://bugs.python.org/issue10811 @@ -305,7 +305,6 @@ def test_convert_timestamp_microsecond_padding(self): datetime.datetime(2012, 4, 4, 15, 6, 0, 123456), ]) - @unittest.expectedFailure # TODO: RUSTPYTHON; error message mismatch def test_invalid_isolation_level_type(self): # isolation level is a string, not an integer regex = "isolation_level must be str or None" @@ -396,7 +395,7 @@ def test_del_isolation_level_segfault(self): with self.assertRaises(AttributeError): del self.con.isolation_level - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_bpo37347(self): class Printer: def log(self, *args): @@ -440,7 +439,7 @@ def test_table_lock_cursor_dealloc(self): con.execute("drop table t") con.commit() - @unittest.skip('TODO: RUSTPYTHON; recursive cursor use causes lock contention') + @unittest.skip("TODO: RUSTPYTHON; recursive cursor use causes lock contention") def test_table_lock_cursor_non_readonly_select(self): with memory_database() as con: con.execute("create table t(t)") @@ -469,7 +468,7 @@ def test_executescript_step_through_select(self): self.assertEqual(steps, values) -@unittest.skip('TODO: RUSTPYTHON; recursive cursor use causes lock contention') +@unittest.skip("TODO: RUSTPYTHON; recursive cursor use causes lock contention") class RecursiveUseOfCursors(unittest.TestCase): # GH-80254: sqlite3 should not segfault for recursive use of cursors. msg = "Recursive use of cursors not allowed" diff --git a/Lib/test/test_sqlite3/test_transactions.py b/Lib/test/test_sqlite3/test_transactions.py index 3b57b7f6a08..a3de7a7a82e 100644 --- a/Lib/test/test_sqlite3/test_transactions.py +++ b/Lib/test/test_sqlite3/test_transactions.py @@ -387,7 +387,6 @@ def test_autocommit_setget(self): cx.autocommit = mode self.assertEqual(cx.autocommit, mode) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit validation error messages differ def test_autocommit_setget_invalid(self): msg = "autocommit must be True, False, or.*LEGACY" for mode in "a", 12, (), None: @@ -395,7 +394,6 @@ def test_autocommit_setget_invalid(self): with self.assertRaisesRegex(ValueError, msg): sqlite.connect(":memory:", autocommit=mode) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_disabled(self): expected = [ "SELECT 1", @@ -411,7 +409,6 @@ def test_autocommit_disabled(self): cx.commit() cx.rollback() - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_disabled_implicit_rollback(self): expected = ["ROLLBACK"] with memory_database(autocommit=False) as cx: @@ -438,7 +435,6 @@ def test_autocommit_enabled_txn_ctl(self): meth() # expect this to pass silently self.assertFalse(cx.in_transaction) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_disabled_then_enabled(self): expected = ["COMMIT"] with memory_database(autocommit=False) as cx: @@ -472,7 +468,6 @@ def test_autocommit_enabled_ctx_mgr(self): self.assertFalse(cx.in_transaction) self.assertFalse(cx.in_transaction) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_disabled_ctx_mgr(self): expected = ["COMMIT", "BEGIN"] with memory_database(autocommit=False) as cx: @@ -492,7 +487,6 @@ def test_autocommit_compat_ctx_mgr(self): self.assertTrue(cx.in_transaction) self.assertFalse(cx.in_transaction) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_enabled_executescript(self): expected = ["BEGIN", "SELECT 1"] with memory_database(autocommit=True) as cx: @@ -502,7 +496,6 @@ def test_autocommit_enabled_executescript(self): cx.executescript("SELECT 1") self.assertTrue(cx.in_transaction) - @unittest.expectedFailure # TODO: RUSTPYTHON autocommit behavior differs def test_autocommit_disabled_executescript(self): expected = ["SELECT 1"] with memory_database(autocommit=False) as cx: diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 3fdde4a26cd..d63bccf9696 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -170,7 +170,6 @@ def setUp(self): def tearDown(self): self.con.close() - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for invalid num args def test_func_error_on_create(self): with self.assertRaisesRegex(sqlite.ProgrammingError, "not -100"): self.con.create_function("bla", -100, lambda x: 2*x) @@ -255,7 +254,7 @@ def test_func_return_nan(self): cur.execute("select returnnan()") self.assertIsNone(cur.fetchone()[0]) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, msg_regex="func_raiseexception") def test_func_exception(self): cur = self.con.cursor() @@ -264,7 +263,7 @@ def test_func_exception(self): cur.fetchone() self.assertEqual(str(cm.exception), 'user-defined function raised exception') - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(MemoryError, msg_regex="func_memoryerror") def test_func_memory_error(self): cur = self.con.cursor() @@ -272,7 +271,7 @@ def test_func_memory_error(self): cur.execute("select memoryerror()") cur.fetchone() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(OverflowError, msg_regex="func_overflowerror") def test_func_overflow_error(self): cur = self.con.cursor() @@ -306,7 +305,7 @@ def test_non_contiguous_blob(self): self.con.execute, "select spam(?)", (memoryview(b"blob")[::2],)) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(BufferError, regex="buffer.*contiguous") def test_return_non_contiguous_blob(self): with self.assertRaises(sqlite.OperationalError): @@ -385,7 +384,7 @@ def md5sum(t): del x,y gc_collect() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(OverflowError) def test_func_return_too_large_int(self): cur = self.con.cursor() @@ -395,7 +394,7 @@ def test_func_return_too_large_int(self): with self.assertRaisesRegex(sqlite.DataError, msg): cur.execute("select largeint()") - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(UnicodeEncodeError, "surrogates not allowed") def test_func_return_text_with_surrogates(self): cur = self.con.cursor() @@ -428,7 +427,7 @@ def test_func_return_illegal_value(self): self.assertRaisesRegex(sqlite.OperationalError, msg, self.con.execute, "select badreturn()") - @unittest.expectedFailure # TODO: RUSTPYTHON deprecation warning not emitted for keyword args + @unittest.expectedFailure # TODO: RUSTPYTHON; deprecation warning not emitted for keyword args def test_func_keyword_args(self): regex = ( r"Passing keyword arguments 'name', 'narg' and 'func' to " @@ -514,12 +513,11 @@ def test_win_sum_int(self): self.cur.execute(self.query % "sumint") self.assertEqual(self.cur.fetchall(), self.expected) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for invalid num args def test_win_error_on_create(self): with self.assertRaisesRegex(sqlite.ProgrammingError, "not -100"): self.con.create_window_function("shouldfail", -100, WindowSumInt) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(BadWindow) def test_win_exception_in_method(self): for meth in "__init__", "step", "value", "inverse": @@ -532,7 +530,7 @@ def test_win_exception_in_method(self): self.cur.execute(self.query % name) self.cur.fetchall() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(BadWindow) def test_win_exception_in_finalize(self): # Note: SQLite does not (as of version 3.38.0) propagate finalize @@ -544,7 +542,7 @@ def test_win_exception_in_finalize(self): self.cur.execute(self.query % name) self.cur.fetchall() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(AttributeError) def test_win_missing_method(self): class MissingValue: @@ -576,7 +574,7 @@ def finalize(self): return 42 self.cur.execute(self.query % name) self.cur.fetchall() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(AttributeError) def test_win_missing_finalize(self): # Note: SQLite does not (as of version 3.38.0) propagate finalize @@ -649,12 +647,11 @@ def setUp(self): def tearDown(self): self.con.close() - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs for invalid num args def test_aggr_error_on_create(self): with self.assertRaisesRegex(sqlite.ProgrammingError, "not -100"): self.con.create_function("bla", -100, AggrSum) - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(AttributeError, msg_regex="AggrNoStep") def test_aggr_no_step(self): cur = self.con.cursor() @@ -670,7 +667,7 @@ def test_aggr_no_finalize(self): cur.execute("select nofinalize(t) from test") val = cur.fetchone()[0] - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, msg_regex="AggrExceptionInInit") def test_aggr_exception_in_init(self): cur = self.con.cursor() @@ -679,7 +676,7 @@ def test_aggr_exception_in_init(self): val = cur.fetchone()[0] self.assertEqual(str(cm.exception), "user-defined aggregate's '__init__' method raised error") - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, msg_regex="AggrExceptionInStep") def test_aggr_exception_in_step(self): cur = self.con.cursor() @@ -688,7 +685,7 @@ def test_aggr_exception_in_step(self): val = cur.fetchone()[0] self.assertEqual(str(cm.exception), "user-defined aggregate's 'step' method raised error") - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ZeroDivisionError, msg_regex="AggrExceptionInFinalize") def test_aggr_exception_in_finalize(self): cur = self.con.cursor() @@ -754,7 +751,7 @@ def test_aggr_text(self): val = cur.fetchone()[0] self.assertEqual(val, txt) - @unittest.expectedFailure # TODO: RUSTPYTHON keyword-only arguments not supported for create_aggregate + @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for create_aggregate def test_agg_keyword_args(self): regex = ( r"Passing keyword arguments 'name', 'n_arg' and 'aggregate_class' to " @@ -803,13 +800,11 @@ def setUp(self): def tearDown(self): self.con.close() - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs def test_table_access(self): with self.assertRaises(sqlite.DatabaseError) as cm: self.con.execute("select * from t2") self.assertIn('prohibited', str(cm.exception)) - @unittest.expectedFailure # TODO: RUSTPYTHON error message differs def test_column_access(self): with self.assertRaises(sqlite.DatabaseError) as cm: self.con.execute("select c2 from t1") @@ -820,7 +815,7 @@ def test_clear_authorizer(self): self.con.execute("select * from t2") self.con.execute("select c2 from t1") - @unittest.expectedFailure # TODO: RUSTPYTHON keyword-only arguments not supported for set_authorizer + @unittest.expectedFailure # TODO: RUSTPYTHON; keyword-only arguments not supported for set_authorizer def test_authorizer_keyword_args(self): regex = ( r"Passing keyword argument 'authorizer_callback' to " @@ -843,12 +838,12 @@ def authorizer_cb(action, arg1, arg2, dbname, source): raise ValueError return sqlite.SQLITE_OK - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ValueError, msg_regex="authorizer_cb") def test_table_access(self): super().test_table_access() - @unittest.expectedFailure # TODO: RUSTPYTHON unraisable exception handling not implemented + @unittest.expectedFailure # TODO: RUSTPYTHON; unraisable exception handling not implemented @with_tracebacks(ValueError, msg_regex="authorizer_cb") def test_column_access(self): super().test_table_access() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7cfbe0c97dc..e1759f1aa6b 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1,5 +1,6 @@ # Test the support for SSL and sockets +import contextlib import sys import unittest import unittest.mock @@ -47,9 +48,20 @@ PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) HOST = socket_helper.HOST +IS_AWS_LC = "AWS-LC" in ssl.OPENSSL_VERSION IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0) PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS') +HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') +requires_keylog = unittest.skipUnless( + HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') +CAN_SET_KEYLOG = HAS_KEYLOG and os.name != "nt" +requires_keylog_setter = unittest.skipUnless( + CAN_SET_KEYLOG, + "cannot set 'keylog_filename' on Windows" +) + + PROTOCOL_TO_TLS_VERSION = {} for proto, ver in ( ("PROTOCOL_SSLv3", "SSLv3"), @@ -258,26 +270,67 @@ def utc_offset(): #NOTE: ignore issues like #1647654 ) -def test_wrap_socket(sock, *, - cert_reqs=ssl.CERT_NONE, ca_certs=None, - ciphers=None, certfile=None, keyfile=None, - **kwargs): - if not kwargs.get("server_side"): - kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - else: +def make_test_context( + *, + server_side=False, + check_hostname=None, + cert_reqs=ssl.CERT_NONE, + ca_certs=None, certfile=None, keyfile=None, + ciphers=None, + min_version=None, max_version=None, +): + if server_side: context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - if cert_reqs is not None: + else: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + + if check_hostname is None: if cert_reqs == ssl.CERT_NONE: context.check_hostname = False + else: + context.check_hostname = check_hostname + + if cert_reqs is not None: context.verify_mode = cert_reqs + if ca_certs is not None: context.load_verify_locations(ca_certs) if certfile is not None or keyfile is not None: context.load_cert_chain(certfile, keyfile) + if ciphers is not None: context.set_ciphers(ciphers) - return context.wrap_socket(sock, **kwargs) + + if min_version is not None: + context.minimum_version = min_version + if max_version is not None: + context.maximum_version = max_version + + return context + + +def test_wrap_socket( + sock, + *, + server_side=False, + check_hostname=None, + cert_reqs=ssl.CERT_NONE, + ca_certs=None, certfile=None, keyfile=None, + ciphers=None, + min_version=None, max_version=None, + **kwargs, +): + context = make_test_context( + server_side=server_side, + check_hostname=check_hostname, + cert_reqs=cert_reqs, + ca_certs=ca_certs, certfile=certfile, keyfile=keyfile, + ciphers=ciphers, + min_version=min_version, max_version=max_version, + ) + if not server_side: + kwargs.setdefault("server_hostname", SIGNED_CERTFILE_HOSTNAME) + return context.wrap_socket(sock, server_side=server_side, **kwargs) USE_SAME_TEST_CONTEXT = False @@ -317,6 +370,20 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True): return client_context, server_context, hostname +def do_ssl_object_handshake(sslobject, outgoing, max_retry=25): + """Call do_handshake() on the sslobject and return the sent data. + + If do_handshake() fails more than *max_retry* times, return None. + """ + data, attempt = None, 0 + while not data and attempt < max_retry: + with contextlib.suppress(ssl.SSLWantReadError): + sslobject.do_handshake() + data = outgoing.read() + attempt += 1 + return data + + class BasicSocketTests(unittest.TestCase): def test_constants(self): @@ -348,7 +415,6 @@ def test_options(self): value = getattr(ssl, name) self.assertGreaterEqual(value, 0, f"ssl.{name}") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by Certificate def test_ssl_types(self): ssl_types = [ _ssl._SSLContext, @@ -584,7 +650,6 @@ def test_timeout(self): with test_wrap_socket(s) as ss: self.assertEqual(timeout, ss.gettimeout()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_openssl111_deprecations(self): options = [ ssl.OP_NO_TLSv1, @@ -689,7 +754,6 @@ def test_tls_unique_channel_binding(self): with test_wrap_socket(s, server_side=True, certfile=CERTFILE) as ss: self.assertIsNone(ss.get_channel_binding("tls-unique")) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "" not found in "unclosed " def test_dealloc_warn(self): ss = test_wrap_socket(socket.socket(socket.AF_INET)) r = repr(ss) @@ -698,6 +762,7 @@ def test_dealloc_warn(self): support.gc_collect() self.assertIn(r, str(cm.warning.args[0])) + @unittest.expectedFailureIf(sys.platform == "android", "TODO: RUSTPYTHON; TypeError: path should be string, bytes, os.PathLike or integer, not NoneType") def test_get_default_verify_paths(self): paths = ssl.get_default_verify_paths() self.assertEqual(len(paths), 6) @@ -1035,7 +1100,6 @@ def test_hostname_checks_common_name(self): with self.assertRaises(AttributeError): ctx.hostname_checks_common_name = True - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: not found in {, , } @ignore_deprecation def test_min_max_version(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) @@ -1089,7 +1153,12 @@ def test_min_max_version(self): ctx.maximum_version = ssl.TLSVersion.MINIMUM_SUPPORTED self.assertIn( ctx.maximum_version, - {ssl.TLSVersion.TLSv1, ssl.TLSVersion.TLSv1_1, ssl.TLSVersion.SSLv3} + { + ssl.TLSVersion.TLSv1, + ssl.TLSVersion.TLSv1_1, + ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.SSLv3, + } ) ctx.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED @@ -1410,7 +1479,49 @@ def dummycallback(sock, servername, ctx): ctx.set_servername_callback(None) ctx.set_servername_callback(dummycallback) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: is not None + def test_sni_callback_on_dead_references(self): + # See https://github.com/python/cpython/issues/146080. + c_ctx = make_test_context() + c_inc, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = c_ctx.wrap_bio(c_inc, c_out, server_hostname=SIGNED_CERTFILE_HOSTNAME) + + def sni_callback(sock, servername, ctx): pass + sni_callback = unittest.mock.Mock(wraps=sni_callback) + s_ctx = make_test_context(server_side=True, certfile=SIGNED_CERTFILE) + s_ctx.set_servername_callback(sni_callback) + + s_inc, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + server = s_ctx.wrap_bio(s_inc, s_out, server_side=True) + server_impl = server._sslobj + + # Perform the handshake on the client side first. + data = do_ssl_object_handshake(client, c_out) + sni_callback.assert_not_called() + if data is None: + self.skipTest("cannot establish a handshake from the client") + s_inc.write(data) + sni_callback.assert_not_called() + # Delete the server object before it starts doing its handshake + # and ensure that we did not call the SNI callback yet. + del server + gc.collect() + # Try to continue the server's handshake by directly using + # the internal SSL object. The latter is a weak reference + # stored in the server context and has now a dead owner. + with self.assertRaises(ssl.SSLError) as cm: + server_impl.do_handshake() + # The SNI C callback raised an exception before calling our callback. + sni_callback.assert_not_called() + + # In AWS-LC, any handshake failures reports SSL_R_PARSE_TLSEXT, + # while OpenSSL uses SSL_R_CALLBACK_FAILED on SNI callback failures. + if IS_AWS_LC: + libssl_error_reason = "PARSE_TLSEXT" + else: + libssl_error_reason = "callback failed" + self.assertIn(libssl_error_reason, str(cm.exception)) + self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_SSL) + def test_sni_callback_refcycle(self): # Reference cycles through the servername callback are detected # and cleared. @@ -1423,6 +1534,59 @@ def dummycallback(sock, servername, ctx, cycle=ctx): gc.collect() self.assertIs(wr(), None) + @unittest.skipUnless(support.Py_GIL_DISABLED, + "test is only useful if the GIL is disabled") + @threading_helper.requires_working_threading() + def test_sni_callback_race(self): + # Replacing sni_callback while handshakes are in-flight must not + # crash (use-after-free on the callback in free-threaded builds). + client_ctx, server_ctx, hostname = testing_context() + + server_ctx.sni_callback = lambda *a: None + done = threading.Event() + + def do_handshakes(): + while not done.is_set(): + c_in = ssl.MemoryBIO() + c_out = ssl.MemoryBIO() + s_in = ssl.MemoryBIO() + s_out = ssl.MemoryBIO() + client = client_ctx.wrap_bio( + c_in, c_out, server_hostname=hostname) + server = server_ctx.wrap_bio(s_in, s_out, server_side=True) + for _ in range(50): + try: + client.do_handshake() + except ssl.SSLWantReadError: + pass + except ssl.SSLError: + break + if c_out.pending: + s_in.write(c_out.read()) + try: + server.do_handshake() + except ssl.SSLWantReadError: + pass + except ssl.SSLError: + break + if s_out.pending: + c_in.write(s_out.read()) + + def toggle_callback(): + while not done.is_set(): + server_ctx.sni_callback = lambda *a: None + server_ctx.sni_callback = None + + workers = max(4, (os.cpu_count() or 4) * 2) + threads = [threading.Thread(target=do_handshakes) + for _ in range(workers)] + threads.append(threading.Thread(target=toggle_callback)) + + with threading_helper.catch_threading_exception() as cm: + with threading_helper.start_threads(threads): + done.set() + self.assertIsNone(cm.exc_value) + def test_cert_store_stats(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) self.assertEqual(ctx.cert_store_stats(), @@ -1665,6 +1829,39 @@ def test_num_tickest(self): with self.assertRaises(ValueError): ctx.num_tickets = 1 + @support.cpython_only + def test_refcycle_msg_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context() + def msg_callback(*args, _=ctx, **kwargs): ... + ctx._msg_callback = msg_callback + + @support.cpython_only + @requires_keylog_setter + def test_refcycle_keylog_filename(self): + # See https://github.com/python/cpython/issues/142516. + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + ctx = make_test_context() + class KeylogFilename(str): ... + ctx.keylog_filename = KeylogFilename(os_helper.TESTFN) + ctx.keylog_filename._ = ctx + + @support.cpython_only + @unittest.skipUnless(ssl.HAS_PSK, 'requires TLS-PSK') + def test_refcycle_psk_client_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context() + def psk_client_callback(*args, _=ctx, **kwargs): ... + ctx.set_psk_client_callback(psk_client_callback) + + @support.cpython_only + @unittest.skipUnless(ssl.HAS_PSK, 'requires TLS-PSK') + def test_refcycle_psk_server_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context(server_side=True) + def psk_server_callback(*args, _=ctx, **kwargs): ... + ctx.set_psk_server_callback(psk_server_callback) + class SSLErrorTests(unittest.TestCase): @@ -4359,7 +4556,6 @@ def test_sendfile(self): s.sendfile(file) self.assertEqual(s.recv(1024), TEST_DATA) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_session(self): client_context, server_context, hostname = testing_context() # TODO: sessions aren't compatible with TLSv1.3 yet @@ -4417,7 +4613,6 @@ def test_session(self): self.assertEqual(sess_stat['accept'], 4) self.assertEqual(sess_stat['hits'], 2) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False != True def test_session_handling(self): client_context, server_context, hostname = testing_context() client_context2, _, _ = testing_context() @@ -4922,10 +5117,6 @@ def test_internal_chain_server(self): self.assertEqual(res, b'\x02\n') -HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') -requires_keylog = unittest.skipUnless( - HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') - class TestSSLDebug(unittest.TestCase): def keylog_lines(self, fname=os_helper.TESTFN): @@ -5164,15 +5355,27 @@ def non_linux_skip_if_other_okay_error(self, err): return # Expect the full test setup to always work on Linux. if (isinstance(err, ConnectionResetError) or (isinstance(err, OSError) and err.errno == errno.EINVAL) or - re.search('wrong.version.number', str(getattr(err, "reason", "")), re.I)): + re.search( + # Matches the following error messages: + # '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1123)' + # '[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1109)' + # '[SSL: HTTP_REQUEST] http request (_ssl.c:1143)' + r'wrong.version.number|record.layer.failure|http.request', + str(getattr(err, "reason", "")), + re.IGNORECASE, + ) + ): # On Windows the TCP RST leads to a ConnectionResetError # (ECONNRESET) which Linux doesn't appear to surface to userspace. # If wrap_socket() winds up on the "if connected:" path and doing - # the actual wrapping... we get an SSLError from OpenSSL. Typically - # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario - # we're specifically trying to test. The way this test is written - # is known to work on Linux. We'll skip it anywhere else that it - # does not present as doing so. + # the actual wrapping... we get an SSLError from OpenSSL. This is + # typically WRONG_VERSION_NUMBER. The same happens on iOS, but + # RECORD_LAYER_FAILURE or HTTP_REQUEST is the error. + # + # While appropriate, these scenarios aren't what we're specifically + # trying to test. The way this test is written is known to work on + # Linux. We'll skip it anywhere else that it does not present as + # doing so. try: self.skipTest(f"Could not recreate conditions on {sys.platform}:" f" {err=}") diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 5135564284e..2a3c36f2e57 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -1070,7 +1070,7 @@ def test_issue18183(self): '\U00100000'.ljust(3, '\U00010000') '\U00100000'.rjust(3, '\U00010000') - @unittest.expectedFailure # TODO: RUSTPYTHON; '{0:08s}'.format('result') misalign — '0' fill treated as numeric zero-pad for str type + @unittest.expectedFailure # TODO: RUSTPYTHON; '{0.}'.format() raises ValueError instead of IndexError def test_format(self): self.assertEqual(''.format(), '') self.assertEqual('a'.format(), 'a') diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index f8d3a4be27d..f828b778659 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -365,7 +365,6 @@ def test_p_code(self): (got,) = struct.unpack(code, got) self.assertEqual(got, expectedback) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_705836(self): # SF bug 705836. "f" had a severe rounding bug, where a carry # from the low-order discarded bits could propagate into the exponent @@ -499,12 +498,10 @@ def _test_pack_into(self, pack_into): with self.assertRaises((IndexError, OverflowError)): pack_into(writable_buf, -2**1000, test_string) - @unittest.expectedFailure # TODO: RUSTPYTHON; BufferError: non-contiguous buffer is not a bytes-like object def test_pack_into(self): s = struct.Struct('21s') self._test_pack_into(s.pack_into) - @unittest.expectedFailure # TODO: RUSTPYTHON; BufferError: non-contiguous buffer is not a bytes-like object def test_pack_into_fn(self): pack_into = lambda *args: struct.pack_into('21s', *args) self._test_pack_into(pack_into) @@ -604,7 +601,6 @@ def test_trailing_counter(self): 'spam and eggs') self.assertRaises(struct.error, struct.unpack_from, '14s42', store, 0) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '>h' != '>hh' def test_Struct_reinitialization(self): # Issue 9422: there was a memory leak when reinitializing a # Struct instance. This test can be used to detect the leak @@ -829,7 +825,6 @@ def test_error_propagation(fmt_str): test_error_propagation('N') test_error_propagation('n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_struct_subclass_instantiation(self): # Regression test for https://github.com/python/cpython/issues/112358 class MyStruct(struct.Struct): diff --git a/Lib/test/test_structseq.py b/Lib/test/test_structseq.py index 8ef6dd2fee8..d4014a784da 100644 --- a/Lib/test/test_structseq.py +++ b/Lib/test/test_structseq.py @@ -87,7 +87,6 @@ def test_fields(self): self.assertEqual(t.n_unnamed_fields, 0) self.assertEqual(t.n_fields, time._STRUCT_TM_ITEMS) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument dict def test_constructor(self): t = time.struct_time @@ -111,7 +110,6 @@ def test_constructor(self): s = "123456789" self.assertEqual("".join(t(s)), s) - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_constructor_with_duplicate_fields(self): t = time.struct_time @@ -125,7 +123,6 @@ def test_constructor_with_duplicate_fields(self): with self.assertRaisesRegex(TypeError, error_message): t("1234567890", dict={"error": 0, "tm_zone": "some zone", "tm_mon": 1}) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2 def test_constructor_with_duplicate_unnamed_fields(self): assert os.stat_result.n_unnamed_fields > 0 n_visible_fields = os.stat_result.n_sequence_fields @@ -142,7 +139,6 @@ def test_constructor_with_duplicate_unnamed_fields(self): re.escape("got duplicate or unexpected field name(s)")): os.stat_result((*range(n_visible_fields), -1.0), {'st_atime': -1.0}) - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_constructor_with_unknown_fields(self): t = time.struct_time @@ -185,7 +181,6 @@ def test_pickling(self): self.assertEqual(t2.tm_year, t.tm_year) self.assertEqual(t2.tm_zone, t.tm_zone) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2 def test_pickling_with_unnamed_fields(self): assert os.stat_result.n_unnamed_fields > 0 @@ -220,7 +215,6 @@ def test_copying(self): self.assertIsNot(t3[0], t[0]) self.assertIsNot(t3.tm_year, t.tm_year) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2 def test_copying_with_unnamed_fields(self): assert os.stat_result.n_unnamed_fields > 0 diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 2ba98616ea6..f237508fbf4 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -22,6 +22,7 @@ import sysconfig import select import shutil +import socket import threading import gc import textwrap @@ -1044,19 +1045,49 @@ def test_communicate_timeout_large_input(self): # On Windows, stdin writing must also honor the timeout rather than # blocking indefinitely when the pipe buffer fills. - # Input larger than typical pipe buffer (4-64KB on Windows) - input_data = b"x" * (128 * 1024) + input_data = b"x" * (128 * 1024) # > typical pipe buffer + + # Cross-platform wake mechanism: the slow reader connects to a + # loopback TCP socket and blocks in select() on it (capped at 9s + # as a safety net we don't expect to hit). After phase 1 raises + # TimeoutExpired, the parent sends a byte to release the child so + # it drains stdin. A socket (rather than a raw pipe) is required + # because Windows select() only supports sockets, not arbitrary + # file descriptors. + server = socket.create_server(('127.0.0.1', 0), backlog=1) + server.settimeout(10) # bound the accept() if the child fails to start + port = server.getsockname()[1] + # The child sends one byte (low byte of its PID) first so the parent + # can detect the rare case of an unrelated process on the same host + # connecting to our ephemeral port before our child does. A single + # byte gives 1/256 collision odds, which is plenty for flake-prevention. + slow_reader = ( + "import os, socket, sys, select; " + f"s = socket.create_connection(('127.0.0.1', {port}), timeout=9); " + "s.sendall(bytes([os.getpid() & 0xff])); " + "select.select([s], [], [], 9); " + "sys.stdout.buffer.write(sys.stdin.buffer.read())" + ) p = subprocess.Popen( - [sys.executable, "-c", - "import sys, time; " - "time.sleep(30); " # Don't read stdin for a long time - "sys.stdout.buffer.write(sys.stdin.buffer.read())"], + [sys.executable, "-c", slow_reader], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + conn = None try: + conn, _ = server.accept() + server.close() + server = None + + conn.settimeout(5) + peer_byte = conn.recv(1) + conn.settimeout(None) + self.assertEqual(peer_byte, bytes([p.pid & 0xff]), + f"loopback handshake byte {peer_byte!r} != " + f"low byte of child PID {p.pid} ({p.pid & 0xff:#x})") + timeout = 0.2 start = time.monotonic() try: @@ -1065,7 +1096,7 @@ def test_communicate_timeout_large_input(self): elapsed = time.monotonic() - start self.fail( f"TimeoutExpired not raised. communicate() completed in " - f"{elapsed:.2f}s, but subprocess sleeps for 30s. " + f"{elapsed:.2f}s, but slow reader stalls for up to 9s. " "Stdin writing blocked without enforcing timeout.") except subprocess.TimeoutExpired: elapsed = time.monotonic() - start @@ -1073,11 +1104,16 @@ def test_communicate_timeout_large_input(self): # Timeout should occur close to the specified timeout value, # not after waiting for the subprocess to finish sleeping. # Allow generous margin for slow CI, but must be well under - # the subprocess sleep time. + # the slow-reader's stall cap. self.assertLess(elapsed, 5.0, f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. " "Stdin writing blocked without checking timeout.") + # Release the slow reader so it stops blocking and drains stdin. + conn.sendall(b'go') + conn.close() + conn = None + # After timeout, continue communication. The remaining input # should be sent and we should receive all data back. stdout, stderr = p.communicate() @@ -1087,6 +1123,10 @@ def test_communicate_timeout_large_input(self): f"Expected {len(input_data)} bytes output but got {len(stdout)}") self.assertEqual(stdout, input_data) finally: + if conn is not None: + conn.close() + if server is not None: + server.close() p.kill() p.wait() @@ -3693,13 +3733,17 @@ def test_startupinfo_copy(self): self.assertEqual(startupinfo.wShowWindow, subprocess.SW_HIDE) self.assertEqual(startupinfo.lpAttributeList, {"handle_list": []}) + # CREATE_NEW_CONSOLE creates a "popup" window. + @support.requires_resource('gui') def test_creationflags(self): # creationflags argument CREATE_NEW_CONSOLE = 16 sys.stderr.write(" a DOS box should flash briefly ...\n") - subprocess.call(sys.executable + - ' -c "import time; time.sleep(0.25)"', - creationflags=CREATE_NEW_CONSOLE) + rc = subprocess.call(sys.executable + + ' -c "import time; time.sleep(0.25)"', + creationflags=CREATE_NEW_CONSOLE) + support.skip_on_low_desktop_heap_memory_subprocess(rc) + self.assertEqual(rc, 0) def test_invalid_args(self): # invalid arguments should raise ValueError diff --git a/Lib/test/test_super.py b/Lib/test/test_super.py index b53e38c77f4..4d338bbbc5a 100644 --- a/Lib/test/test_super.py +++ b/Lib/test/test_super.py @@ -112,7 +112,7 @@ def f(): __class__""", globals(), {}) self.assertIs(type(e.exception), NameError) # Not UnboundLocalError class X: - # global __class__ # TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before global declaration + global __class__ __class__ = 42 def f(): __class__ @@ -120,7 +120,7 @@ def f(): del globals()["__class__"] self.assertNotIn("__class__", X.__dict__) class X: - # nonlocal __class__ # TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before nonlocal declaration + nonlocal __class__ __class__ = 42 def f(): __class__ diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 6b3aa466d06..42aa7e3d9bb 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -458,7 +458,6 @@ def test_detect_api_mismatch__ignore(self): self.OtherClass, self.RefClass, ignore=ignore) self.assertEqual(set(), missing_items) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_check__all__(self): extra = {'tempdir'} not_exported = {'template'} @@ -632,7 +631,6 @@ def test_has_strftime_extensions(self): else: self.assertTrue(support.has_strftime_extensions) - @unittest.expectedFailure # TODO: RUSTPYTHON; - _testinternalcapi module not available def test_get_recursion_depth(self): # test support.get_recursion_depth() code = textwrap.dedent(""" diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 1653ab4a718..16204bc45dd 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -190,20 +190,14 @@ class SymtableTest(unittest.TestCase): foo = find_block(top, "foo") Alias = find_block(top, "Alias") GenericAlias = find_block(top, "GenericAlias") - # XXX: RUSTPYTHON - # GenericAlias_inner = find_block(GenericAlias, "GenericAlias") + GenericAlias_inner = find_block(GenericAlias, "GenericAlias") generic_spam = find_block(top, "generic_spam") - # XXX: RUSTPYTHON - # generic_spam_inner = find_block(generic_spam, "generic_spam") + generic_spam_inner = find_block(generic_spam, "generic_spam") GenericMine = find_block(top, "GenericMine") - # XXX: RUSTPYTHON - # GenericMine_inner = find_block(GenericMine, "GenericMine") - # XXX: RUSTPYTHON - # T = find_block(GenericMine, "T") - # XXX: RUSTPYTHON - # U = find_block(GenericMine, "U") + GenericMine_inner = find_block(GenericMine, "GenericMine") + T = find_block(GenericMine, "T") + U = find_block(GenericMine, "U") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_type(self): self.assertEqual(self.top.get_type(), "module") self.assertEqual(self.Mine.get_type(), "class") @@ -221,7 +215,6 @@ def test_type(self): self.assertEqual(self.T.get_type(), "type variable") self.assertEqual(self.U.get_type(), "type variable") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_id(self): self.assertGreater(self.top.get_id(), 0) self.assertGreater(self.Mine.get_id(), 0) @@ -254,7 +247,6 @@ def test_lineno(self): self.assertEqual(self.top.get_lineno(), 0) self.assertEqual(self.spam.get_lineno(), 14) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_function_info(self): func = self.spam self.assertEqual(sorted(func.get_parameters()), ["a", "b", "kw", "var"]) @@ -263,7 +255,6 @@ def test_function_info(self): self.assertEqual(sorted(func.get_globals()), ["bar", "glob", "some_assigned_global_var"]) self.assertEqual(self.internal.get_frees(), ("x",)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_globals(self): self.assertTrue(self.spam.lookup("glob").is_global()) self.assertFalse(self.spam.lookup("glob").is_declared_global()) @@ -276,14 +267,12 @@ def test_globals(self): self.assertTrue(self.top.lookup("some_non_assigned_global_var").is_global()) self.assertTrue(self.top.lookup("some_assigned_global_var").is_global()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_nonlocal(self): self.assertFalse(self.spam.lookup("some_var").is_nonlocal()) self.assertTrue(self.other_internal.lookup("some_var").is_nonlocal()) expected = ("some_var",) self.assertEqual(self.other_internal.get_nonlocals(), expected) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_local(self): self.assertTrue(self.spam.lookup("x").is_local()) self.assertFalse(self.spam.lookup("bar").is_local()) @@ -291,11 +280,9 @@ def test_local(self): self.assertTrue(self.top.lookup("some_non_assigned_global_var").is_local()) self.assertTrue(self.top.lookup("some_assigned_global_var").is_local()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_free(self): self.assertTrue(self.internal.lookup("x").is_free()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_referenced(self): self.assertTrue(self.internal.lookup("x").is_referenced()) self.assertTrue(self.spam.lookup("internal").is_referenced()) @@ -337,7 +324,6 @@ def test_assigned(self): self.assertTrue(self.Mine.lookup("a_method").is_assigned()) self.assertFalse(self.internal.lookup("x").is_assigned()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_annotated(self): st1 = symtable.symtable('def f():\n x: int\n', 'test', 'exec') st2 = st1.get_children()[1] @@ -365,7 +351,6 @@ def test_annotated(self): ' x: int', 'test', 'exec') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_imported(self): self.assertTrue(self.top.lookup("sys").is_imported()) @@ -375,7 +360,7 @@ def test_name(self): self.assertEqual(self.spam.lookup("x").get_name(), "x") self.assertEqual(self.Mine.get_name(), "Mine") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Tuples differ: () != ('a_method',) def test_class_get_methods(self): deprecation_mess = ( re.escape('symtable.Class.get_methods() is deprecated ' @@ -457,7 +442,7 @@ def check_body(body, expected_methods): check_body('\n'.join((gen, func)), ('genexpr',)) check_body('\n'.join((func, gen)), ('genexpr',)) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError: name 'x' is parameter and global def test_filename_correct(self): ### Bug tickler: SyntaxError file name correct whether error raised ### while parsing or building symbol table. @@ -489,7 +474,7 @@ def test_single(self): def test_exec(self): symbols = symtable.symtable("def f(x): return x", "?", "exec") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'str' but 'bytes' found. def test_bytes(self): top = symtable.symtable(TEST_CODE.encode('utf8'), "?", "exec") self.assertIsNotNone(find_block(top, "Mine")) @@ -503,7 +488,6 @@ def test_symtable_repr(self): self.assertEqual(str(self.top), "") self.assertEqual(str(self.spam), "") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_symbol_repr(self): self.assertEqual(repr(self.spam.lookup("glob")), "") @@ -579,7 +563,7 @@ def test_loopvar_in_only_one_scope(self): class CommandLineTest(unittest.TestCase): maxDiff = None - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Expected type 'str' but 'bytes' found. def test_file(self): filename = os_helper.TESTFN self.addCleanup(os_helper.unlink, filename) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 0934f22d470..5013eb096f5 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -59,15 +59,15 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> def __debug__(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def __debug__(): pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> async def __debug__(): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> async def __debug__(): pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> class __debug__: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> class __debug__: pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -75,7 +75,7 @@ Traceback (most recent call last): SyntaxError: cannot delete __debug__ ->>> f() = 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f() = 1 Traceback (most recent call last): SyntaxError: cannot assign to function call here. Maybe you meant '==' instead of '='? @@ -83,11 +83,11 @@ Traceback (most recent call last): SyntaxError: assignment to yield expression not possible ->>> del f() # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> del f() Traceback (most recent call last): SyntaxError: cannot delete function call ->>> a + 1 = 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> a + 1 = 2 Traceback (most recent call last): SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='? @@ -120,7 +120,7 @@ This test just checks a couple of cases rather than enumerating all of them. ->>> (a, "b", c) = (1, 2, 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, "b", c) = (1, 2, 3) Traceback (most recent call last): SyntaxError: cannot assign to literal @@ -168,15 +168,15 @@ Traceback (most recent call last): SyntaxError: expected 'else' after 'if' expression ->>> x = 1 if 1 else pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = 1 if 1 else pass Traceback (most recent call last): SyntaxError: expected expression after 'else', but statement is given ->>> x = pass if 1 else 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = pass if 1 else 1 Traceback (most recent call last): SyntaxError: expected expression before 'if', but statement is given ->>> x = pass if 1 else pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> x = pass if 1 else pass Traceback (most recent call last): SyntaxError: expected expression before 'if', but statement is given @@ -200,15 +200,15 @@ Traceback (most recent call last): SyntaxError: assignment to yield expression not possible ->>> a, b += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> a, b += 1, 2 Traceback (most recent call last): SyntaxError: 'tuple' is an illegal expression for augmented assignment ->>> (a, b) += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (a, b) += 1, 2 Traceback (most recent call last): SyntaxError: 'tuple' is an illegal expression for augmented assignment ->>> [a, b] += 1, 2 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [a, b] += 1, 2 Traceback (most recent call last): SyntaxError: 'list' is an illegal expression for augmented assignment @@ -243,7 +243,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> for i < (): pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> for i < (): pass Traceback (most recent call last): SyntaxError: invalid syntax @@ -285,11 +285,11 @@ Comprehensions without 'in' keyword: ->>> [x for x if range(1)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for x if range(1)] Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables ->>> tuple(x for x if range(1)) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> tuple(x for x if range(1)) Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables @@ -301,7 +301,7 @@ Traceback (most recent call last): SyntaxError: cannot assign to expression ->>> [x for a, b, (c + 1, d()) if y] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x for a, b, (c + 1, d()) if y] Traceback (most recent call last): SyntaxError: 'in' expected after for-loop variables @@ -316,11 +316,11 @@ Comprehensions creating tuples without parentheses should produce a specialized error message: ->>> [x,y for x,y in range(100)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x,y for x,y in range(100)] Traceback (most recent call last): SyntaxError: did you forget parentheses around the comprehension target? ->>> {x,y for x,y in range(100)} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {x,y for x,y in range(100)} Traceback (most recent call last): SyntaxError: did you forget parentheses around the comprehension target? @@ -385,7 +385,7 @@ # But prefixes of soft keywords should # still raise specialized errors ->>> (mat x) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (mat x) Traceback (most recent call last): SyntaxError: invalid syntax. Perhaps you forgot a comma? @@ -413,7 +413,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> def f(*None): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def f(*None): ... pass Traceback (most recent call last): SyntaxError: invalid syntax @@ -423,7 +423,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax ->>> def foo(/,a,b=,c): +>>> def foo(/,a,b=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... pass Traceback (most recent call last): SyntaxError: at least one argument must precede / @@ -468,12 +468,12 @@ Traceback (most recent call last): SyntaxError: var-positional argument cannot have default value ->>> def foo(a,**b=3): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,**b=3): ... pass Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value ->>> def foo(a,**b: int=3): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,**b: int=3): ... pass Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value @@ -523,22 +523,22 @@ Traceback (most recent call last): SyntaxError: * argument may appear only once ->>> def foo(a=1,/*,b,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a=1,/*,b,c): ... pass Traceback (most recent call last): SyntaxError: expected comma between / and * ->>> def foo(a=1,d=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a=1,d=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression ->>> def foo(a,d=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,d=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression ->>> def foo(a,d: int=,c): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> def foo(a,d: int=,c): ... pass Traceback (most recent call last): SyntaxError: expected default value expression @@ -571,7 +571,7 @@ Traceback (most recent call last): SyntaxError: / must be ahead of * ->>> lambda a=1,/*,b,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a=1,/*,b,c: None Traceback (most recent call last): SyntaxError: expected comma between / and * @@ -579,7 +579,7 @@ Traceback (most recent call last): SyntaxError: var-positional argument cannot have default value ->>> lambda a,**b=3: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a,**b=3: None Traceback (most recent call last): SyntaxError: var-keyword argument cannot have default value @@ -619,11 +619,11 @@ Traceback (most recent call last): SyntaxError: * argument may appear only once ->>> lambda a=1,d=,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a=1,d=,c: None Traceback (most recent call last): SyntaxError: expected default value expression ->>> lambda a,d=,c: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> lambda a,d=,c: None Traceback (most recent call last): SyntaxError: expected default value expression @@ -641,7 +641,7 @@ ... a, # type: int ... ): ... pass -... ''', type_comments=True) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +... ''', type_comments=True) Traceback (most recent call last): SyntaxError: bare * has associated type comment @@ -784,7 +784,7 @@ ... 290, 291, 292, 293, 294, 295, 296, 297, 298, 299) # doctest: +ELLIPSIS (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 297, 298, 299) ->>> f(lambda x: x[0] = 3) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(lambda x: x[0] = 3) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? @@ -796,25 +796,25 @@ The grammar accepts any test (basically, any expression) in the keyword slot of a call site. Test a few different options. ->>> f(x()=2) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(x()=2) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(a or b=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a or b=1) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(x.y=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(x.y=1) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f((x)=2) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f((x)=2) Traceback (most recent call last): SyntaxError: expression cannot contain assignment, perhaps you meant "=="? ->>> f(True=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(True=1) Traceback (most recent call last): SyntaxError: cannot assign to True ->>> f(False=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(False=1) Traceback (most recent call last): SyntaxError: cannot assign to False ->>> f(None=1) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(None=1) Traceback (most recent call last): SyntaxError: cannot assign to None >>> f(__debug__=1) @@ -826,42 +826,42 @@ >>> x.__debug__: int Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> f(a=) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a=) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(a, b, c=) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, c=) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(a, b, c=, d) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, c=, d) Traceback (most recent call last): SyntaxError: expected argument value expression ->>> f(*args=[0]) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(*args=[0]) Traceback (most recent call last): SyntaxError: cannot assign to iterable argument unpacking ->>> f(a, b, *args=[0]) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, *args=[0]) Traceback (most recent call last): SyntaxError: cannot assign to iterable argument unpacking ->>> f(**kwargs={'a': 1}) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(**kwargs={'a': 1}) Traceback (most recent call last): SyntaxError: cannot assign to keyword argument unpacking ->>> f(a, b, *args, **kwargs={'a': 1}) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f(a, b, *args, **kwargs={'a': 1}) Traceback (most recent call last): SyntaxError: cannot assign to keyword argument unpacking More set_context(): ->>> (x for x in x) += 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (x for x in x) += 1 Traceback (most recent call last): SyntaxError: 'generator expression' is an illegal expression for augmented assignment ->>> None += 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> None += 1 Traceback (most recent call last): SyntaxError: 'None' is an illegal expression for augmented assignment >>> __debug__ += 1 Traceback (most recent call last): SyntaxError: cannot assign to __debug__ >>> f() += 1 # TODO: RUSTPYTHON; Raises an exception # doctest: +SKIP -Traceback (most recent call last): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +Traceback (most recent call last): SyntaxError: 'function call' is an illegal expression for augmented assignment @@ -957,7 +957,7 @@ elif can't come after an else. - >>> if a % 2 == 0: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if a % 2 == 0: ... pass ... else: ... pass @@ -1185,7 +1185,7 @@ Traceback (most recent call last): SyntaxError: expected ':' - >>> with (blech as something) + >>> with (blech as something) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... pass Traceback (most recent call last): SyntaxError: expected ':' @@ -1195,12 +1195,12 @@ Traceback (most recent call last): SyntaxError: expected ':' - >>> with (blech, block as something) + >>> with (blech, block as something) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... pass Traceback (most recent call last): SyntaxError: expected ':' - >>> with (blech, block as something, bluch) + >>> with (blech, block as something, bluch) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE ... pass Traceback (most recent call last): SyntaxError: expected ':' @@ -1313,39 +1313,39 @@ Parenthesized arguments in function definitions - >>> def f(x, (y, z), w): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f(x, (y, z), w): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f((x, y, z, w)): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f((x, y, z, w)): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f(x, (y, z, w)): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f(x, (y, z, w)): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> def f((x, y, z), w): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def f((x, y, z), w): ... pass Traceback (most recent call last): SyntaxError: Function parameters cannot be parenthesized - >>> lambda x, (y, z), w: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda x, (y, z), w: None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda (x, y, z, w): None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda (x, y, z, w): None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda x, (y, z, w): None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda x, (y, z, w): None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized - >>> lambda (x, y, z), w: None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> lambda (x, y, z), w: None Traceback (most recent call last): SyntaxError: Lambda expression parameters cannot be parenthesized @@ -1361,7 +1361,7 @@ >>> try: ... pass - ... except TypeError as __debug__: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except TypeError as __debug__: ... pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -1410,28 +1410,28 @@ Better error message for using `except as` with not a name: - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except TypeError as obj.attr: ... pass Traceback (most recent call last): SyntaxError: cannot use except statement with attribute - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except TypeError as obj[1]: ... pass Traceback (most recent call last): SyntaxError: cannot use except statement with subscript - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except* TypeError as (obj, name): ... pass Traceback (most recent call last): SyntaxError: cannot use except* statement with tuple - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass ... except* TypeError as 1: ... pass @@ -1440,18 +1440,18 @@ Regression tests for gh-133999: - >>> try: pass - ... except TypeError as name: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except TypeError as name: raise from None Traceback (most recent call last): SyntaxError: invalid syntax - >>> try: pass - ... except* TypeError as name: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... except* TypeError as name: raise from None Traceback (most recent call last): SyntaxError: invalid syntax - >>> match 1: - ... case 1 | 2 as abc: raise from None # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match 1: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + ... case 1 | 2 as abc: raise from None Traceback (most recent call last): SyntaxError: invalid syntax @@ -1464,7 +1464,7 @@ Traceback (most recent call last): SyntaxError: invalid syntax - >>> dict(x=34, (x for x in range 10), 1); x $ y + >>> dict(x=34, (x for x in range 10), 1); x $ y # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE Traceback (most recent call last): SyntaxError: invalid syntax @@ -1474,27 +1474,27 @@ Incomplete dictionary literals - >>> {1:2, 3:4, 5} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:2, 3:4, 5} Traceback (most recent call last): SyntaxError: ':' expected after dictionary key - >>> {1:2, 3:4, 5:} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:2, 3:4, 5:} Traceback (most recent call last): SyntaxError: expression expected after dictionary key and ':' - >>> {1: *12+1, 23: 1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: *12+1, 23: 1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1: *12+1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: *12+1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1: 23, 1: *12+1} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: 23, 1: *12+1} Traceback (most recent call last): SyntaxError: cannot use a starred expression in a dictionary value - >>> {1:} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1:} Traceback (most recent call last): SyntaxError: expression expected after dictionary key and ':' @@ -1506,7 +1506,7 @@ # Ensure that the error is not raised for invalid expressions - >>> {1: 2, 3: foo(,), 4: 5} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> {1: 2, 3: foo(,), 4: 5} Traceback (most recent call last): SyntaxError: invalid syntax @@ -1516,48 +1516,48 @@ Specialized indentation errors: - >>> while condition: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while condition: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'while' statement on line 1 - >>> for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> for x in range(10): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'for' statement on line 1 - >>> for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> for x in range(10): ... pass ... else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 3 - >>> async for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async for x in range(10): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'for' statement on line 1 - >>> async for x in range(10): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async for x in range(10): ... pass ... else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 3 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'if' statement on line 1 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass ... elif something_else: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'elif' statement on line 3 - >>> if something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if something: ... pass ... elif something_else: ... pass @@ -1566,33 +1566,33 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'else' statement on line 5 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'try' statement on line 1 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except* A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'except*' statement on line 3 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except A: ... pass @@ -1601,7 +1601,7 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'finally' statement on line 5 - >>> try: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> try: ... something() ... except* A: ... pass @@ -1610,57 +1610,57 @@ Traceback (most recent call last): IndentationError: expected an indented block after 'finally' statement on line 5 - >>> with A: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> with A as a, B as b: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with A as a, B as b: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> with (A as a, B as b): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> with (A as a, B as b): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with A: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with A: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with A as a, B as b: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with A as a, B as b: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> async with (A as a, B as b): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> async with (A as a, B as b): ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'with' statement on line 1 - >>> def foo(x, /, y, *, z=2): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def foo(x, /, y, *, z=2): ... pass Traceback (most recent call last): IndentationError: expected an indented block after function definition on line 1 - >>> def foo[T](x, /, y, *, z=2): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> def foo[T](x, /, y, *, z=2): ... pass Traceback (most recent call last): IndentationError: expected an indented block after function definition on line 1 - >>> class Blech(A): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class Blech(A): ... pass Traceback (most recent call last): IndentationError: expected an indented block after class definition on line 1 - >>> class Blech[T](A): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class Blech[T](A): ... pass Traceback (most recent call last): IndentationError: expected an indented block after class definition on line 1 - >>> class C(__debug__=42): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class C(__debug__=42): ... Traceback (most recent call last): SyntaxError: cannot assign to __debug__ @@ -1668,23 +1668,23 @@ ... def __new__(*args, **kwargs): ... pass - >>> class C(metaclass=Meta, __debug__=42): # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class C(metaclass=Meta, __debug__=42): ... pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'match' statement on line 1 - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... case []: ... pass Traceback (most recent call last): IndentationError: expected an indented block after 'case' statement on line 2 - >>> match something: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match something: ... case []: ... ... ... case {}: @@ -1981,23 +1981,23 @@ Traceback (most recent call last): SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='? ->>> (x, y, z=3, d, e) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> (x, y, z=3, d, e) Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> [x, y, z=3, d, e] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [x, y, z=3, d, e] Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> [z=3] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> [z=3] Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> {x, y, z=3, d, e} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {x, y, z=3, d, e} Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? ->>> {z=3} # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {z=3} Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? @@ -2009,35 +2009,35 @@ Traceback (most recent call last): SyntaxError: trailing comma not allowed without surrounding parentheses ->>> import a from b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a from b Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z from b.y.z # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z from b.y.z Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a from b as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a from b as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z from b.y.z as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z from b.y.z as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a, b,c from b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a, b,c from b Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z, b.y.z, c.y.z from b.y.z # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z, b.y.z, c.y.z from b.y.z Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a,b,c from b as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a,b,c from b as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? ->>> import a.y.z, b.y.z, c.y.z from b.y.z as bar # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.y.z, b.y.z, c.y.z from b.y.z as bar Traceback (most recent call last): SyntaxError: Did you mean to use 'from ... import ...' instead? @@ -2061,19 +2061,19 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ ->>> import a as b.c # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a as b.c Traceback (most recent call last): SyntaxError: cannot use attribute as import target ->>> import a.b as (a, b) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.b as (a, b) Traceback (most recent call last): SyntaxError: cannot use tuple as import target ->>> import a, a.b as 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a, a.b as 1 Traceback (most recent call last): SyntaxError: cannot use literal as import target ->>> import a.b as 'a', a # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> import a.b as 'a', a Traceback (most recent call last): SyntaxError: cannot use literal as import target @@ -2081,7 +2081,7 @@ Traceback (most recent call last): SyntaxError: cannot use attribute as import target ->>> from a import b as 1 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b as 1 Traceback (most recent call last): SyntaxError: cannot use literal as import target @@ -2103,11 +2103,11 @@ Traceback (most recent call last): SyntaxError: cannot use tuple as import target ->>> from a import b, с as d[e] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import b, с as d[e] Traceback (most recent call last): SyntaxError: cannot use subscript as import target ->>> from a import с as d[e], b # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> from a import с as d[e], b Traceback (most recent call last): SyntaxError: cannot use subscript as import target @@ -2239,7 +2239,7 @@ Invalid pattern matching constructs: - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as _: ... ... Traceback (most recent call last): @@ -2251,13 +2251,13 @@ Traceback (most recent call last): SyntaxError: cannot use expression as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as a.b: ... ... Traceback (most recent call last): SyntaxError: cannot use attribute as pattern target - >>> match ...: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> match ...: ... case 42 as (a, b): ... ... Traceback (most recent call last): @@ -2307,7 +2307,7 @@ Traceback (most recent call last): ... SyntaxError: invalid syntax - >>> A[:(*b)] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[:(*b)] Traceback (most recent call last): ... SyntaxError: cannot use starred expression here @@ -2326,7 +2326,7 @@ Traceback (most recent call last): ... SyntaxError: invalid syntax - >>> A[(*b):] # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> A[(*b):] Traceback (most recent call last): ... SyntaxError: cannot use starred expression here @@ -2636,26 +2636,26 @@ def f(x: *b) Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> class A[__debug__]: pass # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[__debug__]: pass Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> class A[T]((x := 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((x := 3)): ... Traceback (most recent call last): ... SyntaxError: named expression cannot be used within the definition of a generic - >>> class A[T]((yield 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((yield 3)): ... Traceback (most recent call last): ... SyntaxError: yield expression cannot be used within the definition of a generic - >>> class A[T]((await 3)): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((await 3)): ... Traceback (most recent call last): ... SyntaxError: await expression cannot be used within the definition of a generic - >>> class A[T]((yield from [])): ... # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> class A[T]((yield from [])): ... Traceback (most recent call last): ... SyntaxError: yield expression cannot be used within the definition of a generic @@ -2664,23 +2664,23 @@ def f(x: *b) Traceback (most recent call last): SyntaxError: iterable argument unpacking follows keyword argument unpacking - >>> f(**x, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(**x, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x, *:) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x, *:) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x = 5, *) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x = 5, *) Traceback (most recent call last): SyntaxError: Invalid star expression - >>> f(x = 5, *:) # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> f(x = 5, *:) Traceback (most recent call last): SyntaxError: Invalid star expression """ @@ -2702,7 +2702,6 @@ def check_warning(self, code, errtext, filename="", mode="exec"): with self.assertWarnsRegex(SyntaxWarning, errtext): compile(code, filename, mode) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxWarning not triggered def test_return_in_finally(self): source = textwrap.dedent(""" def f(): @@ -2737,7 +2736,6 @@ def f(): """) self.check_warning(source, "'return' in a 'finally' block") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxWarning not triggered def test_break_and_continue_in_finally(self): for kw in ('break', 'continue'): @@ -2807,7 +2805,6 @@ def _check_error(self, code, errtext, else: self.fail("compile() did not raise SyntaxError") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expression_with_assignment(self): self._check_error( "print(end1 + end2 = ' ')", @@ -2821,7 +2818,6 @@ def test_curly_brace_after_primary_raises_immediately(self): def test_assign_call(self): self._check_error("f() = 1", "assign") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_assign_del(self): self._check_error("del (,)", "invalid syntax") self._check_error("del 1", "cannot delete literal") @@ -2955,13 +2951,11 @@ def test_generator_in_function_call(self): "Generator expression must be parenthesized", lineno=1, end_lineno=1, offset=11, end_offset=53) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_except_then_except_star(self): self._check_error("try: pass\nexcept ValueError: pass\nexcept* TypeError: pass", r"cannot have both 'except' and 'except\*' on the same 'try'", lineno=3, end_lineno=3, offset=1, end_offset=8) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_except_star_then_except(self): self._check_error("try: pass\nexcept* ValueError: pass\nexcept TypeError: pass", r"cannot have both 'except' and 'except\*' on the same 'try'", @@ -3109,7 +3103,6 @@ def func2(): """ self._check_error(code, "expected ':'") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_line_continuation_error_position(self): self._check_error(r"a = 3 \ 4", "unexpected character after line continuation character", @@ -3129,7 +3122,6 @@ def test_invalid_line_continuation_left_recursive(self): self._check_error("A.\u03bc\\\n", "unexpected EOF while parsing") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_error_parenthesis(self): for paren in "([{": self._check_error(paren + "1 + 2", f"\\{paren}' was never closed") @@ -3155,7 +3147,6 @@ def test_error_parenthesis(self): s = b'# coding=latin\n(aaaaaaaaaaaaaaaaa\naaaaaaaaaaa\xb5' self._check_error(s, r"'\(' was never closed") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_error_string_literal(self): self._check_error("'blech", r"unterminated string literal \(.*\)$") @@ -3169,7 +3160,6 @@ def test_error_string_literal(self): self._check_error("'''blech", "unterminated triple-quoted string literal") self._check_error('"""blech', "unterminated triple-quoted string literal") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_invisible_characters(self): self._check_error('print\x17("Hello")', "invalid non-printable character") self._check_error(b"with(0,,):\n\x01", "invalid non-printable character") @@ -3252,7 +3242,6 @@ def test_deep_invalid_rule(self): with self.assertRaises(SyntaxError): compile(source, "", "exec") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_except_stmt_invalid_as_expr(self): self._check_error( textwrap.dedent( @@ -3270,7 +3259,6 @@ def test_except_stmt_invalid_as_expr(self): end_offset=22 + len("obj.attr"), ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_match_stmt_invalid_as_expr(self): self._check_error( textwrap.dedent( @@ -3287,7 +3275,6 @@ def test_match_stmt_invalid_as_expr(self): end_offset=15 + len("obj.attr"), ) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_else_stmt(self): msg = "expected expression after 'else', but statement is given" @@ -3308,7 +3295,6 @@ def test_ifexp_else_stmt(self): ]: self._check_error(f"x = 1 if 1 else {stmt}", msg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_body_stmt_else_expression(self): msg = "expected expression before 'if', but statement is given" @@ -3319,7 +3305,6 @@ def test_ifexp_body_stmt_else_expression(self): ]: self._check_error(f"x = {stmt} if 1 else 1", msg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_ifexp_body_stmt_else_stmt(self): msg = "expected expression before 'if', but statement is given" for lhs_stmt, rhs_stmt in [ diff --git a/Lib/test/test_sys_setprofile.py b/Lib/test/test_sys_setprofile.py index 813adff2a32..d0d2b0c3e01 100644 --- a/Lib/test/test_sys_setprofile.py +++ b/Lib/test/test_sys_setprofile.py @@ -169,7 +169,6 @@ def g(p): (1, 'return', g_ident), ]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_exception_propagation(self): def f(p): 1/0 diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 0e6b9bced3f..42d1cf2832c 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -332,7 +332,9 @@ def test_read_only_directory(self): with _inside_empty_temp_dir(): probe = os.path.join(tempfile.tempdir, 'probe') if os.name == 'nt': - cmd = ['icacls', tempfile.tempdir, '/deny', 'Everyone:(W)'] + # Use security identifier *S-1-1-0 instead + # of localized "Everyone" to not depend on the locale. + cmd = ['icacls', tempfile.tempdir, '/deny', '*S-1-1-0:(W)'] stdout = None if support.verbose > 1 else subprocess.DEVNULL subprocess.run(cmd, check=True, stdout=stdout) else: @@ -355,7 +357,9 @@ def test_read_only_directory(self): self.make_temp() finally: if os.name == 'nt': - cmd = ['icacls', tempfile.tempdir, '/grant:r', 'Everyone:(M)'] + # Use security identifier *S-1-1-0 instead + # of localized "Everyone" to not depend on the locale. + cmd = ['icacls', tempfile.tempdir, '/grant:r', '*S-1-1-0:(M)'] subprocess.run(cmd, check=True, stdout=stdout) else: os.chmod(tempfile.tempdir, oldmode) @@ -1747,7 +1751,7 @@ def test_cleanup_with_symlink_to_a_directory(self): d2.cleanup() @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; flaky, sometimes pollute env on CI") - @unittest.expectedFailureIf(sys.platform in ("android", "linux"), "TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory: ''") + @unittest.expectedFailureIf(sys.platform in ('android', 'linux'), "TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory: ''") @os_helper.skip_unless_symlink def test_cleanup_with_symlink_modes(self): # cleanup() should not follow symlinks when fixing mode bits (#91133) diff --git a/Lib/test/test_termios.py b/Lib/test/test_termios.py index 216609719ac..1207855fa2d 100644 --- a/Lib/test/test_termios.py +++ b/Lib/test/test_termios.py @@ -221,7 +221,6 @@ def writer(): 'output was not resumed') self.assertEqual(os.read(rfd, 1024), b'def') - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcgetwinsize(self): size = termios.tcgetwinsize(self.fd) self.assertIsInstance(size, tuple) @@ -230,7 +229,6 @@ def test_tcgetwinsize(self): self.assertIsInstance(size[1], int) self.assertEqual(termios.tcgetwinsize(self.stream), size) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcgetwinsize_errors(self): self.assertRaisesTermiosError(errno.ENOTTY, termios.tcgetwinsize, self.bad_fd) self.assertRaises(ValueError, termios.tcgetwinsize, -1) @@ -238,14 +236,12 @@ def test_tcgetwinsize_errors(self): self.assertRaises(TypeError, termios.tcgetwinsize, object()) self.assertRaises(TypeError, termios.tcgetwinsize) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcsetwinsize(self): size = termios.tcgetwinsize(self.fd) termios.tcsetwinsize(self.fd, size) termios.tcsetwinsize(self.fd, list(size)) termios.tcsetwinsize(self.stream, size) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'termios' has no attribute 'tcgetwinsize' def test_tcsetwinsize_errors(self): size = termios.tcgetwinsize(self.fd) self.assertRaises(TypeError, termios.tcsetwinsize, self.fd, size[:-1]) diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index dc55174421a..ebb193eabda 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -115,7 +115,7 @@ def test_nt_and_posix_stack_size(self): thread.stack_size(0) - @unittest.skipIf(__import__("sys").platform in ("linux", "win32"), "TODO: RUSTPYTHON; Flakey on CI") + @unittest.skip("TODO: RUSTPYTHON; Flakey on CI") def test__count(self): # Test the _count() function. orig = thread._count() @@ -151,7 +151,6 @@ def task(): support.gc_collect() # For PyPy or other GCs. self.assertEqual(thread._count(), orig) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_unraisable_exception(self): def task(): started.release() diff --git a/Lib/test/test_threading_local.py b/Lib/test/test_threading_local.py index 0c805c5b055..17f031def7c 100644 --- a/Lib/test/test_threading_local.py +++ b/Lib/test/test_threading_local.py @@ -228,14 +228,6 @@ def __eq__(self, other): class ThreadLocalTest(unittest.TestCase, BaseLocalTest): _local = _thread._local - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_cycle_collection(self): - return super().test_cycle_collection() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: TypeError not raised by _local - def test_arguments(self): - return super().test_arguments() - class PyThreadingLocalTest(unittest.TestCase, BaseLocalTest): _local = _threading_local.local diff --git a/Lib/test/test_tokenize.py b/Lib/test/test_tokenize.py index 394a87c3601..0e81c6f6db2 100644 --- a/Lib/test/test_tokenize.py +++ b/Lib/test/test_tokenize.py @@ -1922,7 +1922,6 @@ def test_newline_and_space_at_the_end_of_the_source_without_newline(self): tokens = list(tokenize.tokenize(BytesIO(source.encode('utf-8')).readline)) self.assertEqual(tokens, expected_tokens) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'SyntaxError' not found in b'OSError: stream did not contain valid UTF-8\n' def test_invalid_character_in_fstring_middle(self): # See gh-103824 script = b'''F""" @@ -2173,6 +2172,7 @@ def test_string_concatenation(self): # Two string literals on the same line self.check_roundtrip("'' ''") + @unittest.skipIf(support.is_resource_enabled("cpu") and __import__("sys").platform == "win32", "TODO: RUSTPYTHON; Timeout after 10 minutes") def test_random_files(self): # Test roundtrip on random python modules. # pass the '-ucpu' option to process the full directory. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index ec56f26a735..42f066a6239 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3141,7 +3141,6 @@ def last_returns_frame4(self): def last_returns_frame5(self): return self.last_returns_frame4() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 not greater than 5 def test_extract_stack(self): frame = self.last_returns_frame5() def extract(**kwargs): diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 3f056f2b753..c98b99e98e9 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -843,7 +843,6 @@ def test_complex_comprehension_inlining_exec(self): lamb = list(genexp)[0] self.assertEqual(lamb(), 42) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '__annotate__' != 'f.__annotate__' def test_annotate_qualname(self): code = """ def f() -> None: diff --git a/Lib/test/test_type_comments.py b/Lib/test/test_type_comments.py index 0deb25f16d3..d827ac27108 100644 --- a/Lib/test/test_type_comments.py +++ b/Lib/test/test_type_comments.py @@ -252,7 +252,6 @@ def parse_all(self, source, minver=lowest, maxver=highest, expected_regex=""): def classic_parse(self, source): return ast.parse(source) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment' def test_funcdef(self): for tree in self.parse_all(funcdef): self.assertEqual(tree.body[0].type_comment, "() -> int") @@ -261,7 +260,6 @@ def test_funcdef(self): self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[1].type_comment, None) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_asyncdef(self): for tree in self.parse_all(asyncdef, minver=5): self.assertEqual(tree.body[0].type_comment, "() -> int") @@ -274,12 +272,10 @@ def test_asyncvar(self): with self.assertRaises(SyntaxError): self.classic_parse(asyncvar) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_asynccomp(self): for tree in self.parse_all(asynccomp, minver=6): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_matmul(self): for tree in self.parse_all(matmul, minver=5): pass @@ -288,37 +284,31 @@ def test_fstring(self): for tree in self.parse_all(fstring): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_underscorednumber(self): for tree in self.parse_all(underscorednumber, minver=6): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_redundantdef(self): for tree in self.parse_all(redundantdef, maxver=0, expected_regex="^Cannot have two type comments on def"): pass - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'FunctionDef' object has no attribute 'type_comment' def test_nonasciidef(self): for tree in self.parse_all(nonasciidef): self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt") - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'For' object has no attribute 'type_comment' def test_forstmt(self): for tree in self.parse_all(forstmt): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(forstmt) self.assertEqual(tree.body[0].type_comment, None) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'With' object has no attribute 'type_comment' def test_withstmt(self): for tree in self.parse_all(withstmt): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(withstmt) self.assertEqual(tree.body[0].type_comment, None) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'With' object has no attribute 'type_comment' def test_parenthesized_withstmt(self): for tree in self.parse_all(parenthesized_withstmt): self.assertEqual(tree.body[0].type_comment, "int") @@ -327,14 +317,12 @@ def test_parenthesized_withstmt(self): self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[1].type_comment, None) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'int' def test_vardecl(self): for tree in self.parse_all(vardecl): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(vardecl) self.assertEqual(tree.body[0].type_comment, None) - @unittest.expectedFailure # TODO: RUSTPYTHON; + (11, ' whatever')] def test_ignores(self): for tree in self.parse_all(ignores): self.assertEqual( @@ -350,7 +338,6 @@ def test_ignores(self): tree = self.classic_parse(ignores) self.assertEqual(tree.type_ignores, []) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: SyntaxError not raised : feature_version=(3, 4) def test_longargs(self): for tree in self.parse_all(longargs, minver=8): for t in tree.body: @@ -381,7 +368,6 @@ def test_longargs(self): self.assertIsNone(arg.type_comment, "%s(%s:%r)" % (t.name, arg.arg, arg.type_comment)) - @unittest.expectedFailure # TODO: RUSTPYTHON; Tests for inappropriately-placed type comments. def test_inappropriate_type_comments(self): """Tests for inappropriately-placed type comments. @@ -416,7 +402,6 @@ def test_non_utf8_type_comment_with_ignore_cookie(self): _testcapi.Py_CompileStringExFlags( b"def a(f=8, #type: \x80\n\x80", "", 256, flags) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: mode must be "exec", "eval", "ipython", or "single" def test_func_type_input(self): def parse_func_type_input(source): diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index c63ea2d291c..65261fcb6ed 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -683,7 +683,6 @@ def foo[U: T](self): ... self.assertIs(X.foo.__type_params__[0].__bound__, float) self.assertIs(X.Alias.__value__, float) - @unittest.expectedFailure # TODO: RUSTPYTHON; + global def test_binding_uses_global(self): ns = run_code(""" x = "global" @@ -1076,7 +1075,6 @@ async def coroutine[B](): class TypeParamsTypeVarTupleTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot use bound with TypeVarTuple" does not match "invalid syntax (, line 1)" def test_typevartuple_01(self): code = """def func1[*A: str](): pass""" check_syntax_error(self, code, "cannot use bound with TypeVarTuple") @@ -1100,7 +1098,6 @@ def func1[*A](): class TypeParamsTypeVarParamSpecTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "cannot use bound with ParamSpec" does not match "invalid syntax (, line 1)" def test_paramspec_01(self): code = """def func1[**A: str](): pass""" check_syntax_error(self, code, "cannot use bound with ParamSpec") diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 01da70b4c68..2b48e6789b6 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -45,7 +45,6 @@ def clear_typing_caches(): class TypesTests(unittest.TestCase): - @unittest.skipUnless(c_types, "TODO: RUSTPYTHON; requires _types module") def test_names(self): c_only_names = {'CapsuleType'} ignored = {'new_class', 'resolve_bases', 'prepare_class', @@ -636,7 +635,7 @@ def test_slot_wrapper_types(self): self.assertIsInstance(object.__lt__, types.WrapperDescriptorType) self.assertIsInstance(int.__lt__, types.WrapperDescriptorType) - @unittest.expectedFailure # TODO: RUSTPYTHON; No signature found in builtin method __get__ of 'method_descriptor' objects. + @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: no signature found for builtin >> {**{} for a in [1]} # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> {**{} for a in [1]} Traceback (most recent call last): ... SyntaxError: dict unpacking cannot be used in dict comprehension @@ -356,7 +356,7 @@ ... SyntaxError: can't use starred expression here - >>> (*x),y = 1, 2 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE + >>> (*x),y = 1, 2 Traceback (most recent call last): ... SyntaxError: cannot use starred expression here @@ -366,12 +366,12 @@ ... SyntaxError: cannot use starred expression here - >>> z,(*x),y = 1, 2, 4 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE + >>> z,(*x),y = 1, 2, 4 Traceback (most recent call last): ... SyntaxError: cannot use starred expression here - >>> z,(*x) = 1, 2 # TODO: RUSTPYTHON # doctest:+ELLIPSIS +EXPECTED_FAILURE + >>> z,(*x) = 1, 2 Traceback (most recent call last): ... SyntaxError: cannot use starred expression here diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 34757c97a4f..2ea5f502247 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -1094,6 +1094,7 @@ def nicer_error(self): f"**Subprocess Error**\n{err}" ) + @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; FileNotFoundError: [WinError 2] No such file or directory") @requires_venv_with_pip() @requires_resource('cpu') def test_with_pip(self): diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index 2e8e679c8d7..3a3a5a7c3a1 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -289,7 +289,6 @@ def test_ref_reuse(self): self.assertEqual(weakref.getweakrefcount(o), 1, "wrong weak ref count for object after deleting proxy") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_proxy_reuse(self): o = C() proxy1 = weakref.proxy(o) @@ -380,11 +379,9 @@ def __imatmul__(self, other): # was not honored, and was broken in different ways for # PyWeakref_NewRef() and PyWeakref_NewProxy(). (Two tests.) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_shared_ref_without_callback(self): self.check_shared_without_callback(weakref.ref) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_shared_proxy_without_callback(self): self.check_shared_without_callback(weakref.proxy) diff --git a/Lib/test/test_winconsoleio.py b/Lib/test/test_winconsoleio.py index 1bae884ed9a..516d5563218 100644 --- a/Lib/test/test_winconsoleio.py +++ b/Lib/test/test_winconsoleio.py @@ -142,6 +142,7 @@ def test_write_empty_data(self): with ConIO('CONOUT$', 'w') as f: self.assertEqual(f.write(b''), 0) + @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON") @requires_resource('console') def test_write(self): testcases = [] diff --git a/Lib/test/test_winsound.py b/Lib/test/test_winsound.py index 9724d830ade..d013d8396cb 100644 --- a/Lib/test/test_winsound.py +++ b/Lib/test/test_winsound.py @@ -100,6 +100,7 @@ def test_keyword_args(self): class PlaySoundTest(unittest.TestCase): + @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'str'") def test_errors(self): self.assertRaises(TypeError, winsound.PlaySound) self.assertRaises(TypeError, winsound.PlaySound, "bad", "bad") diff --git a/Lib/test/test_wmi.py b/Lib/test/test_wmi.py index 90eb40439d4..e2d924c9769 100644 --- a/Lib/test/test_wmi.py +++ b/Lib/test/test_wmi.py @@ -59,6 +59,7 @@ def test_wmi_query_not_select(self): with self.assertRaises(ValueError): wmi_exec_query("not select, just in case someone tries something") + @unittest.skipIf(__import__("sys").platform == "win32", "TODO: RUSTPYTHON; Timeout after 10 minutes") @support.requires_resource('cpu') def test_wmi_query_overflow(self): # Ensure very big queries fail diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 6a75126f260..2b0777c1d23 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -392,7 +392,6 @@ def test_cdata(self): self.serialize_check(ET.XML(""), 'hello') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_init(self): stringfile = io.BytesIO(SAMPLE_XML.encode("utf-8")) tree = ET.ElementTree(file=stringfile) @@ -508,7 +507,6 @@ def test_makeelement(self): elem[:] = tuple([subelem]) self.serialize_check(elem, '') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_parsefile(self): # Test parsing from file. @@ -688,7 +686,6 @@ def test_initialize_parser_without_target(self): parser2 = ET.XMLParser() self.assertIsInstance(parser2.target, ET.TreeBuilder) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_children(self): # Test Element children iteration @@ -1091,7 +1088,6 @@ def test_entity(self): self.assertEqual(str(cm.exception), 'undefined entity &entity;: line 4, column 10') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_namespace(self): # Test namespace issues. @@ -1335,7 +1331,6 @@ def test_attlist_default(self): class IterparseTest(unittest.TestCase): # Test iterparse interface. - @unittest.expectedFailure # TODO: RUSTPYTHON def test_basic(self): iterparse = ET.iterparse @@ -1361,7 +1356,6 @@ def test_basic(self): ]) it.close() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_external_file(self): with open(SIMPLE_XMLFILE, 'rb') as source: it = ET.iterparse(source) @@ -1374,7 +1368,6 @@ def test_external_file(self): ]) self.assertEqual(it.root.tag, 'root') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_events(self): iterparse = ET.iterparse @@ -1475,7 +1468,6 @@ def test_nonexistent_file(self): with self.assertRaises(FileNotFoundError): ET.iterparse("nonexistent") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_resource_warnings_not_exhausted(self): # Not exhausting the iterator still closes the underlying file (bpo-43292) it = ET.iterparse(SIMPLE_XMLFILE) @@ -1514,7 +1506,6 @@ def test_resource_warnings_exhausted(self): del it gc_collect() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_close_not_exhausted(self): iterparse = ET.iterparse @@ -1800,7 +1791,6 @@ def test_events_comment(self): self._feed(parser, "\n") self.assert_events(parser, [('comment', (ET.Comment, ' text here '))]) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_events_pi(self): parser = ET.XMLPullParser(events=('start', 'pi', 'end')) self._feed(parser, "\n") @@ -2042,7 +2032,6 @@ def _my_loader(self, href, parse): else: return None - @unittest.expectedFailure # TODO: RUSTPYTHON def test_xinclude_default(self): from xml.etree import ElementInclude doc = self.xinclude_loader('default.xml') @@ -2057,7 +2046,6 @@ def test_xinclude_default(self): '\n' '') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_xinclude(self): from xml.etree import ElementInclude @@ -2122,7 +2110,6 @@ def test_xinclude(self): ' \n' '') # C5 - @unittest.expectedFailure # TODO: RUSTPYTHON def test_xinclude_repeated(self): from xml.etree import ElementInclude @@ -2130,7 +2117,6 @@ def test_xinclude_repeated(self): ElementInclude.include(document, self.xinclude_loader) self.assertEqual(1+4*2, len(document.findall(".//p"))) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_xinclude_failures(self): from xml.etree import ElementInclude @@ -2235,7 +2221,6 @@ def check(elem): elem.set("123", 123) check(elem) # attribute value - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bug_xmltoolkit25(self): # typo in ElementTree.findtext @@ -2259,7 +2244,6 @@ def test_bug_xmltoolkitX1(self): ET.dump(tree) self.assertEqual(stdout.getvalue(), '
\n') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bug_xmltoolkit39(self): # non-ascii element and attribute names doesn't work @@ -2346,7 +2330,6 @@ def xmltoolkit63(): xmltoolkit63() self.assertEqual(sys.getrefcount(None), count) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_bug_200708_newline(self): # Preserve newlines in attributes. @@ -2462,7 +2445,6 @@ def test_issue6233(self): b"\n" b'tãg') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_issue6565(self): elem = ET.XML("") self.assertEqual(summarize_list(elem), ['tag']) @@ -2534,7 +2516,6 @@ def check_expat224_utf8_bug(self, text): root = ET.XML(xml) self.assertEqual(root.get('b'), text.decode('utf-8')) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expat224_utf8_bug(self): # bpo-31170: Expat 2.2.3 had a bug in its UTF-8 decoder. # Check that Expat 2.2.4 fixed the bug. @@ -3356,7 +3337,6 @@ class MyElement(ET.Element): class ElementFindTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_find_simple(self): e = ET.XML(SAMPLE_XML) self.assertEqual(e.find('tag').tag, 'tag') @@ -3380,7 +3360,6 @@ def test_find_simple(self): # Issue #16922 self.assertEqual(ET.XML('').findtext('empty'), '') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_find_xpath(self): LINEAR_XML = ''' @@ -3403,7 +3382,6 @@ def test_find_xpath(self): self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]') self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_findall(self): e = ET.XML(SAMPLE_XML) e[2] = ET.XML(SAMPLE_SECTION) @@ -3592,7 +3570,6 @@ def test_bad_find(self): with self.assertRaisesRegex(SyntaxError, 'cannot use absolute path'): e.findall('/tag') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_find_through_ElementTree(self): e = ET.XML(SAMPLE_XML) self.assertEqual(ET.ElementTree(e).find('tag').tag, 'tag') @@ -3821,7 +3798,6 @@ class TreeBuilderSubclass(ET.TreeBuilder): a = parser.close() self.assertEqual(a.text, "texttail") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_late_tail_mix_pi_comments(self): # Issue #37399: The tail of an ignored comment could overwrite the text before it. # Test appending tails to comments/pis. @@ -4578,7 +4554,6 @@ def test_correct_import_pyET(self): # -------------------------------------------------------------------- class BoolTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_warning(self): e = ET.fromstring('') msg = ( diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index c9cd182d6b7..23bbd6c88c5 100644 --- a/Lib/test/test_zipimport_support.py +++ b/Lib/test/test_zipimport_support.py @@ -37,9 +37,8 @@ def _run_object_doctest(obj, module): from test.support.rustpython import DocTestChecker # TODO: RUSTPYTHON finder = doctest.DocTestFinder(verbose=verbose, recurse=False) - # TODO: RUSTPYTHON - # runner = doctest.DocTestRunner(verbose=verbose) - runner = doctest.DocTestRunner(verbose=verbose, checker=DocTestChecker()) + runner = doctest.DocTestRunner(verbose=verbose) + runner = doctest.DocTestRunner(verbose=verbose, checker=DocTestChecker()) # TODO: RUSTPYTHON # Use the object's fully qualified name if it has one # Otherwise, use the module's name try: diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index e9516c0b127..638b4c52a4a 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1970,7 +1970,6 @@ def test_getattr_error(self): with self.assertRaises(AttributeError): self.module.NOATTRIBUTE - @unittest.expectedFailure # TODO: RUSTPYTHON; dir(self.module) should at least contain everything in __all__. def test_dir_contains_all(self): """dir(self.module) should at least contain everything in __all__.""" module_all_set = set(self.module.__all__) diff --git a/README.md b/README.md index 37b577d0f0e..cb086687d8f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ needed to prevent stack overflow on Windows): ```bash $ cd RustPython -$ cargo run --release demo_closures.py +$ cargo run --release -- -c 'print("Hello, RustPython!")' Hello, RustPython! ``` diff --git a/crates/capi/Cargo.toml b/crates/capi/Cargo.toml index 70b7fc4b8c5..671c8d1640c 100644 --- a/crates/capi/Cargo.toml +++ b/crates/capi/Cargo.toml @@ -14,16 +14,15 @@ crate-type = ["cdylib", "rlib"] [dependencies] bitflags = { workspace = true } itertools = { workspace = true } +libc = { workspace = true } +malachite-bigint = { workspace = true } num-complex = { workspace = true } -rustpython-vm = { workspace = true, features = ["threading", "compiler"] } +rustpython-vm = { workspace = true, features = ["threading", "compiler", "importlib", "host_env"] } rustpython-stdlib = {workspace = true, features = ["threading"] } +rustpython-pylib = { workspace = true } [dev-dependencies] -pyo3 = { workspace = true, features = ["auto-initialize", "abi3"] } +pyo3 = { workspace = true, features = ["auto-initialize", "abi3t"] } [lints] workspace = true - -[package.metadata.cargo-shear] -# Not a direct dependency (yet), but we need to enable threading support in the stdlib. -ignored = ["rustpython-stdlib"] diff --git a/crates/capi/pyo3-rustpython.config b/crates/capi/pyo3-rustpython.config index fe59e46e895..601b56440d7 100644 --- a/crates/capi/pyo3-rustpython.config +++ b/crates/capi/pyo3-rustpython.config @@ -1,5 +1,5 @@ -implementation=CPython -version=3.14 +implementation=RustPython +version=3.15 shared=true -abi3=true +target_abi=RustPython-abi3t-3.15 suppress_build_script_link_lines=true diff --git a/crates/capi/src/abstract_.rs b/crates/capi/src/abstract_.rs index d01e31e9626..fd6e966bdae 100644 --- a/crates/capi/src/abstract_.rs +++ b/crates/capi/src/abstract_.rs @@ -1,6 +1,7 @@ +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; use alloc::slice; -use core::ffi::c_int; +use core::ffi::{c_char, c_int}; pub use iter::*; pub use mapping::*; pub use number::*; @@ -24,9 +25,11 @@ fn dict_to_kwargs(vm: &VirtualMachine, dict: &Py) -> PyResult { dict.items_vec() .into_iter() .map(|(key, value)| { + // `to_string()` would replace lone surrogates with U+FFFD; keep the + // raw WTF-8 so surrogate keys round-trip (issue #8228). let key = key .downcast_ref::() - .map(|s| s.to_string()) + .map(|s| s.as_wtf8().to_owned()) .ok_or_else(|| vm.new_type_error("keywords must be strings"))?; Ok((key, value)) }) @@ -57,6 +60,21 @@ pub unsafe extern "C" fn PyObject_CallNoArgs(callable: *mut PyObject) -> *mut Py with_vm(|vm| unsafe { &*callable }.call((), vm)) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_CallObject( + callable: *mut PyObject, + args: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let callable = unsafe { &*callable }; + if let Some(args) = unsafe { args.as_ref() } { + callable.call(tuple_to_args(args.try_downcast_ref::(vm)?), vm) + } else { + callable.call((), vm) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_Vectorcall( callable: *mut PyObject, @@ -121,6 +139,42 @@ pub unsafe extern "C" fn PyObject_VectorcallMethod( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyVectorcall_Call( + callable: *mut PyObject, + tuple: *mut PyObject, + kwargs: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let callable = unsafe { &*callable }; + let tuple = unsafe { &*tuple }.try_downcast_ref::(vm)?; + + let mut args = tuple.iter().cloned().collect::>(); + let num_positional_args = args.len(); + + let mut kwnames = Vec::new(); + if let Some(kwargs) = unsafe { kwargs.as_ref() } { + let kwargs = kwargs.try_downcast_ref::(vm)?; + for (key, value) in kwargs.items_vec() { + let key = key + .downcast_ref::() + .map(ToOwned::to_owned) + .ok_or_else(|| vm.new_type_error("keywords must be strings"))?; + kwnames.push(key.into()); + args.push(value); + } + } + + let kwnames = if kwnames.is_empty() { + None + } else { + Some(kwnames.as_slice()) + }; + + callable.vectorcall(args, num_positional_args, kwnames, vm) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_GetItem(obj: *mut PyObject, key: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -153,6 +207,30 @@ pub unsafe extern "C" fn PyObject_DelItem(obj: *mut PyObject, key: *mut PyObject }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_DelItemString(obj: *mut PyObject, key: *const c_char) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { key.try_as_str(vm) }?; + obj.del_item(key, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Format( + obj: *mut PyObject, + format_spec: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let spec = unsafe { format_spec.as_ref() } + .map(|spec| spec.try_downcast_ref::(vm)) + .transpose()? + .unwrap_or_else(|| vm.ctx.empty_str); + vm.format(obj, spec.to_owned()) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_IsSubclass(derived: *mut PyObject, cls: *mut PyObject) -> c_int { with_vm(|vm| { @@ -178,3 +256,50 @@ pub unsafe extern "C" fn PyObject_Size(obj: *mut PyObject) -> isize { obj.length(vm) }) } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Length(obj: *mut PyObject) -> isize { + unsafe { PyObject_Size(obj) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Type(obj: *mut PyObject) -> *mut PyObject { + with_vm(|_vm| unsafe { &*obj }.obj_type()) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyDict, PyString}; + + #[test] + fn call_method1() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert!( + string + .call_method1("endswith", ("!",)) + .unwrap() + .is_truthy() + .unwrap() + ); + }) + } + + #[test] + fn object_set_get_del_item() { + Python::attach(|py| { + let obj = PyDict::new(py).into_any(); + obj.set_item("key", "value").unwrap(); + assert_eq!( + obj.get_item("key") + .unwrap() + .cast_into::() + .unwrap(), + "value" + ); + obj.del_item("key").unwrap(); + assert!(obj.get_item("key").is_err()); + }) + } +} diff --git a/crates/capi/src/abstract_/iter.rs b/crates/capi/src/abstract_/iter.rs index 1ba5bd04d19..a0827f62537 100644 --- a/crates/capi/src/abstract_/iter.rs +++ b/crates/capi/src/abstract_/iter.rs @@ -9,6 +9,15 @@ pub unsafe extern "C" fn PyIter_Check(obj: *mut PyObject) -> c_int { with_vm(|_vm| Ok(PyIter::check(unsafe { &*obj }))) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyAIter_Check(obj: *mut PyObject) -> c_int { + with_vm(|vm| { + Ok(unsafe { &*obj } + .class() + .has_attr(rustpython_vm::identifier!(vm, __anext__))) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_GetIter(obj: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -17,6 +26,11 @@ pub unsafe extern "C" fn PyObject_GetIter(obj: *mut PyObject) -> *mut PyObject { }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GetAIter(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.get_aiter(vm)) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyIter_NextItem(iter: *mut PyObject, item: *mut *mut PyObject) -> c_int { with_vm(|vm| { @@ -89,7 +103,7 @@ pub unsafe extern "C" fn PyIter_Send( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyIterator, PyList, PySendResult}; diff --git a/crates/capi/src/abstract_/mapping.rs b/crates/capi/src/abstract_/mapping.rs index 18b188613dc..840a9aed69c 100644 --- a/crates/capi/src/abstract_/mapping.rs +++ b/crates/capi/src/abstract_/mapping.rs @@ -1,4 +1,16 @@ +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; +use core::ffi::{c_char, c_int}; +use rustpython_vm::AsObject; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Check(obj: *mut PyObject) -> c_int { + with_vm(|_vm| { + let obj = unsafe { &*obj }; + Ok(obj.mapping_unchecked().check()) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyMapping_Size(obj: *mut PyObject) -> isize { with_vm(|vm| { @@ -7,6 +19,11 @@ pub unsafe extern "C" fn PyMapping_Size(obj: *mut PyObject) -> isize { }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_Length(obj: *mut PyObject) -> isize { + unsafe { PyMapping_Size(obj) } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyMapping_Keys(obj: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -37,7 +54,140 @@ pub unsafe extern "C" fn PyMapping_Items(obj: *mut PyObject) -> *mut PyObject { }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_GetItemString( + obj: *mut PyObject, + key: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { key.try_as_str(vm) }?; + obj.get_item(key, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_GetOptionalItem( + obj: *mut PyObject, + key: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + unsafe { + *result = core::ptr::null_mut(); + } + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + + match obj.get_item(key, vm) { + Ok(value) => { + unsafe { + *result = value.into_raw().as_ptr(); + } + Ok(true) + } + Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(false), + Err(err) => Err(err), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_GetOptionalItemString( + obj: *mut PyObject, + key: *const c_char, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + unsafe { + *result = core::ptr::null_mut(); + } + let obj = unsafe { &*obj }; + let key = unsafe { key.try_as_str(vm) }?; + + match obj.get_item(key, vm) { + Ok(value) => { + unsafe { + *result = value.into_raw().as_ptr(); + } + Ok(true) + } + Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(false), + Err(err) => Err(err), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_HasKey(obj: *mut PyObject, key: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + obj.get_item(key, vm).is_ok() + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_HasKeyString(obj: *mut PyObject, key: *const c_char) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + if let Ok(key) = unsafe { key.try_as_str(vm) } { + obj.get_item(key, vm).is_ok() + } else { + false + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_HasKeyWithError( + obj: *mut PyObject, + key: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { &*key }; + + match obj.get_item(key, vm) { + Ok(_) => Ok(true), + Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(false), + Err(err) => Err(err), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_HasKeyStringWithError( + obj: *mut PyObject, + key: *const c_char, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { key.try_as_str(vm) }?; + + match obj.get_item(key, vm) { + Ok(_) => Ok(true), + Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(false), + Err(err) => Err(err), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMapping_SetItemString( + obj: *mut PyObject, + key: *const c_char, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let key = unsafe { key.try_as_str(vm) }?; + let value = unsafe { &*value }.to_owned(); + obj.set_item(key, value, vm) + }) +} + +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyMapping, PyMappingMethods, PyTuple}; diff --git a/crates/capi/src/abstract_/number.rs b/crates/capi/src/abstract_/number.rs index c5ec492a73d..ffc78ea4e24 100644 --- a/crates/capi/src/abstract_/number.rs +++ b/crates/capi/src/abstract_/number.rs @@ -1,15 +1,217 @@ use crate::{PyObject, pystate::with_vm}; +use core::ffi::c_int; +use rustpython_vm::protocol::PyNumber; #[unsafe(no_mangle)] pub unsafe extern "C" fn PyNumber_Add(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { with_vm(|vm| vm._add(unsafe { &*o1 }, unsafe { &*o2 })) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyIndex_Check(obj: *mut PyObject) -> c_int { + with_vm(|_vm| unsafe { obj.as_ref() }.is_some_and(|obj| obj.number().is_index())) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Absolute(o: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._abs(unsafe { &*o })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_And(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._and(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Check(o: *mut PyObject) -> c_int { + with_vm(|_vm| unsafe { o.as_ref() }.is_some_and(PyNumber::check)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Divmod(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._divmod(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Float(o: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*o }.try_float(vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_FloorDivide( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._floordiv(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceAdd( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._iadd(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceAnd( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._iand(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceFloorDivide( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._ifloordiv(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceLshift( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._ilshift(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceMatrixMultiply( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._imatmul(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceMultiply( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._imul(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceOr(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._ior(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlacePower( + o1: *mut PyObject, + o2: *mut PyObject, + o3: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._ipow(unsafe { &*o1 }, unsafe { &*o2 }, unsafe { &*o3 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceRemainder( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._imod(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceRshift( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._irshift(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceSubtract( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._isub(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceTrueDivide( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._itruediv(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_InPlaceXor( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._ixor(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Invert(o: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._invert(unsafe { &*o })) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyNumber_Index(obj: *mut PyObject) -> *mut PyObject { with_vm(|vm| unsafe { &*obj }.try_index(vm)) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_MatrixMultiply( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._matmul(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Multiply(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._mul(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Negative(o: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._neg(unsafe { &*o })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Positive(o: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._pos(unsafe { &*o })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Power( + o1: *mut PyObject, + o2: *mut PyObject, + o3: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._pow(unsafe { &*o1 }, unsafe { &*o2 }, unsafe { &*o3 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Remainder(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._mod(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_TrueDivide( + o1: *mut PyObject, + o2: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| vm._truediv(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Xor(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { + with_vm(|vm| vm._xor(unsafe { &*o1 }, unsafe { &*o2 })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyNumber_Long(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.try_int(vm)) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyNumber_Lshift(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject { with_vm(|vm| vm._lshift(unsafe { &*o1 }, unsafe { &*o2 })) @@ -30,7 +232,7 @@ pub unsafe extern "C" fn PyNumber_Subtract(o1: *mut PyObject, o2: *mut PyObject) with_vm(|vm| vm._sub(unsafe { &*o1 }, unsafe { &*o2 })) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; diff --git a/crates/capi/src/abstract_/sequence.rs b/crates/capi/src/abstract_/sequence.rs index f6022b93e91..d011dfdb8eb 100644 --- a/crates/capi/src/abstract_/sequence.rs +++ b/crates/capi/src/abstract_/sequence.rs @@ -177,7 +177,7 @@ pub unsafe extern "C" fn PySequence_In(obj: *mut PyObject, value: *mut PyObject) unsafe { PySequence_Contains(obj, value) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyAnyMethods, PyDict, PyList, PySequence, PySequenceMethods, PyTuple}; diff --git a/crates/capi/src/bytearrayobject.rs b/crates/capi/src/bytearrayobject.rs index 2bc56895ba8..cc9db5dd50e 100644 --- a/crates/capi/src/bytearrayobject.rs +++ b/crates/capi/src/bytearrayobject.rs @@ -71,7 +71,7 @@ pub unsafe extern "C" fn PyByteArray_Resize(bytearray: *mut PyObject, len: isize }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyByteArray, PyBytes}; diff --git a/crates/capi/src/bytesobject.rs b/crates/capi/src/bytesobject.rs index 1fe535efba5..f4db16af6b3 100644 --- a/crates/capi/src/bytesobject.rs +++ b/crates/capi/src/bytesobject.rs @@ -1,6 +1,5 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; +use crate::{PyObject, pystate::with_vm}; use core::ffi::c_char; use rustpython_vm::builtins::PyBytes; @@ -46,13 +45,13 @@ pub unsafe extern "C" fn PyBytes_AsString(bytes: *mut PyObject) -> *mut c_char { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyBytes; #[test] - fn test_bytes() { + fn bytes() { Python::attach(|py| { let bytes = PyBytes::new(py, b"Hello, World!"); assert_eq!(bytes.as_bytes(), b"Hello, World!"); @@ -60,7 +59,7 @@ mod tests { } #[test] - fn test_bytes_uninit() { + fn bytes_uninit() { Python::attach(|py| { let bytes = PyBytes::new_with(py, 13, |data| { data.copy_from_slice(b"Hello, World!"); diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs index d28dad4d6df..7de5b3cf6fe 100644 --- a/crates/capi/src/ceval.rs +++ b/crates/capi/src/ceval.rs @@ -1,16 +1,13 @@ +use crate::pyframe::PyFrameObject; use crate::pystate::with_vm; +use crate::unicodeobject::decode_fsdefault_and_size; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::builtins::{PyCode, PyDict}; -use rustpython_vm::compiler::Mode; use rustpython_vm::function::ArgMapping; use rustpython_vm::scope::Scope; use rustpython_vm::{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; +use rustpython_vm::{PyObjectRef, version}; #[unsafe(no_mangle)] pub unsafe extern "C" fn Py_CompileString( @@ -19,27 +16,11 @@ pub unsafe extern "C" fn Py_CompileString( start: c_int, ) -> *mut PyObject { with_vm(|vm| { - let code = unsafe { CStr::from_ptr(code) }.to_str().map_err(|_| { - vm.new_system_error("Py_CompileString called with non UTF-8 code string") - })?; - let filename = unsafe { CStr::from_ptr(filename) } - .to_str() - .map_err(|_| vm.new_system_error("Py_CompileString called with non UTF-8 filename"))?; - - let mode = match start { - PY_SINGLE_INPUT => Mode::Single, - PY_FILE_INPUT => Mode::Exec, - PY_EVAL_INPUT => Mode::Eval, - PY_FUNC_TYPE_INPUT => Mode::BlockExpr, - _ => { - return Err( - vm.new_system_error("Invalid start argument passed to Py_CompileString") - ); - } - }; - - vm.compile(code, mode, filename) - .map_err(|err| vm.new_syntax_error(&err, Some(code))) + let code = unsafe { CStr::from_ptr(code) }.to_bytes(); + let filename_size = unsafe { CStr::from_ptr(filename) }.to_bytes().len(); + let filename = decode_fsdefault_and_size(vm, filename, filename_size)?; + let filename = filename.to_string_lossy(); + vm.compile_string_object_with_flags(code, &filename, start, 0, version::MINOR as c_int, -1) }) } @@ -62,23 +43,117 @@ pub unsafe extern "C" fn PyEval_EvalCode( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyEval_EvalFrame(f: *mut PyFrameObject) -> *mut PyObject { + unsafe { PyEval_EvalFrameEx(f, 0) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyEval_EvalFrameEx(f: *mut PyFrameObject, _exc: c_int) -> *mut PyObject { + with_vm(|vm| vm.run_frame(unsafe { &*f }.to_owned())) +} + #[unsafe(no_mangle)] pub extern "C" fn PyEval_GetBuiltins() -> *mut PyObject { with_vm(|vm| { vm.current_frame().map_or_else( || vm.builtins.as_object().as_raw(), - |frame| frame.builtins.as_object().as_raw(), + |frame| frame.iframe().builtins().as_raw(), + ) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetFrame() -> *mut PyFrameObject { + with_vm(|vm| -> *mut PyObject { + vm.current_frame() + .map(|frame| frame.as_object().as_raw().cast_mut()) + .unwrap_or_default() + }) + .cast() +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetFrameBuiltins() -> *mut PyObject { + with_vm(|vm| { + vm.current_frame().map_or_else( + || vm.builtins.as_object().to_owned(), + |frame| frame.iframe().builtins().to_owned(), ) }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetFrameGlobals() -> *mut PyObject { + with_vm(|vm| { + vm.current_frame() + .map(|frame| { + frame + .iframe() + .globals() + .as_object() + .to_owned() + .into_raw() + .as_ptr() + }) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetFrameLocals() -> *mut PyObject { + with_vm(|vm| { + let Some(frame) = vm.current_frame() else { + return Ok(core::ptr::null_mut()); + }; + let locals: PyObjectRef = frame.locals(vm)?.into(); + Ok(locals.into_raw().as_ptr()) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetGlobals() -> *mut PyObject { + with_vm(|vm| { + vm.current_frame() + .map(|frame| frame.iframe().globals().as_object().as_raw()) + .unwrap_or_default() + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyEval_GetLocals() -> *mut PyObject { + with_vm(|vm| { + let Some(frame) = vm.current_frame() else { + return Ok(core::ptr::null_mut()); + }; + let _ = frame.locals(vm)?; + Ok(frame.iframe().locals.as_object(vm).as_raw().cast_mut()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyEval_GetFuncDesc(func: *mut PyObject) -> *const c_char { + with_vm(|vm| { + let func = unsafe { &*func }; + let cls = func.class(); + if cls.is(vm.ctx.types.bound_method_type) + || cls.is(vm.ctx.types.function_type) + || cls.is(vm.ctx.types.builtin_function_or_method_type) + { + c"()" + } else { + c" object" + } + }) +} + +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::prelude::*; #[test] - fn test_code_eval() { + fn code_eval() { Python::attach(|py| { let result = py.eval(c"1 + 1", None, None).unwrap(); assert_eq!(result.extract::().unwrap(), 2); @@ -86,7 +161,7 @@ mod tests { } #[test] - fn test_code_run_exception() { + fn code_run_exception() { Python::attach(|py| { let err = py.run(c"raise Exception()", None, None).unwrap_err(); assert!(err.is_instance_of::(py)); diff --git a/crates/capi/src/codecs.rs b/crates/capi/src/codecs.rs new file mode 100644 index 00000000000..8433bf3baa4 --- /dev/null +++ b/crates/capi/src/codecs.rs @@ -0,0 +1,210 @@ +use crate::util::CStrExt; +use crate::{PyObject, pystate::with_vm}; +use core::ffi::{c_char, c_int}; +use rustpython_vm::{AsObject, VirtualMachine}; + +fn call_codec_error_handler( + vm: &VirtualMachine, + handler_name: &str, + exc: *mut PyObject, +) -> rustpython_vm::PyResult { + vm.state + .codec_registry + .lookup_error(handler_name, vm)? + .call((unsafe { &*exc }.to_owned(),), vm) +} + +fn codec_stream( + vm: &VirtualMachine, + encoding: *const c_char, + stream: *mut PyObject, + errors: *const c_char, + method: &str, +) -> rustpython_vm::PyResult { + let encoding = unsafe { encoding.try_as_str_opt(vm) }?.unwrap_or("utf-8"); + let errors = unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_str(errors)); + let stream = unsafe { &*stream }.to_owned(); + let codec = vm.state.codec_registry.lookup(encoding, vm)?; + let args = match errors { + Some(errors) => vec![stream, errors.into()], + None => vec![stream], + }; + vm.call_method(codec.as_tuple().as_object(), method, args) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Register(search_function: *mut PyObject) -> c_int { + with_vm(|vm| { + let search_function = unsafe { &*search_function }.to_owned(); + vm.state.codec_registry.register(search_function, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Unregister(search_function: *mut PyObject) -> c_int { + with_vm(|vm| { + let search_function = unsafe { &*search_function }.to_owned(); + vm.state.codec_registry.unregister(search_function); + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_KnownEncoding(encoding: *const c_char) -> c_int { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + match vm.state.codec_registry.lookup(encoding, vm) { + Ok(_) => Ok(true), + Err(err) if err.fast_isinstance(vm.ctx.exceptions.lookup_error) => Ok(false), + Err(err) => Err(err), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Encode( + object: *mut PyObject, + encoding: *const c_char, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let object = unsafe { &*object }.to_owned(); + let encoding = unsafe { encoding.try_as_str_opt(vm) }?.unwrap_or("utf-8"); + let errors = + unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_utf8_str(errors)); + vm.state.codec_registry.encode(object, encoding, errors, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Decode( + object: *mut PyObject, + encoding: *const c_char, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let object = unsafe { &*object }.to_owned(); + let encoding = unsafe { encoding.try_as_str_opt(vm) }?.unwrap_or("utf-8"); + let errors = + unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_utf8_str(errors)); + vm.state.codec_registry.decode(object, encoding, errors, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Encoder(encoding: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + vm.state + .codec_registry + .lookup(encoding, vm) + .map(|codec| codec.get_encode_func().to_owned()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_Decoder(encoding: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + vm.state + .codec_registry + .lookup(encoding, vm) + .map(|codec| codec.get_decode_func().to_owned()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_IncrementalEncoder( + encoding: *const c_char, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + let errors = unsafe { errors.try_as_str_opt(vm) }?.map(|s| vm.ctx.new_str(s)); + let codec = vm.state.codec_registry.lookup(encoding, vm)?; + codec.get_incremental_encoder(errors, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_IncrementalDecoder( + encoding: *const c_char, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + let errors = unsafe { errors.try_as_str_opt(vm) }?.map(|s| vm.ctx.new_str(s)); + let codec = vm.state.codec_registry.lookup(encoding, vm)?; + codec.get_incremental_decoder(errors, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_StreamReader( + encoding: *const c_char, + stream: *mut PyObject, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| codec_stream(vm, encoding, stream, errors, "streamreader")) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_StreamWriter( + encoding: *const c_char, + stream: *mut PyObject, + errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| codec_stream(vm, encoding, stream, errors, "streamwriter")) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_RegisterError(name: *const c_char, error: *mut PyObject) -> c_int { + with_vm(|vm| { + let name = unsafe { name.try_as_str(vm) }?; + let error = unsafe { &*error }.to_owned(); + if !error.is_callable() { + return Err(vm.new_type_error("handler must be callable")); + } + vm.state + .codec_registry + .register_error(name.to_owned(), error); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_LookupError(name: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { name.try_as_str(vm) }?; + vm.state.codec_registry.lookup_error(name, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_StrictErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "strict", exc)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_IgnoreErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "ignore", exc)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_ReplaceErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "replace", exc)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_XMLCharRefReplaceErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "xmlcharrefreplace", exc)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_BackslashReplaceErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "backslashreplace", exc)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyCodec_NameReplaceErrors(exc: *mut PyObject) -> *mut PyObject { + with_vm(|vm| call_codec_error_handler(vm, "namereplace", exc)) +} diff --git a/crates/capi/src/complexobject.rs b/crates/capi/src/complexobject.rs index a6b2bb731a0..79f1804d1bd 100644 --- a/crates/capi/src/complexobject.rs +++ b/crates/capi/src/complexobject.rs @@ -36,13 +36,13 @@ pub unsafe extern "C" fn PyComplex_ImagAsDouble(obj: *mut PyObject) -> c_double with_vm(|vm| try_to_complex(vm, unsafe { &*obj }).map(|complex| complex.im)) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyComplex; #[test] - fn test_py_int() { + fn py_int() { Python::attach(|py| { let number = PyComplex::from_doubles(py, 1.0, 2.0); assert_eq!(number.real(), 1.0); diff --git a/crates/capi/src/critical_section.rs b/crates/capi/src/critical_section.rs new file mode 100644 index 00000000000..f50f2b61607 --- /dev/null +++ b/crates/capi/src/critical_section.rs @@ -0,0 +1,31 @@ +use crate::PyObject; + +#[repr(C)] +pub struct PyCriticalSection; + +#[repr(C)] +pub struct PyCriticalSection2; + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject) { + let _ = (c, op); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection_End(c: *mut PyCriticalSection) { + let _ = c; +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection2_Begin( + c: *mut PyCriticalSection2, + a: *mut PyObject, + b: *mut PyObject, +) { + let _ = (c, a, b); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyCriticalSection2_End(c: *mut PyCriticalSection2) { + let _ = c; +} diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 5232634fabb..863f286e13a 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -1,7 +1,191 @@ use crate::PyObject; +use crate::methodobject::{PyMethodDef, build_method_def}; +use crate::object::PyTypeObject; use crate::pystate::with_vm; -use rustpython_vm::PyPayload; -use rustpython_vm::builtins::PyMappingProxy; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int, c_void}; +use core::ptr::NonNull; +use rustpython_vm::builtins::{ + DescriptorMemberDef, MemberGetter, MemberKind, MemberSetter, PyDescriptorOwned, PyGetSet, + PyMappingProxy, PyMemberDescriptor, PyType, +}; +use rustpython_vm::common::lock::PyRwLock; +use rustpython_vm::function::PySetterValue; +use rustpython_vm::{Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine}; + +#[repr(C)] +pub struct PyGetSetDef { + pub name: *const c_char, + pub get: + Option *mut PyObject>, + pub set: Option< + unsafe extern "C" fn( + slf: *mut PyObject, + value: *mut PyObject, + closure: *mut c_void, + ) -> c_int, + >, + pub doc: *const c_char, + pub closure: *mut c_void, +} + +impl PyGetSetDef { + pub(crate) fn build( + &self, + ty: &'static Py, + vm: &VirtualMachine, + ) -> PyResult> { + let name = unsafe { self.name.try_as_str(vm) }?; + let closure = self.closure as usize; + + let descriptor = match (self.get, self.set) { + (Some(get), Some(set)) => vm.ctx.new_static_getset( + name, + ty, + move |obj: PyObjectRef, vm: &VirtualMachine| -> PyResult { + unsafe { + let closure = closure as *mut c_void; + let ret_ptr = get(obj.as_raw().cast_mut(), closure); + let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { + vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error( + "Native function returned NULL, but there was no exception set", + ) + }) + })?; + Ok(PyObjectRef::from_raw(ret_ptr)) + } + }, + move |obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine| unsafe { + let closure = closure as *mut c_void; + let value = value.unwrap_or_none(vm); + let result = set(obj.as_raw().cast_mut(), value.as_raw().cast_mut(), closure); + if result == 0 { + Ok(()) + } else { + Err(vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error( + "C setter returned error but did not set an exception", + ) + })) + } + }, + ), + (Some(get), None) => vm.ctx.new_readonly_getset( + name, + ty, + move |obj: PyObjectRef, vm: &VirtualMachine| -> PyResult { + unsafe { + let closure = closure as *mut c_void; + let ret_ptr = get(obj.as_raw().cast_mut(), closure); + let ret_ptr = NonNull::new(ret_ptr).ok_or_else(|| { + vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error( + "Native function returned NULL, but there was no exception set", + ) + }) + })?; + Ok(PyObjectRef::from_raw(ret_ptr)) + } + }, + ), + (None, Some(set)) => vm.ctx.new_static_getset( + name, + ty, + move |_obj: PyObjectRef, vm: &VirtualMachine| -> PyResult { + Err(vm.new_attribute_error("unreadable attribute")) + }, + move |obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine| unsafe { + let closure = closure as *mut c_void; + let value = value.unwrap_or_none(vm); + let result = set(obj.as_raw().cast_mut(), value.as_raw().cast_mut(), closure); + if result == 0 { + Ok(()) + } else { + Err(vm.take_raised_exception().unwrap_or_else(|| { + vm.new_system_error( + "C setter returned error but did not set an exception", + ) + })) + } + }, + ), + (None, None) => vm.ctx.new_readonly_getset( + name, + ty, + move |_obj: PyObjectRef, vm: &VirtualMachine| -> PyResult { + Err(vm.new_attribute_error("unreadable attribute")) + }, + ), + }; + + Ok(descriptor) + } +} + +#[repr(C)] +pub struct PyMemberDef { + pub name: *const c_char, + pub type_code: c_int, + pub offset: isize, + pub flags: c_int, + pub doc: *const c_char, +} + +impl PyMemberDef { + const PY_READONLY: c_int = 1; + const PY_RELATIVE_OFFSET: c_int = 8; + + pub(crate) fn build( + &self, + ty: &Py, + vm: &VirtualMachine, + ) -> PyResult> { + let name = unsafe { self.name.try_as_str(vm) }?; + let kind = match self.type_code { + 6 => MemberKind::Object, + 16 => MemberKind::ObjectEx, + 14 => MemberKind::Bool, + _ => { + return Err(vm.new_system_error(format!( + "PyDescr_NewMember does not support member type code {}", + self.type_code + ))); + } + }; + if self.offset < 0 { + return Err(vm.new_system_error("PyDescr_NewMember does not support negative offsets")); + } + if self.flags & Self::PY_RELATIVE_OFFSET != 0 { + return Err( + vm.new_system_error("PyDescr_NewMember does not support Py_RELATIVE_OFFSET") + ); + } + + let doc = unsafe { self.doc.try_as_str_opt(vm) }?.map(str::to_owned); + + let descriptor = PyMemberDescriptor { + common: PyDescriptorOwned { + typ: ty.to_owned(), + name: vm.ctx.intern_str(name), + qualname: PyRwLock::new(None), + }, + member: DescriptorMemberDef { + name: name.to_owned(), + kind, + getter: MemberGetter::Offset(self.offset as usize), + setter: if self.flags & Self::PY_READONLY != 0 { + MemberSetter::Setter(None) + } else { + MemberSetter::Offset(self.offset as usize) + }, + doc, + }, + }; + + Ok(descriptor.into_ref(&vm.ctx)) + } +} #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDictProxy_New(mapping: *mut PyObject) -> *mut PyObject { @@ -11,7 +195,58 @@ pub unsafe extern "C" fn PyDictProxy_New(mapping: *mut PyObject) -> *mut PyObjec }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDescr_NewMethod( + typ: *mut PyTypeObject, + method: *mut PyMethodDef, +) -> *mut PyObject { + with_vm(|vm| { + let method = build_method_def(vm, unsafe { &*method }, true)?; + Ok(method.build_method(unsafe { &*typ }, vm)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDescr_NewClassMethod( + typ: *mut PyTypeObject, + method: *mut PyMethodDef, +) -> *mut PyObject { + with_vm(|vm| { + let method = build_method_def(vm, unsafe { &*method }, true)?; + Ok(method.build_method(unsafe { &*typ }, vm)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDescr_NewGetSet( + typ: *mut PyTypeObject, + getset: *mut PyGetSetDef, +) -> *mut PyObject { + with_vm(|vm| unsafe { &*getset }.build(unsafe { &*typ }, vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDescr_NewMember( + typ: *mut PyTypeObject, + member: *mut PyMemberDef, +) -> *mut PyObject { + with_vm(|vm| Ok(unsafe { &*member }.build(unsafe { &*typ }, vm))) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyWrapper_New(descr: *mut PyObject, obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let descr = unsafe { &*descr }; + let obj = unsafe { &*obj }; + vm.call_special_method( + descr, + vm.ctx.names.__get__, + (obj.to_owned(), obj.class().to_owned()), + ) + }) +} + +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyDict, PyInt, PyMappingProxy}; @@ -23,7 +258,7 @@ mod tests { dict.set_item("x", 7).unwrap(); let mapping = dict.as_mapping(); - let proxy = PyMappingProxy::new(py, &mapping); + let proxy = PyMappingProxy::new(py, mapping); let value = proxy.get_item("x").unwrap().cast_into::().unwrap(); assert_eq!(value, 7); }) diff --git a/crates/capi/src/dictobject.rs b/crates/capi/src/dictobject.rs index ebc4a827f36..ed29c693463 100644 --- a/crates/capi/src/dictobject.rs +++ b/crates/capi/src/dictobject.rs @@ -1,7 +1,8 @@ use crate::PyObject; use crate::object::define_py_check; use crate::pystate::with_vm; -use core::ffi::c_int; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::AsObject; use rustpython_vm::PyPayload; @@ -18,6 +19,15 @@ pub extern "C" fn PyDict_New() -> *mut PyObject { with_vm(|vm| vm.ctx.new_dict()) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_Clear(dict: *mut PyObject) { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + dict.clear(); + Ok(()) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_SetItem( dict: *mut PyObject, @@ -32,6 +42,90 @@ pub unsafe extern "C" fn PyDict_SetItem( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_SetItemString( + dict: *mut PyObject, + key: *const c_char, + val: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { key.try_as_str(vm) }?; + let value = unsafe { &*val }.to_owned(); + dict.inner_setitem(key, value, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_GetItem(dict: *mut PyObject, key: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + match dict.inner_getitem_opt(key, vm) { + Ok(Some(value)) => Ok(value.as_object().as_raw().cast_mut()), + Ok(None) | Err(_) => Ok(core::ptr::null_mut()), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_GetItemWithError( + dict: *mut PyObject, + key: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + if let Some(value) = dict.inner_getitem_opt(key, vm)? { + Ok(value.as_object().as_raw().cast_mut()) + } else { + Ok(core::ptr::null_mut()) + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_GetItemString( + dict: *mut PyObject, + key: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { key.try_as_str(vm) }?; + + match dict.inner_getitem_opt(key, vm)? { + Some(value) => Ok(value.as_object().as_raw().cast_mut()), + None => Ok(core::ptr::null_mut()), + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_GetItemStringRef( + dict: *mut PyObject, + key: *const c_char, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + unsafe { + *result = core::ptr::null_mut(); + } + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { key.try_as_str(vm) }?; + + if let Some(value) = dict.inner_getitem_opt(key, vm)? { + unsafe { + *result = value.into_raw().as_ptr(); + } + Ok(true) + } else { + Ok(false) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_GetItemRef( dict: *mut PyObject, @@ -57,6 +151,43 @@ pub unsafe extern "C" fn PyDict_GetItemRef( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_SetDefaultRef( + dict: *mut PyObject, + key: *mut PyObject, + default_value: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + let result = NonNull::new(result); + if let Some(result) = result { + unsafe { + result.write(core::ptr::null_mut()); + } + } + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { &*key }; + + if let Some(value) = dict.inner_getitem_opt(key, vm)? { + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(true) + } else { + let value = unsafe { &*default_value }.to_owned(); + dict.inner_setitem(key, value.clone(), vm)?; + if let Some(result) = result { + unsafe { + result.write(value.into_raw().as_ptr()); + } + } + Ok(false) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_Size(dict: *mut PyObject) -> isize { with_vm(|vm| { @@ -91,6 +222,15 @@ pub unsafe extern "C" fn PyDict_DelItem(dict: *mut PyObject, key: *mut PyObject) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyDict_DelItemString(dict: *mut PyObject, key: *const c_char) -> c_int { + with_vm(|vm| { + let dict = unsafe { &*dict }.try_downcast_ref::(vm)?; + let key = unsafe { key.try_as_str(vm) }?; + dict.del_item(key, vm) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyDict_Items(dict: *mut PyObject) -> *mut PyObject { with_vm(|vm| { @@ -187,13 +327,13 @@ pub unsafe extern "C" fn PyDict_Next( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyDictMethods, PyInt, PyList}; #[test] - fn test_create_empty_dict() { + fn create_empty_dict() { Python::attach(|py| { let dict = PyDict::new(py); assert!(dict.is_instance_of::()); @@ -201,7 +341,7 @@ mod tests { } #[test] - fn test_create_dict_with_items() { + fn create_dict_with_items() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py)?; let value = dict.get_item(1)?.unwrap().cast_into::()?; @@ -214,7 +354,7 @@ mod tests { } #[test] - fn test_dict_iter() { + fn dict_iter() { Python::attach(|py| { let dict = [(1, 2), (3, 4)].into_py_dict(py).unwrap(); let values = dict diff --git a/crates/capi/src/floatobject.rs b/crates/capi/src/floatobject.rs index f1bb078106d..ead09c219bb 100644 --- a/crates/capi/src/floatobject.rs +++ b/crates/capi/src/floatobject.rs @@ -1,6 +1,8 @@ use crate::object::define_py_check; use crate::{PyObject, pystate::with_vm}; use core::ffi::c_double; +use core::ptr::NonNull; +use rustpython_vm::AsObject; use rustpython_vm::builtins::PyFloat; define_py_check!(fn PyFloat_Check, types.float_type); @@ -24,14 +26,45 @@ pub unsafe extern "C" fn PyFloat_AsDouble(obj: *mut PyObject) -> c_double { }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub extern "C" fn PyFloat_GetMax() -> c_double { + c_double::MAX +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyFloat_GetMin() -> c_double { + c_double::MIN_POSITIVE +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyFloat_GetInfo() -> *mut PyObject { + with_vm(|vm| { + vm.sys_module + .as_object() + .get_attr("float_info", vm) + .map(|obj| obj.into_raw().as_ptr()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyFloat_FromString(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = NonNull::new(obj) + .ok_or_else(|| vm.new_type_error("float() argument must be a string or a number"))?; + let obj = unsafe { obj.as_ref() }.to_owned(); + let float = rustpython_vm::builtins::parse_float_from_string(obj, vm)?; + Ok(vm.ctx.new_float(float)) + }) +} + +#[cfg(test)] mod tests { use core::f64::consts::PI; use pyo3::prelude::*; use pyo3::types::PyFloat; #[test] - fn test_py_float() { + fn py_float() { Python::attach(|py| { let pi = PyFloat::new(py, PI); assert!(pi.is_instance_of::()); diff --git a/crates/capi/src/genericaliasobject.rs b/crates/capi/src/genericaliasobject.rs new file mode 100644 index 00000000000..1ab443e13ad --- /dev/null +++ b/crates/capi/src/genericaliasobject.rs @@ -0,0 +1,15 @@ +use crate::{PyObject, pystate::with_vm}; +use rustpython_vm::PyPayload; +use rustpython_vm::builtins::PyGenericAlias; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_GenericAlias( + origin: *mut PyObject, + args: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let origin = unsafe { &*origin }.to_owned(); + let args = unsafe { &*args }.to_owned(); + PyGenericAlias::from_args(origin, args, vm).map(|alias| alias.into_pyobject(vm)) + }) +} diff --git a/crates/capi/src/import.rs b/crates/capi/src/import.rs index d380d1f8266..37a4bbf8882 100644 --- a/crates/capi/src/import.rs +++ b/crates/capi/src/import.rs @@ -1,22 +1,89 @@ +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; -use rustpython_vm::builtins::PyStr; +use core::ffi::c_char; +use rustpython_vm::builtins::{PyCode, PyDict, PyModule, PyStr}; +use rustpython_vm::import::import_code_obj; #[unsafe(no_mangle)] pub unsafe extern "C" fn PyImport_Import(name: *mut PyObject) -> *mut PyObject { with_vm(|vm| { let name = unsafe { (&*name).try_downcast_ref::(vm)? }; - vm.import(name, 0) + let _ = vm.import(name, 0)?; + + vm.sys_module + .get_attr(rustpython_vm::identifier!(vm, modules), vm)? + .get_item(name, vm) }) } -#[cfg(false)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyImport_AddModuleRef(name: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { name.try_as_str(vm) }?; + + let sys_modules = vm + .sys_module + .get_attr(rustpython_vm::identifier!(vm, modules), vm)?; + + sys_modules + .try_downcast_ref::(vm)? + .get_item_opt(name, vm)? + .map_or_else( + || { + let module = vm.new_module(name, vm.ctx.new_dict(), None); + sys_modules.set_item(name, module.clone().into(), vm)?; + Ok(module) + }, + |module| { + let module = module.try_downcast_ref::(vm)?; + Ok(module.to_owned()) + }, + ) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyImport_ExecCodeModuleEx( + name: *const c_char, + co: *mut PyObject, + pathname: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let name = unsafe { name.try_as_str(vm) }?; + let code = unsafe { &*co }.try_downcast_ref::(vm)?; + let module = import_code_obj(vm, name, code.to_owned(), false)?; + + if let Some(pathname) = unsafe { pathname.try_as_str_opt(vm) }? { + module.set_attr("__file__", vm.ctx.new_str(pathname), vm)?; + } + + Ok(module) + }) +} + +#[cfg(test)] mod tests { use pyo3::prelude::*; #[test] - fn test_import() { + fn import() { Python::attach(|py| { let _module = py.import("sys").unwrap(); }) } + + #[test] + fn import_stdlib() { + Python::attach(|py| { + let _module = py.import("types").unwrap(); + }) + } + + #[test] + fn import_sub_module() { + Python::attach(|py| { + let module = py.import("collections.abc").unwrap(); + module.getattr("Sequence").unwrap(); + }) + } } diff --git a/crates/capi/src/lib.rs b/crates/capi/src/lib.rs index fb3bc687f4d..2aff75fe15b 100644 --- a/crates/capi/src/lib.rs +++ b/crates/capi/src/lib.rs @@ -1,7 +1,8 @@ #![allow(clippy::missing_safety_doc)] use crate::pyerrors::init_exception_statics; -use crate::pylifecycle::MAIN_INTERP; +use crate::pylifecycle::{MAIN_INTERP, MAIN_INTERP_PTR}; +use core::sync::atomic::Ordering; pub use rustpython_vm::PyObject; use rustpython_vm::{Context, Interpreter}; use std::sync::MutexGuard; @@ -13,20 +14,29 @@ pub mod boolobject; pub mod bytearrayobject; pub mod bytesobject; pub mod ceval; +pub mod codecs; pub mod complexobject; +pub mod critical_section; pub mod descrobject; pub mod dictobject; pub mod floatobject; +pub mod genericaliasobject; pub mod import; pub mod listobject; pub mod longobject; +pub mod memoryobject; pub mod methodobject; pub mod moduleobject; pub mod object; +pub mod objimpl; +pub mod osmodule; pub mod pycapsule; pub mod pyerrors; +pub mod pyframe; pub mod pylifecycle; +pub mod pymem; pub mod pystate; +pub mod pystrcmp; pub mod refcount; pub mod setobject; pub mod sliceobject; @@ -52,4 +62,8 @@ pub fn init_main_interpreter(interpreter: Interpreter) { // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used unsafe { init_exception_statics(&Context::genesis().exceptions) }; *interp = Some(interpreter); + MAIN_INTERP_PTR.store( + interp.as_ref().unwrap() as *const _ as *mut _, + Ordering::Release, + ); } diff --git a/crates/capi/src/listobject.rs b/crates/capi/src/listobject.rs index 796720f99d7..03069b0495b 100644 --- a/crates/capi/src/listobject.rs +++ b/crates/capi/src/listobject.rs @@ -165,7 +165,7 @@ pub unsafe extern "C" fn PyList_Sort(list: *mut PyObject) -> c_int { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; diff --git a/crates/capi/src/longobject.rs b/crates/capi/src/longobject.rs index 8c9fe5e1acb..d88523a7ddc 100644 --- a/crates/capi/src/longobject.rs +++ b/crates/capi/src/longobject.rs @@ -1,9 +1,12 @@ -use crate::PyObject; use crate::object::define_py_check; -use crate::pystate::with_vm; -use core::ffi::{c_long, c_longlong, c_ulong, c_ulonglong}; -use rustpython_vm::PyResult; -use rustpython_vm::builtins::PyInt; +use crate::{PyObject, pystate::with_vm}; +use bitflags::bitflags; +use core::ffi::{CStr, c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; +use malachite_bigint::{BigInt, Sign}; +use rustpython_vm::builtins::{PyInt, try_bigint_to_f64, try_f64_to_bigint}; +use rustpython_vm::common::int::bytes_to_int; +use rustpython_vm::protocol::handle_bytes_to_int_err; +use rustpython_vm::{AsObject, PyResult, VirtualMachine}; define_py_check!(fn PyLong_Check, types.int_type); define_py_check!(exact fn PyLong_CheckExact, types.int_type); @@ -38,6 +41,150 @@ pub extern "C" fn PyLong_FromUnsignedLongLong(value: c_ulonglong) -> *mut PyObje with_vm(|vm| vm.ctx.new_int(value)) } +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromDouble(value: c_double) -> *mut PyObject { + with_vm(|vm| Ok(vm.ctx.new_bigint(&try_f64_to_bigint(value, vm)?))) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromInt32(value: i32) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromInt64(value: i64) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromUInt32(value: u32) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromUInt64(value: u64) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(value)) +} + +bitflags! { + #[derive(Clone, Copy)] + struct AsNativeBytesFlags: c_int { + const BIG_ENDIAN = 0; + const LITTLE_ENDIAN = 1; + const NATIVE_ENDIAN = 3; + const UNSIGNED_BUFFER = 4; + const REJECT_NEGATIVE = 8; + const ALLOW_INDEX = 16; + } +} + +impl AsNativeBytesFlags { + #[inline] + fn is_little_endian(self) -> bool { + if self.contains(Self::NATIVE_ENDIAN) { + cfg!(target_endian = "little") + } else { + self.contains(Self::LITTLE_ENDIAN) + } + } + + fn from_bits_or_default(vm: &VirtualMachine, raw_flags: c_int) -> PyResult { + const PY_ASNATIVEBYTES_DEFAULTS: c_int = -1; + if raw_flags == PY_ASNATIVEBYTES_DEFAULTS { + return Ok(Self::default()); + }; + + let flags = Self::from_bits(raw_flags) + .ok_or_else(|| vm.new_value_error("Invalid NativeBytes flags"))?; + if flags.contains(Self::LITTLE_ENDIAN) & flags.contains(Self::NATIVE_ENDIAN) { + Err(vm.new_value_error("Cannot specify both LITTLE_ENDIAN and NATIVE_ENDIAN")) + } else { + Ok(flags) + } + } +} + +impl Default for AsNativeBytesFlags { + fn default() -> Self { + Self::NATIVE_ENDIAN | Self::UNSIGNED_BUFFER + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_FromNativeBytes( + buffer: *const c_void, + n_bytes: usize, + flags: c_int, +) -> *mut PyObject { + with_vm(|vm| { + let flags = AsNativeBytesFlags::from_bits_or_default(vm, flags)?; + let little_endian = flags.is_little_endian(); + let bytes = unsafe { core::slice::from_raw_parts(buffer.cast::(), n_bytes) }; + + let value = if flags.contains(AsNativeBytesFlags::UNSIGNED_BUFFER) { + if little_endian { + BigInt::from_bytes_le(Sign::Plus, bytes) + } else { + BigInt::from_bytes_be(Sign::Plus, bytes) + } + } else if little_endian { + BigInt::from_signed_bytes_le(bytes) + } else { + BigInt::from_signed_bytes_be(bytes) + }; + + Ok(vm.ctx.new_bigint(&value)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_FromUnsignedNativeBytes( + buffer: *const c_void, + n_bytes: usize, + flags: c_int, +) -> *mut PyObject { + with_vm(|vm| { + let flags = AsNativeBytesFlags::from_bits_or_default(vm, flags)?; + let bytes = unsafe { core::slice::from_raw_parts(buffer.cast::(), n_bytes) }; + + let value = if flags.is_little_endian() { + BigInt::from_bytes_le(Sign::Plus, bytes) + } else { + BigInt::from_bytes_be(Sign::Plus, bytes) + }; + + Ok(vm.ctx.new_bigint(&value)) + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyLong_FromVoidPtr(ptr: *mut c_void) -> *mut PyObject { + with_vm(|vm| vm.ctx.new_int(ptr as usize)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_FromString( + str: *const c_char, + pend: *mut *mut c_char, + base: c_int, +) -> *mut PyObject { + with_vm(|vm| { + let bytes = unsafe { CStr::from_ptr(str) }.to_bytes(); + let parsed = bytes_to_int(bytes, base as u32, vm.state.int_max_str_digits.load()) + .map(|value| vm.ctx.new_bigint(&value)); + + if let Some(pend) = unsafe { pend.as_mut() } { + let end_offset = if parsed.is_ok() { bytes.len() } else { 0 }; + unsafe { *pend = bytes.as_ptr().add(end_offset).cast_mut().cast() }; + } + + parsed.map_err(|err| { + let obj = vm.ctx.new_bytes(bytes.to_vec()); + handle_bytes_to_int_err(err, obj.as_object(), vm) + }) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { with_vm::, _>(|vm| { @@ -50,12 +197,173 @@ pub unsafe extern "C" fn PyLong_AsLong(obj: *mut PyObject) -> c_long { }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsDouble(obj: *mut PyObject) -> c_double { + with_vm::, _>(|vm| { + let int = unsafe { &*obj }.try_downcast_ref::(vm)?; + try_bigint_to_f64(int.as_bigint(), vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsInt(obj: *mut PyObject) -> c_int { + with_vm::, _>(|vm| { + unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C int")) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsInt32(obj: *mut PyObject, out: *mut i32) -> c_int { + with_vm(|vm| { + let value: i32 = unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to int32_t"))?; + unsafe { *out = value }; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsInt64(obj: *mut PyObject, out: *mut i64) -> c_int { + with_vm(|vm| { + let value: i64 = unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to int64_t"))?; + unsafe { *out = value }; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsLongLong(obj: *mut PyObject) -> c_longlong { + with_vm::, _>(|vm| { + unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long long")) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsSize_t(obj: *mut PyObject) -> usize { + with_vm::, _>(|vm| { + let value: usize = unsafe { &*obj } + .try_downcast_ref::(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C size_t"))?; + Ok(value) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsSsize_t(obj: *mut PyObject) -> isize { + with_vm::, _>(|vm| { + unsafe { &*obj } + .try_downcast_ref::(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C ssize_t")) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsUInt32(obj: *mut PyObject, out: *mut u32) -> c_int { + with_vm(|vm| { + let value: u32 = unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to uint32_t"))?; + unsafe { *out = value }; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsUInt64(obj: *mut PyObject, out: *mut u64) -> c_int { + with_vm(|vm| { + let value: u64 = unsafe { &*obj } + .to_owned() + .try_index(vm)? + .as_bigint() + .try_into() + .map_err(|_| vm.new_overflow_error("Python int too large to convert to uint64_t"))?; + unsafe { *out = value }; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsUnsignedLong(obj: *mut PyObject) -> c_ulong { + with_vm::, _>(|vm| { + unsafe { &*obj } + .try_downcast_ref::(vm)? + .as_bigint() + .try_into() + .map_err(|_| { + vm.new_overflow_error("Python int too large to convert to C unsigned long") + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsUnsignedLongMask(obj: *mut PyObject) -> c_ulong { + with_vm::, _>(|vm| { + let int = unsafe { &*obj }.to_owned().try_index(vm)?; + if const { c_ulong::BITS == 32 } { + Ok(c_ulong::from(int.as_u32_mask())) + } else { + Ok(int.as_u64_mask() as c_ulong) + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsUnsignedLongLongMask(obj: *mut PyObject) -> c_ulonglong { + with_vm::, _>(|vm| { + let int = unsafe { &*obj }.to_owned().try_index(vm)?; + Ok(int.as_u64_mask()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyLong_AsVoidPtr(obj: *mut PyObject) -> *mut c_void { + with_vm(|vm| { + let value = unsafe { &*obj }.try_downcast_ref::(vm)?; + + let unsigned: Result = value.as_bigint().try_into(); + if let Ok(v) = unsigned { + return Ok(v as *mut c_void); + } + let signed: Result = value.as_bigint().try_into(); + if let Ok(v) = signed { + return Ok((v as usize) as *mut c_void); + } + + Err(vm.new_overflow_error("int too large to convert to pointer")) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyLong_AsUnsignedLongLong(obj: *mut PyObject) -> c_ulonglong { with_vm::, _>(|vm| { unsafe { &*obj } - .to_owned() - .try_downcast::(vm)? + .try_downcast_ref::(vm)? .as_bigint() .try_into() .map_err(|_| { @@ -64,13 +372,13 @@ pub unsafe extern "C" fn PyLong_AsUnsignedLongLong(obj: *mut PyObject) -> c_ulon }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyInt; #[test] - fn test_py_int_u32() { + fn py_int_u32() { Python::attach(|py| { let number = PyInt::new(py, 123); assert!(number.is_instance_of::()); @@ -79,11 +387,20 @@ mod tests { } #[test] - fn test_py_int_u64() { + fn py_int_u64() { Python::attach(|py| { let number = PyInt::new(py, 123u64); assert!(number.is_instance_of::()); assert_eq!(number.extract::().unwrap(), 123); }) } + + #[test] + fn py_int_u128() { + Python::attach(|py| { + let value = 1u128 << 100; + let number = PyInt::new(py, value); + assert_eq!(number.extract::().unwrap(), value); + }) + } } diff --git a/crates/capi/src/memoryobject.rs b/crates/capi/src/memoryobject.rs new file mode 100644 index 00000000000..11019524e6c --- /dev/null +++ b/crates/capi/src/memoryobject.rs @@ -0,0 +1,37 @@ +use crate::object::define_py_check; +use crate::{PyObject, pystate::with_vm}; +use rustpython_vm::PyPayload; +use rustpython_vm::builtins::PyMemoryView; + +define_py_check!(fn PyMemoryView_Check, types.memoryview_type); + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMemoryView_FromObject(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let obj = unsafe { &*obj }; + Ok(PyMemoryView::from_object(obj, vm)?.into_ref(&vm.ctx)) + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyBytes, PyMemoryView}; + + #[test] + fn memoryview_from_bytes() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"hello"); + let view = PyMemoryView::from(&bytes).unwrap(); + + assert!(view.is_instance_of::()); + + let copied = view + .call_method1("tobytes", ()) + .unwrap() + .cast_into::() + .unwrap(); + assert_eq!(copied.as_bytes(), b"hello"); + }) + } +} diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index c0a6611a01f..2be54ca8939 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -2,7 +2,8 @@ use crate::PyObject; use crate::object::PyTypeObject; use crate::object::define_py_check; use crate::pystate::with_vm; -use core::ffi::{CStr, c_char, c_int}; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int}; use core::ptr::NonNull; use rustpython_vm::function::{FuncArgs, HeapMethodDef, PosArgs, PyMethodFlags}; use rustpython_vm::{AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine}; @@ -46,20 +47,13 @@ pub(crate) fn build_method_def( ml: &PyMethodDef, has_self: bool, ) -> PyResult> { - let name = unsafe { CStr::from_ptr(ml.ml_name) } - .to_str() - .map_err(|_| vm.new_system_error("Method name was not valid UTF-8"))?; - - let doc = NonNull::new(ml.ml_doc.cast_mut()) - .map(|doc| { - unsafe { CStr::from_ptr(doc.as_ptr()) } - .to_str() - .map_err(|_| vm.new_system_error("Method doc was not valid UTF-8")) - }) - .transpose()?; + let name = unsafe { ml.ml_name.try_as_str(vm) }?; + + let doc = unsafe { ml.ml_doc.try_as_str_opt(vm) }?; let flags = PyMethodFlags::from_bits(ml.ml_flags as u32) .ok_or_else(|| vm.new_system_error("PyMethodDef contains unknown flags"))?; + let has_self = has_self && !flags.contains(PyMethodFlags::STATIC); let method = ml.ml_meth; @@ -306,7 +300,7 @@ pub unsafe extern "C" fn PyCFunction_NewEx( unsafe { PyCMethod_New(ml, slf, module, core::ptr::null_mut()) } } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::PyException; use pyo3::ffi::{PyLong_FromLong, PyObject}; @@ -314,7 +308,7 @@ mod tests { use pyo3::types::{PyCFunction, PyInt, PyString}; #[test] - fn test_closure_function() { + fn closure_function() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| "Hello from Rust!") .unwrap(); @@ -327,7 +321,7 @@ mod tests { } #[test] - fn test_function_no_args() { + fn function_no_args() { Python::attach(|py| { unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { assert!(_self.is_null()); @@ -352,7 +346,7 @@ mod tests { } #[test] - fn test_closure_function_error() { + fn closure_function_error() { Python::attach(|py| { let f = PyCFunction::new_closure(py, None, None, |_args, _kwargs| { Err::<(), _>(PyException::new_err("Something went wrong")) @@ -366,4 +360,17 @@ mod tests { ); }) } + + #[test] + fn wrap_static_no_args_function() { + #[pyfunction()] + fn f() {} + + Python::attach(|py| { + let module = PyModule::new(py, "test_wrap_pyfunction_forms").unwrap(); + + let func = wrap_pyfunction!(f, &module).unwrap(); + func.call0().unwrap(); + }); + } } diff --git a/crates/capi/src/object.rs b/crates/capi/src/object.rs index 5e601d3817d..641dbe3ef9f 100644 --- a/crates/capi/src/object.rs +++ b/crates/capi/src/object.rs @@ -1,13 +1,16 @@ use crate::PyObject; use crate::pystate::with_vm; -use core::ffi::{CStr, c_char, c_int, c_uint, c_ulong, c_void}; +use crate::util::CStrExt; +use core::ffi::{c_char, c_int, c_uint, c_void}; use core::ptr::NonNull; -use rustpython_vm::builtins::{PyStr, PyType, object_generic_set_dict, object_get_dict}; +pub use pytype::*; +use rustpython_vm::builtins::{PyStr, object_generic_set_dict, object_get_dict}; use rustpython_vm::bytecode::ComparisonOperator; use rustpython_vm::function::PySetterValue; -use rustpython_vm::{AsObject, Py, PyPayload}; +use rustpython_vm::types::{PyComparisonOp, hash_not_implemented}; +use rustpython_vm::{AsObject, PyPayload, PyResult, VirtualMachine}; -pub type PyTypeObject = Py; +mod pytype; macro_rules! define_py_check { (fn $name:ident, $($ctx_path:ident).+) => { @@ -36,89 +39,28 @@ macro_rules! define_py_check { } pub(crate) use define_py_check; -define_py_check!(fn PyType_Check, types.type_type); -define_py_check!(exact fn PyType_CheckExact, types.type_type); -#[unsafe(no_mangle)] -pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { - unsafe { (*op).class() } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int { - with_vm(|_vm| { - let obj = unsafe { &*op }; - let ty = unsafe { &*ty }; - obj.class().is(ty) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { - let ty = unsafe { &*ptr }; - ty.slots.flags.bits() as u32 as c_ulong -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_IsSubtype(a: *const PyTypeObject, b: *const PyTypeObject) -> c_int { - with_vm(move |_vm| { - let a = unsafe { &*a }; - let b = unsafe { &*b }; - Ok(a.is_subtype(b)) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_GetName(ptr: *const PyTypeObject) -> *mut PyObject { - with_vm(|vm| unsafe { &*ptr }.__name__(vm)) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_GetQualName(ptr: *const PyTypeObject) -> *mut PyObject { - with_vm(|vm| unsafe { &*ptr }.__qualname__(vm)) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_GetModuleName(ptr: *const PyTypeObject) -> *mut PyObject { - with_vm(|vm| unsafe { &*ptr }.__module__(vm)) +#[inline] +fn get_constant(vm: &VirtualMachine, constant_id: c_uint) -> PyResult<&PyObject> { + let ctx = &vm.ctx; + match constant_id { + 0 => Ok(ctx.none.as_object()), + 1 => Ok(ctx.false_value.as_object()), + 2 => Ok(ctx.true_value.as_object()), + 3 => Ok(ctx.ellipsis.as_object()), + 4 => Ok(ctx.not_implemented.as_object()), + _ => Err(vm.new_system_error("Invalid constant ID passed to Py_GetConstantBorrowed")), + } } #[unsafe(no_mangle)] -pub unsafe extern "C" fn PyType_GetFullyQualifiedName(ptr: *const PyTypeObject) -> *mut PyObject { - with_vm(|vm| { - let ty = unsafe { &*ptr }; - let qualname = ty.__qualname__(vm).try_downcast::(vm)?; - let module = ty.__module__(vm); - - if let Some(module) = module.downcast_ref::() - && module.as_wtf8() != "builtins" - { - Ok(vm.ctx.new_str(format!("{module}.{qualname}"))) - } else { - Ok(qualname) - } - }) +pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { + with_vm(|vm| get_constant(vm, constant_id).map(PyObject::as_raw)) } #[unsafe(no_mangle)] -pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject { - with_vm(|vm| { - let ctx = &vm.ctx; - let constant = match constant_id { - 0 => ctx.none.as_object(), - 1 => ctx.false_value.as_object(), - 2 => ctx.true_value.as_object(), - 3 => ctx.ellipsis.as_object(), - 4 => ctx.not_implemented.as_object(), - _ => { - return Err( - vm.new_system_error("Invalid constant ID passed to Py_GetConstantBorrowed") - ); - } - } - .as_raw(); - Ok(constant) - }) +pub extern "C" fn Py_GetConstant(constant_id: c_uint) -> *mut PyObject { + with_vm(|vm| get_constant(vm, constant_id).map(ToOwned::to_owned)) } #[unsafe(no_mangle)] @@ -140,15 +82,21 @@ pub unsafe extern "C" fn PyObject_GetAttrString( ) -> *mut PyObject { with_vm(|vm| { let obj = unsafe { &*obj }; - let name = unsafe { - CStr::from_ptr(attr_name) - .to_str() - .expect("attribute name must be valid UTF-8") - }; + let name = unsafe { attr_name.try_as_str(vm) }?; obj.get_attr(name, vm) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_ASCII(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.ascii(vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Bytes(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*obj }.to_owned().bytes(vm)) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_GetOptionalAttr( obj: *mut PyObject, @@ -172,6 +120,29 @@ pub unsafe extern "C" fn PyObject_GetOptionalAttr( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GetOptionalAttrString( + obj: *mut PyObject, + attr_name: *const c_char, + result: *mut *mut PyObject, +) -> c_int { + with_vm(|vm| { + unsafe { + *result = core::ptr::null_mut(); + } + let obj = unsafe { &*obj }; + let name = unsafe { attr_name.try_as_str(vm) }?; + if let Some(attr) = vm.get_attribute_opt(obj.to_owned(), name)? { + unsafe { + *result = attr.into_raw().as_ptr(); + } + Ok(true) + } else { + Ok(false) + } + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_SetAttrString( obj: *mut PyObject, @@ -180,9 +151,7 @@ pub unsafe extern "C" fn PyObject_SetAttrString( ) -> c_int { with_vm(|vm| { let obj = unsafe { &*obj }; - let name = unsafe { CStr::from_ptr(attr_name) } - .to_str() - .expect("attribute name must be valid UTF-8"); + let name = unsafe { attr_name.try_as_str(vm) }?; let value = unsafe { &*value }.to_owned(); obj.set_attr(name, value, vm) }) @@ -202,6 +171,44 @@ pub unsafe extern "C" fn PyObject_SetAttr( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_DelAttr(obj: *mut PyObject, name: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { &*name }.try_downcast_ref::(vm)?; + obj.del_attr(name, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_DelAttrString( + obj: *mut PyObject, + attr_name: *const c_char, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { attr_name.try_as_str(vm) }?; + obj.del_attr(name, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GenericSetAttr( + obj: *mut PyObject, + name: *mut PyObject, + value: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { &*name }.try_downcast_ref::(vm)?; + let value = match NonNull::new(value) { + Some(value) => PySetterValue::Assign(unsafe { value.as_ref() }.to_owned()), + None => PySetterValue::Delete, + }; + obj.generic_setattr(name, value, vm) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_HasAttrWithError( obj: *mut PyObject, @@ -210,7 +217,62 @@ pub unsafe extern "C" fn PyObject_HasAttrWithError( with_vm(|vm| { let obj = unsafe { &*obj }; let name = unsafe { &*attr_name }.try_downcast_ref::(vm)?; - obj.has_attr(name, vm) + Ok(vm.get_attribute_opt(obj.to_owned(), name)?.is_some()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_HasAttr(obj: *mut PyObject, attr_name: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = match unsafe { &*attr_name }.try_downcast_ref::(vm) { + Ok(name) => name, + Err(err) => { + vm.run_unraisable(err, None, obj.to_owned()); + return false; + } + }; + + match obj.has_attr(name, vm) { + Ok(has_attr) => has_attr, + Err(err) => { + vm.run_unraisable(err, None, obj.to_owned()); + false + } + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_HasAttrString( + obj: *mut PyObject, + attr_name: *const c_char, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let Ok(name) = (unsafe { attr_name.try_as_str(vm) }) else { + return false; + }; + + match obj.has_attr(name, vm) { + Ok(has_attr) => has_attr, + Err(err) => { + vm.run_unraisable(err, None, obj.to_owned()); + false + } + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_HasAttrStringWithError( + obj: *mut PyObject, + attr_name: *const c_char, +) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let name = unsafe { attr_name.try_as_str(vm) }?; + Ok(vm.get_attribute_opt(obj.to_owned(), name)?.is_some()) }) } @@ -248,6 +310,20 @@ pub extern "C" fn PyObject_Str(obj: *mut PyObject) -> *mut PyObject { }) } +#[inline] +fn parse_richcompare_op(vm: &VirtualMachine, op: c_int) -> PyResult { + match op { + 0 => Ok(ComparisonOperator::Less), + 1 => Ok(ComparisonOperator::LessOrEqual), + 2 => Ok(ComparisonOperator::Equal), + 3 => Ok(ComparisonOperator::NotEqual), + 4 => Ok(ComparisonOperator::Greater), + 5 => Ok(ComparisonOperator::GreaterOrEqual), + _ => Err(vm.new_system_error("invalid comparison operator")), + } + .map(Into::into) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_RichCompare( left: *mut PyObject, @@ -255,19 +331,23 @@ pub unsafe extern "C" fn PyObject_RichCompare( op: c_int, ) -> *mut PyObject { with_vm(|vm| { - let op = match op { - 0 => ComparisonOperator::Less, - 1 => ComparisonOperator::LessOrEqual, - 2 => ComparisonOperator::Equal, - 3 => ComparisonOperator::NotEqual, - 4 => ComparisonOperator::Greater, - 5 => ComparisonOperator::GreaterOrEqual, - _ => return Err(vm.new_system_error("invalid comparison operator")), - }; let left = unsafe { &*left }; let right = unsafe { &*right }; left.to_owned() - .rich_compare(right.to_owned(), op.into(), vm) + .rich_compare(right.to_owned(), parse_richcompare_op(vm, op)?, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_RichCompareBool( + left: *mut PyObject, + right: *mut PyObject, + op: c_int, +) -> c_int { + with_vm(|vm| { + let left = unsafe { &*left }; + let right = unsafe { &*right }; + left.rich_compare_bool(right, parse_richcompare_op(vm, op)?, vm) }) } @@ -298,6 +378,69 @@ pub unsafe extern "C" fn PyObject_IsTrue(obj: *mut PyObject) -> c_int { }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Not(obj: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + obj.to_owned().not(vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Hash(obj: *mut PyObject) -> isize { + with_vm(|vm| { + let obj = unsafe { &*obj }; + obj.hash(vm).map(|hash| hash as isize) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_HashNotImplemented(obj: *mut PyObject) -> isize { + with_vm(|vm| { + let obj = unsafe { &*obj }; + hash_not_implemented(obj, vm).map(|hash| hash as isize) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_SelfIter(obj: *mut PyObject) -> *mut PyObject { + with_vm(|_vm| unsafe { (&*obj).to_owned() }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int { + (x == y) as c_int +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_IsNone(x: *mut PyObject) -> c_int { + with_vm(|vm| vm.is_none(unsafe { &*x })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_ReprEnter(obj: *mut PyObject) -> c_int { + with_vm(|vm| { + let obj = unsafe { &*obj }; + let id = obj.get_id(); + let mut guards = vm.repr_guards.borrow_mut(); + if guards.contains(&id) { + true + } else { + guards.insert(id); + false + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_ReprLeave(obj: *mut PyObject) { + with_vm(|vm| { + vm.repr_guards + .borrow_mut() + .remove(&unsafe { &*obj }.get_id()); + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyObject_GenericGetDict( obj: *mut PyObject, @@ -325,11 +468,11 @@ pub unsafe extern "C" fn PyObject_GenericSetDict( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::class::basic::CompareOp; use pyo3::prelude::*; - use pyo3::types::{PyBool, PyDict, PyInt, PyString, PyTypeMethods}; + use pyo3::types::{PyBool, PyDict, PyInt}; #[test] fn is_truthy() { @@ -353,14 +496,6 @@ mod tests { }) } - #[test] - fn type_name() { - Python::attach(|py| { - let string = PyString::new(py, "Hello, World!"); - assert_eq!(string.get_type().name().unwrap().to_str().unwrap(), "str"); - }) - } - #[test] fn repr() { Python::attach(|py| { @@ -438,16 +573,6 @@ mod tests { }) } - #[test] - fn type_get_module_name() { - Python::attach(|py| { - assert_eq!( - py.get_type::().module().unwrap().to_str().unwrap(), - "builtins" - ); - }) - } - #[test] fn generic_get_dict() { Python::attach(|py| { @@ -466,4 +591,16 @@ mod tests { assert!(dict.get_item("foo").is_ok()); }) } + + #[test] + fn hasattr() { + Python::attach(|py| { + let x = 5i32.into_pyobject(py).unwrap(); + assert!(x.is_instance_of::()); + + // spell-checker:ignore bbbbbbytes + assert!(x.hasattr("to_bytes").unwrap()); + assert!(!x.hasattr("bbbbbbytes").unwrap()); + }) + } } diff --git a/crates/capi/src/object/pytype.rs b/crates/capi/src/object/pytype.rs new file mode 100644 index 00000000000..daad7b3133b --- /dev/null +++ b/crates/capi/src/object/pytype.rs @@ -0,0 +1,95 @@ +use crate::object::define_py_check; +use crate::pystate::with_vm; +use core::ffi::{c_int, c_ulong}; +use rustpython_vm::builtins::{PyStr, PyType}; +use rustpython_vm::{AsObject, Py, PyObject}; + +pub type PyTypeObject = Py; + +define_py_check!(fn PyType_Check, types.type_type); +define_py_check!(exact fn PyType_CheckExact, types.type_type); + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_TYPE(op: *mut PyObject) -> *const PyTypeObject { + unsafe { (*op).class() } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_IS_TYPE(op: *mut PyObject, ty: *mut PyTypeObject) -> c_int { + with_vm(|_vm| { + let obj = unsafe { &*op }; + let ty = unsafe { &*ty }; + obj.class().is(ty) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetFlags(ptr: *const PyTypeObject) -> c_ulong { + let ty = unsafe { &*ptr }; + ty.slots.flags.bits() as u32 as c_ulong +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_IsSubtype(a: *const PyTypeObject, b: *const PyTypeObject) -> c_int { + with_vm(move |_vm| { + let a = unsafe { &*a }; + let b = unsafe { &*b }; + Ok(a.is_subtype(b)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetName(ptr: *const PyTypeObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*ptr }.__name__(vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetQualName(ptr: *const PyTypeObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*ptr }.__qualname__(vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetModuleName(ptr: *const PyTypeObject) -> *mut PyObject { + with_vm(|vm| unsafe { &*ptr }.__module__(vm)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyType_GetFullyQualifiedName(ptr: *const PyTypeObject) -> *mut PyObject { + with_vm(|vm| { + let ty = unsafe { &*ptr }; + let qualname = ty.__qualname__(vm).try_downcast::(vm)?; + let module = ty.__module__(vm); + + if let Some(module) = module.downcast_ref::() + && module.as_wtf8() != "builtins" + { + Ok(vm.ctx.new_str(format!("{module}.{qualname}"))) + } else { + Ok(qualname) + } + }) +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + use pyo3::types::{PyInt, PyString, PyTypeMethods}; + + #[test] + fn type_name() { + Python::attach(|py| { + let string = PyString::new(py, "Hello, World!"); + assert_eq!(string.get_type().name().unwrap().to_str().unwrap(), "str"); + }) + } + + #[test] + fn type_get_module_name() { + Python::attach(|py| { + assert_eq!( + py.get_type::().module().unwrap().to_str().unwrap(), + "builtins" + ); + }) + } +} diff --git a/crates/capi/src/objimpl.rs b/crates/capi/src/objimpl.rs new file mode 100644 index 00000000000..aa74bb69379 --- /dev/null +++ b/crates/capi/src/objimpl.rs @@ -0,0 +1,86 @@ +use crate::PyObject; +use crate::pymem::{PyMem_Calloc, PyMem_Free, PyMem_Malloc, PyMem_Realloc}; +use crate::pystate::with_vm; +use core::ffi::{c_int, c_void}; +use rustpython_vm::gc_state; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GC_Track(op: *mut PyObject) { + with_vm(|_vm| { + let obj = unsafe { &*op }; + if !obj.is_gc_tracked() { + unsafe { gc_state::gc_state().track_object(obj.into(), gc_state::current_owner()) }; + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GC_UnTrack(op: *mut PyObject) { + with_vm(|_vm| { + let obj = unsafe { &*op }; + if obj.is_gc_tracked() { + unsafe { gc_state::gc_state().untrack_object(obj.into()) }; + } + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GC_IsTracked(op: *mut PyObject) -> c_int { + with_vm(|_vm| unsafe { (&*op).is_gc_tracked() }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_GC_IsFinalized(op: *mut PyObject) -> c_int { + with_vm(|_vm| unsafe { (&*op).gc_finalized() }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGC_Collect() -> isize { + with_vm(|vm| { + let result = vm.state.gc.collect(2); + (result.collected + result.uncollectable) as isize + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGC_Enable() -> c_int { + with_vm(|vm| { + let was_enabled: c_int = vm.state.gc.is_enabled().into(); + vm.state.gc.enable(); + was_enabled + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGC_Disable() -> c_int { + with_vm(|vm| { + let was_enabled: c_int = vm.state.gc.is_enabled().into(); + vm.state.gc.disable(); + was_enabled + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyGC_IsEnabled() -> c_int { + with_vm(|vm| -> c_int { vm.state.gc.is_enabled().into() }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Malloc(size: usize) -> *mut c_void { + unsafe { PyMem_Malloc(size) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Calloc(nelem: usize, elsize: usize) -> *mut c_void { + unsafe { PyMem_Calloc(nelem, elsize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Realloc(ptr: *mut c_void, new_size: usize) -> *mut c_void { + unsafe { PyMem_Realloc(ptr, new_size) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyObject_Free(ptr: *mut c_void) { + unsafe { PyMem_Free(ptr) } +} diff --git a/crates/capi/src/osmodule.rs b/crates/capi/src/osmodule.rs new file mode 100644 index 00000000000..132935ee8af --- /dev/null +++ b/crates/capi/src/osmodule.rs @@ -0,0 +1,12 @@ +use crate::{PyObject, pystate::with_vm}; +use rustpython_vm::convert::ToPyObject; +use rustpython_vm::function::FsPath; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyOS_FSPath(path: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + let path = unsafe { &*path }.to_owned(); + let fspath = FsPath::try_from_path_like(path, false, vm)?; + Ok(fspath.to_pyobject(vm)) + }) +} diff --git a/crates/capi/src/pycapsule.rs b/crates/capi/src/pycapsule.rs index 7a5d599c851..f5b792eec4d 100644 --- a/crates/capi/src/pycapsule.rs +++ b/crates/capi/src/pycapsule.rs @@ -1,5 +1,6 @@ use crate::PyObject; use crate::pystate::with_vm; +use crate::util::CStrExt; use core::ffi::{CStr, c_char, c_int, c_void}; use core::ptr::NonNull; use rustpython_vm::builtins::PyCapsule; @@ -47,6 +48,11 @@ pub unsafe extern "C" fn PyCapsule_GetContext(capsule: *mut PyObject) -> *mut c_ let capsule = unsafe { &*capsule } .downcast_ref_if_exact::(vm) .ok_or_else(|| vm.new_value_error("Invalid capsule"))?; + + if capsule.pointer().is_null() { + return Err(vm.new_value_error("Capsule has null pointer")); + } + Ok(capsule.context()) }) } @@ -93,9 +99,7 @@ pub unsafe extern "C" fn PyCapsule_IsValid(capsule: *mut PyObject, name: *const #[unsafe(no_mangle)] pub unsafe extern "C" fn PyCapsule_Import(name: *const c_char, _no_block: c_int) -> *mut c_void { with_vm(|vm| { - let capsule_name = unsafe { CStr::from_ptr(name) } - .to_str() - .map_err(|_| vm.new_system_error("capsule name is not valid UTF-8"))?; + let capsule_name = unsafe { name.try_as_str(vm) }?; let (module_name, attrs_path) = capsule_name.split_once('.').ok_or_else(|| { vm.new_import_error( "capsule name is missing attribute path", @@ -142,13 +146,14 @@ fn checked_capsule<'a>( Ok(capsule) } -#[cfg(false)] +#[cfg(test)] mod tests { + use pyo3::ffi; use pyo3::prelude::*; use pyo3::types::PyCapsule; #[test] - fn test_capsule_new() { + fn capsule_new() { Python::attach(|py| { let value = String::from("Some data"); let capsule = PyCapsule::new_with_value(py, value, c"my_capsule").unwrap(); @@ -157,4 +162,21 @@ mod tests { assert_eq!(unsafe { ptr.cast::().as_ref() }, "Some data"); }) } + + #[test] + fn capsule_context_on_invalid_capsule() { + Python::attach(|py| { + let cap = PyCapsule::new_with_value(py, 123u32, c"name").unwrap(); + + // Invalidate the capsule + // SAFETY: intentionally breaking the capsule for testing + unsafe { + ffi::PyCapsule_SetPointer(cap.as_ptr(), core::ptr::null_mut()); + } + + // context() on invalid capsule should fail + let result = cap.context(); + assert!(result.is_err()); + }); + } } diff --git a/crates/capi/src/pyerrors.rs b/crates/capi/src/pyerrors.rs index b767b7c4090..a4ead856be9 100644 --- a/crates/capi/src/pyerrors.rs +++ b/crates/capi/src/pyerrors.rs @@ -1,8 +1,10 @@ use crate::object::define_py_check; +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; use core::convert::Infallible; -use core::ffi::{CStr, c_char, c_int}; +use core::ffi::{c_char, c_int}; use core::ptr::NonNull; +use core::slice; use rustpython_vm::builtins::{PyBaseException, PyTuple, PyType}; use rustpython_vm::convert::IntoObject; use rustpython_vm::exceptions::ExceptionZoo; @@ -143,15 +145,9 @@ pub unsafe extern "C" fn PyErr_SetObject(exception: *mut PyObject, value: *mut P pub unsafe extern "C" fn PyErr_SetString(exception: *mut PyObject, message: *const c_char) { with_vm::, _>(|vm| { let exc_type = unsafe { &*exception }.try_downcast_ref::(vm)?; + let message = unsafe { message.try_as_str(vm) }?; - let Ok(message) = unsafe { CStr::from_ptr(message) }.to_str() else { - return Err(vm.new_type_error("Exception message is not valid UTF-8")); - }; - - let exc = vm.invoke_exception( - exc_type.to_owned(), - vec![vm.ctx.new_str(message).into_object()], - )?; + let exc = vm.invoke_exception(exc_type, vec![vm.ctx.new_str(message).into_object()])?; Err(exc) }) @@ -209,13 +205,10 @@ pub unsafe extern "C" fn PyErr_NewException( dict: *mut PyObject, ) -> *mut PyObject { with_vm(|vm| { - let (module, name) = unsafe { - CStr::from_ptr(name) - .to_str() - .expect("Exception name is not valid UTF-8") - .rsplit_once('.') - .expect("Exception name must be of the form 'module.ExceptionName'") - }; + let (module, name) = unsafe { name.try_as_str(vm) } + .expect("Exception name is not valid UTF-8") + .rsplit_once('.') + .expect("Exception name must be of the form 'module.ExceptionName'"); let bases = unsafe { base.as_ref() }.map(|bases| { if let Some(ty) = bases.downcast_ref::() { @@ -299,9 +292,86 @@ pub unsafe extern "C" fn PyException_GetContext(exc: *mut PyObject) -> *mut PyOb }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetCause(exc: *mut PyObject, cause: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let cause = NonNull::new(cause) + .map(|obj| unsafe { PyObjectRef::from_raw(obj).downcast_unchecked() }); + exc.set___cause__(cause); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetContext(exc: *mut PyObject, context: *mut PyObject) { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let context = NonNull::new(context) + .map(|obj| unsafe { PyObjectRef::from_raw(obj).downcast_unchecked() }); + exc.set___context__(context); + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicodeDecodeError_Create( + encoding: *const c_char, + object: *const c_char, + length: isize, + start: isize, + end: isize, + reason: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let encoding = unsafe { encoding.try_as_str(vm) }?; + let reason = unsafe { reason.try_as_str(vm) }?; + let length: usize = length + .try_into() + .map_err(|_| vm.new_system_error("length must be non-negative"))?; + let start: usize = start + .try_into() + .map_err(|_| vm.new_system_error("start must be non-negative"))?; + let end: usize = end + .try_into() + .map_err(|_| vm.new_system_error("end must be non-negative"))?; + + let bytes = if object.is_null() { + if length != 0 { + return Err(vm.new_system_error( + "PyUnicodeDecodeError_Create called with null object and non-zero length", + )); + } + Vec::new() + } else { + unsafe { slice::from_raw_parts(object.cast::(), length) }.to_vec() + }; + + let exc = vm.new_unicode_decode_error( + vm.ctx.new_str(encoding), + vm.ctx.new_bytes(bytes), + start, + end, + vm.ctx.new_str(reason), + ); + Ok(exc) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyException_SetTraceback(exc: *mut PyObject, tb: *mut PyObject) -> c_int { + with_vm(|vm| { + let exc = unsafe { &*exc }.try_downcast_ref::(vm)?; + let traceback = unsafe { tb.as_ref() }.map(|obj| obj.to_owned()); + exc.set___traceback__(vm.unwrap_or_none(traceback), vm) + }) +} + #[cfg(test)] mod tests { - use pyo3::exceptions::PyTypeError; + use pyo3::PyTypeInfo; + use pyo3::create_exception; + use pyo3::exceptions::{PyException, PyTypeError}; use pyo3::prelude::*; #[test] @@ -309,7 +379,7 @@ mod tests { Python::attach(|py| { PyTypeError::new_err(py.None()).restore(py); assert!(PyErr::occurred(py)); - assert!(unsafe { !pyo3::ffi::PyErr_GetRaisedException().is_null() }); + assert!(PyErr::take(py).is_some()); assert!(!PyErr::occurred(py)); }) } @@ -321,4 +391,19 @@ mod tests { assert!(err.is_instance_of::(py)); }) } + + #[test] + fn new_exception_type() { + create_exception!(my_module, MyError, PyException, "Some description."); + + Python::attach(|py| { + let exc = MyError::new_err("This is a new exception"); + assert!(exc.is_instance_of::(py)); + let exc_type = MyError::type_object(py); + assert_eq!( + exc_type.fully_qualified_name().unwrap(), + "my_module.MyError" + ); + }) + } } diff --git a/crates/capi/src/pyframe.rs b/crates/capi/src/pyframe.rs new file mode 100644 index 00000000000..611cf79b0b6 --- /dev/null +++ b/crates/capi/src/pyframe.rs @@ -0,0 +1,21 @@ +use crate::pystate::with_vm; +use core::ffi::c_int; +use rustpython_vm::Py; +use rustpython_vm::builtins::PyCode; +use rustpython_vm::frame::FrameObject; + +pub type PyFrameObject = Py; +pub type PyCodeObject = Py; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyFrame_GetCode(frame: *mut PyFrameObject) -> *mut PyCodeObject { + with_vm(|_vm| Ok(unsafe { &*frame }.f_code())) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyFrame_GetLineNumber(frame: *mut PyFrameObject) -> c_int { + with_vm(|_vm| { + let lineno = unsafe { &*frame }.f_lineno(); + Ok(lineno.try_into().unwrap_or(c_int::MAX)) + }) +} diff --git a/crates/capi/src/pylifecycle.rs b/crates/capi/src/pylifecycle.rs index 6760b2822a3..7255a0e34a5 100644 --- a/crates/capi/src/pylifecycle.rs +++ b/crates/capi/src/pylifecycle.rs @@ -1,12 +1,18 @@ use crate::get_main_interpreter; use crate::pyerrors::init_exception_statics; use crate::pystate::ensure_thread_has_vm_attached; -use core::ffi::c_int; +use alloc::ffi::CString; +use core::ffi::{c_char, c_int, c_ulong}; +use core::sync::atomic::{AtomicPtr, Ordering}; +use rustpython_vm::common::rc::PyRc; +use rustpython_vm::stdlib::sys; +use rustpython_vm::version::{MAJOR, MICRO, MINOR, RUSTPYTHON_BUILD_INFO, VERSION_HEX}; use rustpython_vm::vm::thread::ThreadedVirtualMachine; use rustpython_vm::{Context, Interpreter}; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; pub(crate) static MAIN_INTERP: Mutex> = Mutex::new(None); +pub(crate) static MAIN_INTERP_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); /// Request a thread local vm from the main interpreter pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { @@ -16,9 +22,12 @@ pub(crate) fn request_vm_from_interpreter() -> ThreadedVirtualMachine { .enter(|vm| vm.new_thread()) } +#[unsafe(no_mangle)] +pub static Py_Version: c_ulong = VERSION_HEX as c_ulong; + #[unsafe(no_mangle)] pub extern "C" fn Py_IsInitialized() -> c_int { - get_main_interpreter().is_some() as c_int + !MAIN_INTERP_PTR.load(Ordering::Acquire).is_null() as c_int } #[unsafe(no_mangle)] @@ -32,7 +41,23 @@ pub extern "C" fn Py_InitializeEx(_initsigs: c_int) { if interp.is_none() { // Safety: Interpreter was not initialized before, so we can safely assume the statics are not used unsafe { init_exception_statics(&Context::genesis().exceptions) }; - *interp = Interpreter::with_init(Default::default(), |_vm| {}).into(); + let builder = Interpreter::builder(Default::default()); + let defs = rustpython_stdlib::stdlib_module_defs(&builder.ctx); + *interp = builder + .add_native_modules(&defs) + .init_hook(|vm| { + let state = PyRc::get_mut(&mut vm.state).unwrap(); + let path = rustpython_pylib::LIB_PATH.to_owned(); + + state.config.paths.stdlib_dir = Some(path.clone()); + state.config.paths.module_search_paths.insert(0, path); + }) + .build() + .into(); + MAIN_INTERP_PTR.store( + interp.as_ref().unwrap() as *const _ as *mut _, + Ordering::Release, + ); drop(interp); ensure_thread_has_vm_attached(); } @@ -52,3 +77,50 @@ pub extern "C" fn Py_FinalizeEx() -> c_int { pub extern "C" fn Py_IsFinalizing() -> c_int { 0 } + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetVersion() -> *const c_char { + static VERSION: LazyLock = LazyLock::new(|| { + CString::new(format!("{MAJOR}.{MINOR}.{MICRO}")) + .expect("version string must not contain interior NULs") + }); + VERSION.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetBuildInfo() -> *const c_char { + static BUILD_INFO: LazyLock = LazyLock::new(|| { + CString::new(RUSTPYTHON_BUILD_INFO).expect("build info must not contain interior NULs") + }); + BUILD_INFO.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetCompiler() -> *const c_char { + c"[RUST]".as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetCopyright() -> *const c_char { + sys::COPYRIGHT.as_ptr() +} + +#[unsafe(no_mangle)] +pub extern "C" fn Py_GetPlatform() -> *const c_char { + sys::PLATFORM.as_ptr() +} + +#[cfg(test)] +mod tests { + use pyo3::prelude::*; + + #[test] + fn get_version() { + Python::attach(|py| { + let version = py.version_info(); + assert!(version >= (3, 14)); + }); + + assert!(unsafe { pyo3::ffi::Py_Version } >= 0x030d0000); + } +} diff --git a/crates/capi/src/pymem.rs b/crates/capi/src/pymem.rs new file mode 100644 index 00000000000..6c8b66f5d3e --- /dev/null +++ b/crates/capi/src/pymem.rs @@ -0,0 +1,46 @@ +use core::ffi::c_void; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_Malloc(n: usize) -> *mut c_void { + unsafe { libc::malloc(if n == 0 { 1 } else { n }) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_Calloc(nelem: usize, elsize: usize) -> *mut c_void { + unsafe { + libc::calloc( + if nelem == 0 || elsize == 0 { 1 } else { nelem }, + if nelem == 0 || elsize == 0 { 1 } else { elsize }, + ) + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_Realloc(ptr: *mut c_void, new_size: usize) -> *mut c_void { + unsafe { libc::realloc(ptr, if new_size == 0 { 1 } else { new_size }) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_Free(ptr: *mut c_void) { + unsafe { libc::free(ptr) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_RawMalloc(n: usize) -> *mut c_void { + unsafe { libc::malloc(n) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_RawCalloc(nelem: usize, elsize: usize) -> *mut c_void { + unsafe { libc::calloc(nelem, elsize) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_RawRealloc(ptr: *mut c_void, new_size: usize) -> *mut c_void { + unsafe { libc::realloc(ptr, new_size) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyMem_RawFree(ptr: *mut c_void) { + unsafe { libc::free(ptr) } +} diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index f3d12f04a0b..cec3bc240b1 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -1,11 +1,12 @@ -use crate::pylifecycle::request_vm_from_interpreter; +use crate::pylifecycle::{MAIN_INTERP_PTR, request_vm_from_interpreter}; use crate::util::FfiResult; use core::ffi::c_int; -use core::ptr; -use rustpython_vm::VirtualMachine; +use core::sync::atomic::Ordering; use rustpython_vm::vm::thread::{ - CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm, + CurrentVmAttachState, SavedThreadState, attach_current_thread, release_current_thread, + restore_current_thread, save_current_thread, with_current_vm, }; +use rustpython_vm::{Interpreter, VirtualMachine}; pub(crate) fn with_vm, O>(f: impl FnOnce(&VirtualMachine) -> R) -> O { with_current_vm(|vm| f(vm).into_output(vm)) @@ -16,9 +17,12 @@ type PyGILState_STATE = c_int; const PYGILSTATE_LOCKED: PyGILState_STATE = 0; const PYGILSTATE_UNLOCKED: PyGILState_STATE = 1; +pub type PyInterpreterState = Interpreter; + #[repr(C)] pub struct PyThreadState { - _interp: *mut core::ffi::c_void, + pub interp: *mut PyInterpreterState, + vm: SavedThreadState, } /// Make sure this thread has a running vm attached. This only creates a new vm if we don't already @@ -44,11 +48,42 @@ pub extern "C" fn PyGILState_Release(state: PyGILState_STATE) { #[unsafe(no_mangle)] pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { - ptr::null_mut() + let interp = PyInterpreterState_Get(); + let state = Box::new(PyThreadState { + interp, + vm: save_current_thread(), + }); + Box::into_raw(state) } #[unsafe(no_mangle)] -pub extern "C" fn PyEval_RestoreThread(_state: *mut PyThreadState) {} +pub unsafe extern "C" fn PyEval_RestoreThread(state: *mut PyThreadState) { + assert!(!state.is_null(), "PyEval_RestoreThread called with null"); + // SAFETY: PyEval_SaveThread returns this allocation and CPython's API + // requires callers to restore exactly that thread state once. + let state = unsafe { Box::from_raw(state) }; + restore_current_thread(state.vm); +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyInterpreterState_Get() -> *mut PyInterpreterState { + let ptr = MAIN_INTERP_PTR.load(Ordering::Acquire); + assert!( + !ptr.is_null(), + "PyInterpreterState_Get() called but the interpreter is not initialized" + ); + ptr +} + +#[unsafe(no_mangle)] +pub extern "C" fn PyInterpreterState_GetID(interp: *mut PyInterpreterState) -> i64 { + with_vm(|vm| { + if interp.is_null() { + return Err(vm.new_system_error("PyInterpreterState_GetID called with null interp")); + } + Ok(interp as usize as i64) + }) +} #[cfg(test)] mod tests { @@ -59,7 +94,7 @@ mod tests { #[test] fn new_thread() { - Python::attach(|_py| { + Python::attach(|py| { with_current_vm(|_vm| { assert!( current_vm_is_set(), @@ -67,18 +102,24 @@ mod tests { ) }); - std::thread::spawn(move || { + let handle = std::thread::spawn(move || { Python::attach(|_py| { - with_current_vm(|_vm| { + with_current_vm(|vm| { assert!( current_vm_is_set(), "This thread did not have a vm attached" - ) + ); + vm.state.stop_the_world.stop_the_world(&vm.state); + vm.state.stop_the_world.start_the_world(&vm.state); }); }); - }) - .join() - .unwrap(); + }); + + py.detach(|| { + assert!(!current_vm_is_set()); + handle.join().unwrap(); + }); + assert!(current_vm_is_set()); }) } diff --git a/crates/capi/src/pystrcmp.rs b/crates/capi/src/pystrcmp.rs new file mode 100644 index 00000000000..8055f97cd5c --- /dev/null +++ b/crates/capi/src/pystrcmp.rs @@ -0,0 +1,28 @@ +use core::ffi::c_char; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyOS_mystricmp(str1: *const c_char, str2: *const c_char) -> i32 { + unsafe { PyOS_mystrnicmp(str1, str2, isize::MAX) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyOS_mystrnicmp( + str1: *const c_char, + str2: *const c_char, + size: isize, +) -> i32 { + let Ok(limit) = usize::try_from(size) else { + return 0; + }; + let mut index = 0usize; + while index < limit { + let left = unsafe { *str1.add(index) } as u8; + let right = unsafe { *str2.add(index) } as u8; + let diff = left.to_ascii_lowercase() as i32 - right.to_ascii_lowercase() as i32; + if diff != 0 || left == 0 || right == 0 { + return diff; + } + index += 1; + } + 0 +} diff --git a/crates/capi/src/refcount.rs b/crates/capi/src/refcount.rs index 917dfeec2b9..48c7132b0f1 100644 --- a/crates/capi/src/refcount.rs +++ b/crates/capi/src/refcount.rs @@ -1,4 +1,4 @@ -use crate::PyObject; +use crate::{PyObject, pystate::with_vm}; use core::ptr::NonNull; use rustpython_vm::PyObjectRef; @@ -13,3 +13,36 @@ pub unsafe extern "C" fn _Py_IncRef(op: *mut PyObject) { // Don't drop the owned value, as we just want to increment the refcount. core::mem::forget(unsafe { (*op).to_owned() }); } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_NewRef(op: *mut PyObject) -> *mut PyObject { + with_vm(|_vm| unsafe { (*op).to_owned() }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_REFCNT(op: *mut PyObject) -> isize { + with_vm(|_vm| unsafe { &*op }.strong_count()) +} + +#[cfg(test)] +mod tests { + use pyo3::ffi; + use pyo3::prelude::*; + use pyo3::types::PyList; + + #[test] + fn refcount() { + Python::attach(|py| unsafe { + // A freshly created, non-empty list is uniquely owned here: its + // reference count is private to this test (so parallel tests cannot + // perturb it) and it is mortal (not interned), so incref then decref + // must move the count by exactly one and back. + let obj = PyList::new(py, [1, 2, 3]).unwrap(); + let ref_count = ffi::Py_REFCNT(obj.as_ptr()); + let obj_clone = obj.clone(); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count + 1); + drop(obj_clone); + assert_eq!(ffi::Py_REFCNT(obj.as_ptr()), ref_count); + }); + } +} diff --git a/crates/capi/src/setobject.rs b/crates/capi/src/setobject.rs index cc479371b27..1036bb1473a 100644 --- a/crates/capi/src/setobject.rs +++ b/crates/capi/src/setobject.rs @@ -116,7 +116,7 @@ pub unsafe extern "C" fn PySet_Size(anyset: *mut PyObject) -> isize { }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PyFrozenSet, PyInt, PySet}; diff --git a/crates/capi/src/sliceobject.rs b/crates/capi/src/sliceobject.rs index 2a625fab523..fb588181531 100644 --- a/crates/capi/src/sliceobject.rs +++ b/crates/capi/src/sliceobject.rs @@ -72,7 +72,7 @@ pub unsafe extern "C" fn PySlice_AdjustIndices( slice_len as isize } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::{PySlice, PySliceMethods}; diff --git a/crates/capi/src/tupleobject.rs b/crates/capi/src/tupleobject.rs index 985141f6d4c..60c4b81b370 100644 --- a/crates/capi/src/tupleobject.rs +++ b/crates/capi/src/tupleobject.rs @@ -90,13 +90,13 @@ pub unsafe extern "C" fn PyTuple_GetSlice( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyTuple; #[test] - fn test_empty_tuple() { + fn empty_tuple() { Python::attach(|py| { let tuple = PyTuple::empty(py); assert_eq!(tuple.len(), 0); @@ -104,7 +104,7 @@ mod tests { } #[test] - fn test_tuple_into_python() { + fn tuple_into_python() { Python::attach(|py| { let tuple = (1, 2, 3).into_pyobject(py).unwrap(); assert_eq!(tuple.len(), 3); @@ -112,7 +112,7 @@ mod tests { } #[test] - fn test_tuple_get_slice() { + fn tuple_get_slice() { Python::attach(|py| { let tuple = (1, 2, 3).into_pyobject(py).unwrap(); let slice = tuple.get_slice(1, 2); diff --git a/crates/capi/src/unicodeobject.rs b/crates/capi/src/unicodeobject.rs index acc6e392c53..00ab1dcb8e2 100644 --- a/crates/capi/src/unicodeobject.rs +++ b/crates/capi/src/unicodeobject.rs @@ -1,11 +1,14 @@ use crate::object::define_py_check; +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; use core::ffi::{CStr, c_char, c_int}; use core::ptr::NonNull; use core::slice; use core::str; -use rustpython_vm::PyObjectRef; -use rustpython_vm::builtins::PyStr; +use rustpython_vm::builtins::{PyBytesRef, PyStr, PyStrRef, PyUtf8StrRef}; +use rustpython_vm::common::wtf8::{CodePoint, Wtf8Buf}; +use rustpython_vm::convert::ToPyObject; +use rustpython_vm::{AsObject, PyObjectRef, PyResult, VirtualMachine}; define_py_check!(fn PyUnicode_Check, types.str_type); define_py_check!(exact fn PyUnicode_CheckExact, types.str_type); @@ -36,6 +39,36 @@ pub unsafe extern "C" fn PyUnicode_FromStringAndSize( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_FromString(s: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let s = unsafe { s.try_as_str(vm)? }; + Ok(vm.ctx.new_str(s)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_FromObject(obj: *mut PyObject) -> *mut PyObject { + with_vm(|vm| { + Ok(unsafe { &*obj } + .try_downcast_ref::(vm)? + .as_object() + .str(vm)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_FromOrdinal(ordinal: c_int) -> *mut PyObject { + with_vm(|vm| { + let ordinal: u32 = ordinal + .try_into() + .map_err(|_| vm.new_value_error("ordinal not in range(0x110000)"))?; + let code_point = CodePoint::from_u32(ordinal) + .ok_or_else(|| vm.new_value_error("ordinal not in range(0x110000)"))?; + Ok(vm.ctx.new_str(Wtf8Buf::from_iter([code_point]))) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyUnicode_AsUTF8AndSize( obj: *mut PyObject, @@ -60,6 +93,52 @@ pub unsafe extern "C" fn PyUnicode_AsUTF8AndSize( }) } +fn encode_unicode( + vm: &VirtualMachine, + unicode: *mut PyObject, + encoding: &str, + errors: Option, +) -> PyResult { + let unicode = unsafe { &*unicode } + .try_downcast_ref::(vm)? + .to_owned(); + vm.state + .codec_registry + .encode_text(unicode, encoding, errors, vm) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsASCIIString(unicode: *mut PyObject) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "ascii", None)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsLatin1String(unicode: *mut PyObject) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "latin-1", None)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsRawUnicodeEscapeString( + unicode: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "raw-unicode-escape", None)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsUTF16String(unicode: *mut PyObject) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "utf-16", None)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsUTF32String(unicode: *mut PyObject) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "utf-32", None)) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_AsUnicodeEscapeString(unicode: *mut PyObject) -> *mut PyObject { + with_vm(|vm| encode_unicode(vm, unicode, "unicode-escape", None)) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyUnicode_AsEncodedString( unicode: *mut PyObject, @@ -67,46 +146,24 @@ pub unsafe extern "C" fn PyUnicode_AsEncodedString( errors: *const c_char, ) -> *mut PyObject { with_vm(|vm| { - let unicode = unsafe { &*unicode } - .try_downcast_ref::(vm)? - .to_owned(); - let encoding = if encoding.is_null() { - "utf-8" - } else { - unsafe { CStr::from_ptr(encoding) } - .to_str() - .expect("encoding must be valid UTF-8") - }; - let errors = if errors.is_null() { - None - } else { - let errors = unsafe { CStr::from_ptr(errors) } - .to_str() - .expect("errors must be valid UTF-8"); - Some(vm.ctx.new_utf8_str(errors)) - }; - vm.state - .codec_registry - .encode_text(unicode, encoding, errors, vm) + let encoding = unsafe { encoding.try_as_str_opt(vm) }?.unwrap_or("utf-8"); + let errors = + unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_utf8_str(errors)); + encode_unicode(vm, unicode, encoding, errors) }) } #[unsafe(no_mangle)] pub unsafe extern "C" fn PyUnicode_AsUTF8String(unicode: *mut PyObject) -> *mut PyObject { - with_vm(|vm| { - let unicode = unsafe { &*unicode } - .try_downcast_ref::(vm)? - .to_owned(); - vm.state - .codec_registry - .encode_text(unicode, "utf-8", None, vm) - }) + with_vm(|vm| encode_unicode(vm, unicode, "utf-8", None)) } #[unsafe(no_mangle)] -pub unsafe extern "C" fn PyUnicode_DecodeFSDefaultAndSize( +pub unsafe extern "C" fn PyUnicode_Decode( s: *const c_char, size: isize, + encoding: *const c_char, + errors: *const c_char, ) -> *mut PyObject { with_vm(|vm| { let size: usize = size @@ -115,35 +172,213 @@ pub unsafe extern "C" fn PyUnicode_DecodeFSDefaultAndSize( 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", - )); + return Err(vm.new_system_error("decode called with null data and non-zero size")); } - &[][..] + Vec::new() } else { - unsafe { slice::from_raw_parts(s.cast::(), size) } + unsafe { slice::from_raw_parts(s.cast::(), size) }.to_vec() }; - 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, - ) + let encoding = unsafe { encoding.try_as_str_opt(vm)?.unwrap_or("utf-8") }; + let errors = + unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_utf8_str(errors)); + + vm.state + .codec_registry + .decode_text(vm.ctx.new_bytes(bytes).into(), encoding, errors, vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeASCII( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"ascii".as_ptr(), errors) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeLatin1( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"latin-1".as_ptr(), errors) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeRawUnicodeEscape( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"raw-unicode-escape".as_ptr(), errors) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeUTF7( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"utf-7".as_ptr(), errors) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeUTF8( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"utf-8".as_ptr(), errors) } +} +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeUnicodeEscape( + s: *const c_char, + size: isize, + errors: *const c_char, +) -> *mut PyObject { + unsafe { PyUnicode_Decode(s, size, c"unicode-escape".as_ptr(), errors) } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_DecodeFSDefaultAndSize( + s: *const c_char, + size: isize, +) -> *mut PyObject { + with_vm(|vm| { + let size: usize = size + .try_into() + .map_err(|_| vm.new_system_error("size must be non-negative"))?; + + decode_fsdefault_and_size(vm, s, size) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Concat( + left: *mut PyObject, + right: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let left = unsafe { &*left }.try_downcast_ref::(vm)?; + let right = unsafe { &*right }.try_downcast_ref::(vm)?; + vm._add(left.as_object(), right.as_object()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_GetLength(unicode: *mut PyObject) -> isize { + with_vm(|vm| { + let unicode = unsafe { &*unicode }.try_downcast_ref::(vm)?; + Ok(unicode.char_len()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_GetDefaultEncoding() -> *const c_char { + c"utf-8".as_ptr() +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_InternFromString(s: *const c_char) -> *mut PyObject { + with_vm(|vm| { + let s = unsafe { s.try_as_str(vm)? }; + Ok(vm.ctx.intern_str(s).to_owned()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Compare(left: *mut PyObject, right: *mut PyObject) -> c_int { + with_vm(|vm| { + let left = unsafe { &*left }.try_downcast_ref::(vm)?; + let right = unsafe { &*right }.try_downcast_ref::(vm)?; + Ok(match left.as_wtf8().cmp(right.as_wtf8()) { + core::cmp::Ordering::Less => -1, + core::cmp::Ordering::Equal => 0, + core::cmp::Ordering::Greater => 1, + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_CompareWithASCIIString( + left: *mut PyObject, + right: *const c_char, +) -> c_int { + with_vm(|vm| { + let left = unsafe { &*left }.try_downcast_ref::(vm)?; + let right = unsafe { right.try_as_str(vm)? }; + Ok(match left.as_wtf8().cmp(right.into()) { + core::cmp::Ordering::Less => -1, + core::cmp::Ordering::Equal => 0, + core::cmp::Ordering::Greater => 1, + }) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Equal(left: *mut PyObject, right: *mut PyObject) -> c_int { + with_vm(|vm| { + let left = unsafe { &*left }.try_downcast_ref::(vm)?; + let right = unsafe { &*right }.try_downcast_ref::(vm)?; + Ok(left.as_wtf8() == right.as_wtf8()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_EqualToUTF8( + unicode: *mut PyObject, + string: *const c_char, +) -> c_int { + with_vm(|vm| { + let unicode = unsafe { &*unicode }.try_downcast_ref::(vm)?; + let other = unsafe { string.try_as_str(vm)? }; + Ok(unicode.to_str().is_some_and(|s| s == other)) + }) +} + +#[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 { + 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::(), size) } + }; + + vm.state.codec_registry.decode_text( + vm.ctx.new_bytes(bytes.to_vec()).into(), + vm.fs_encoding().as_str(), + Some(vm.fs_encode_errors().to_owned()), + vm, + ) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyUnicode_EncodeFSDefault(unicode: *mut PyObject) -> *mut PyObject { with_vm(|vm| { - let unicode = unsafe { &*unicode } - .try_downcast_ref::(vm)? - .to_owned(); - vm.state.codec_registry.encode_text( + encode_unicode( + vm, unicode, vm.fs_encoding().as_str(), Some(vm.fs_encode_errors().to_owned()), - vm, ) }) } @@ -161,21 +396,9 @@ pub unsafe extern "C" fn PyUnicode_FromEncodedObject( return Err(vm.new_type_error("decoding str is not supported")); } - let encoding = if encoding.is_null() { - "utf-8" - } else { - unsafe { CStr::from_ptr(encoding) } - .to_str() - .map_err(|_| vm.new_system_error("encoding must be valid UTF-8"))? - }; - let errors = if errors.is_null() { - None - } else { - let errors = unsafe { CStr::from_ptr(errors) } - .to_str() - .map_err(|_| vm.new_system_error("errors must be valid UTF-8"))?; - Some(vm.ctx.new_utf8_str(errors)) - }; + let encoding = unsafe { encoding.try_as_str_opt(vm) }?.unwrap_or("utf-8"); + let errors = + unsafe { errors.try_as_str_opt(vm) }?.map(|errors| vm.ctx.new_utf8_str(errors)); obj.try_bytes_like(vm, |b| { vm.state.codec_registry.decode_text( @@ -188,6 +411,76 @@ pub unsafe extern "C" fn PyUnicode_FromEncodedObject( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Contains( + container: *mut PyObject, + element: *mut PyObject, +) -> c_int { + with_vm(|vm| { + let container = unsafe { &*container }.try_downcast_ref::(vm)?; + let element = unsafe { &*element }.try_downcast_ref::(vm)?; + Ok(container.as_wtf8().contains(element.as_wtf8())) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Format( + format: *mut PyObject, + args: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let format = unsafe { &*format }.try_downcast_ref::(vm)?; + let result = format.__mod__(unsafe { &*args }.to_owned(), vm)?; + Ok(result.to_pyobject(vm)) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_IsIdentifier(s: *mut PyObject) -> c_int { + with_vm(|vm| { + let s = unsafe { &*s }.try_downcast_ref::(vm)?; + Ok(s.isidentifier()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Partition( + s: *mut PyObject, + sep: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let s = unsafe { &*s }.try_downcast_ref::(vm)?; + let sep = unsafe { &*sep }.try_downcast_ref::(vm)?; + s.partition(sep.to_owned(), vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_RPartition( + s: *mut PyObject, + sep: *mut PyObject, +) -> *mut PyObject { + with_vm(|vm| { + let s = unsafe { &*s }.try_downcast_ref::(vm)?; + let sep = unsafe { &*sep }.try_downcast_ref::(vm)?; + s.rpartition(sep.to_owned(), vm) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn PyUnicode_Translate( + str_obj: *mut PyObject, + table: *mut PyObject, + _errors: *const c_char, +) -> *mut PyObject { + with_vm(|vm| { + let str_obj = unsafe { &*str_obj }.try_downcast_ref::(vm)?; + Ok(str_obj + .translate(unsafe { &*table }.to_owned(), vm)? + .to_pyobject(vm)) + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn PyUnicode_InternInPlace(string: *mut *mut PyObject) { with_vm(|vm| { @@ -225,7 +518,7 @@ pub unsafe extern "C" fn PyUnicode_EqualToUTF8AndSize( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use std::ffi::{OsStr, OsString}; diff --git a/crates/capi/src/util.rs b/crates/capi/src/util.rs index 24d6d3d40e9..32ff775676b 100644 --- a/crates/capi/src/util.rs +++ b/crates/capi/src/util.rs @@ -1,7 +1,8 @@ use crate::PyObject; use core::convert::Infallible; -use core::ffi::{c_char, c_double, c_int, c_long, c_ulonglong, c_void}; -use rustpython_vm::{PyObjectRef, PyRef, PyResult, VirtualMachine}; +use core::ffi::{CStr, c_char, c_double, c_int, c_long, c_ulong, c_void}; +use core::ptr::NonNull; +use rustpython_vm::{Py, PyObjectRef, PyRef, PyResult, VirtualMachine}; pub(crate) trait FfiResult { const ERR_VALUE: Output; @@ -36,6 +37,17 @@ where } } +impl FfiResult<*mut Py> for PyRef +where + Self: Into, +{ + const ERR_VALUE: *mut Py = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *mut Py { + self.into().into_raw().as_ptr().cast() + } +} + impl FfiResult<*mut PyObject> for PyObjectRef { const ERR_VALUE: *mut PyObject = core::ptr::null_mut(); @@ -92,6 +104,14 @@ impl FfiResult for *const c_char { } } +impl FfiResult<*const c_char> for &CStr { + const ERR_VALUE: *const c_char = core::ptr::null_mut(); + + fn into_output(self, _vm: &VirtualMachine) -> *const c_char { + self.as_ptr() + } +} + impl FfiResult for usize { const ERR_VALUE: isize = -1; @@ -101,6 +121,31 @@ impl FfiResult for usize { } } +impl FfiResult for isize { + const ERR_VALUE: Self = -1; + + fn into_output(self, _vm: &VirtualMachine) -> Self { + self + } +} + +#[cfg(not(windows))] +impl FfiResult for c_int { + const ERR_VALUE: Self = -1; + + fn into_output(self, _vm: &VirtualMachine) -> Self { + self + } +} + +impl FfiResult for usize { + const ERR_VALUE: Self = Self::MAX; + + fn into_output(self, _vm: &VirtualMachine) -> Self { + self + } +} + impl FfiResult for c_long { const ERR_VALUE: Self = -1; @@ -109,7 +154,25 @@ impl FfiResult for c_long { } } -impl FfiResult for c_ulonglong { +impl FfiResult for c_ulong { + const ERR_VALUE: Self = Self::MAX; + + fn into_output(self, _vm: &VirtualMachine) -> Self { + self + } +} + +#[cfg(windows)] +impl FfiResult for core::ffi::c_longlong { + const ERR_VALUE: Self = -1; + + fn into_output(self, _vm: &VirtualMachine) -> Self { + self + } +} + +#[cfg(windows)] +impl FfiResult for core::ffi::c_ulonglong { const ERR_VALUE: Self = Self::MAX; fn into_output(self, _vm: &VirtualMachine) -> Self { @@ -159,3 +222,68 @@ where ) } } + +pub(crate) trait CStrExt<'a> { + unsafe fn try_as_str(self, vm: &VirtualMachine) -> PyResult<&'a str>; + unsafe fn try_as_str_opt(self, vm: &VirtualMachine) -> PyResult>; +} + +impl<'a> CStrExt<'a> for *mut c_char { + unsafe fn try_as_str(self, vm: &VirtualMachine) -> PyResult<&'a str> { + unsafe { self.try_as_str_opt(vm) }? + .ok_or_else(|| vm.new_system_error("argument must not be null")) + } + + unsafe fn try_as_str_opt(self, vm: &VirtualMachine) -> PyResult> { + NonNull::new(self) + .map(|ptr| unsafe { CStr::from_ptr(ptr.as_ptr()) }.to_str()) + .transpose() + .map_err(|_| vm.new_system_error("argument must be valid UTF-8")) + } +} + +impl<'a> CStrExt<'a> for *const c_char { + unsafe fn try_as_str(self, vm: &VirtualMachine) -> PyResult<&'a str> { + unsafe { self.cast_mut().try_as_str(vm) } + } + + unsafe fn try_as_str_opt(self, vm: &VirtualMachine) -> PyResult> { + unsafe { self.cast_mut().try_as_str_opt(vm) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::any::type_name; + use core::ffi::{c_longlong, c_ulonglong}; + use core::fmt::Debug; + + #[test] + fn ffi_result_err_value() { + fn assert_error_value(value: Output) + where + T: FfiResult + 'static, + Output: PartialEq + Debug, + { + assert_eq!(value, T::ERR_VALUE, "{}", type_name::(),); + } + + assert_error_value::<(), _>(()); + assert_error_value::<(), c_int>(-1); + + assert_error_value::(-1); + assert_error_value::(usize::MAX); + assert_error_value::(-1); + assert_error_value::(-1); // i32 + assert_error_value::(-1); //Windows i32, unix i64 + assert_error_value::(c_ulong::MAX); // Windows u32, unix u64 + assert_error_value::(-1); // i64 + assert_error_value::(c_ulonglong::MAX); // u64 + assert_error_value::(-1.0); + assert_error_value::(-1); + + assert_error_value::, _>(-1); + assert_error_value::, _>(usize::MAX); + } +} diff --git a/crates/capi/src/warnings.rs b/crates/capi/src/warnings.rs index 22ffd6ab939..f9ed82b9fa9 100644 --- a/crates/capi/src/warnings.rs +++ b/crates/capi/src/warnings.rs @@ -1,5 +1,6 @@ +use crate::util::CStrExt; use crate::{PyObject, pystate::with_vm}; -use core::ffi::{CStr, c_char, c_int}; +use core::ffi::{c_char, c_int}; use rustpython_vm::builtins::{PyType, PyTypeRef}; use rustpython_vm::warn::{warn, warn_explicit}; use rustpython_vm::{AsObject, PyResult}; @@ -32,9 +33,7 @@ pub unsafe extern "C" fn PyErr_WarnEx( stack_level: isize, ) -> c_int { with_vm(|vm| { - let message = unsafe { CStr::from_ptr(message) } - .to_str() - .map_err(|_| vm.new_system_error("warning message is not valid UTF-8"))?; + let message = unsafe { message.try_as_str(vm) }?; let category = resolve_warning_category(vm, category)?; @@ -58,17 +57,11 @@ pub unsafe extern "C" fn PyErr_WarnExplicit( registry: *mut PyObject, ) -> c_int { with_vm(|vm| { - let message = unsafe { CStr::from_ptr(message) } - .to_str() - .map_err(|_| vm.new_system_error("warning message is not valid UTF-8"))?; - let filename = unsafe { CStr::from_ptr(filename) } - .to_str() - .map_err(|_| vm.new_system_error("filename is not valid UTF-8"))?; - - let module = unsafe { module.as_ref().map(|ptr| CStr::from_ptr(ptr).to_str()) } - .transpose() - .map_err(|_| vm.new_system_error("module is not valid UTF-8"))? - .map(|module| vm.ctx.new_str(module).into()); + let message = unsafe { message.try_as_str(vm) }?; + let filename = unsafe { filename.try_as_str(vm) }?; + + let module = + unsafe { module.try_as_str_opt(vm) }?.map(|module| vm.ctx.new_str(module).into()); let category = resolve_warning_category(vm, category)?; @@ -92,7 +85,7 @@ pub unsafe extern "C" fn PyErr_WarnExplicit( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::exceptions::{PyRuntimeWarning, PyUserWarning}; use pyo3::prelude::*; diff --git a/crates/capi/src/weakrefobject.rs b/crates/capi/src/weakrefobject.rs index 706de84b53d..ff4a8bcff13 100644 --- a/crates/capi/src/weakrefobject.rs +++ b/crates/capi/src/weakrefobject.rs @@ -20,8 +20,6 @@ pub unsafe extern "C" fn PyWeakref_GetRef( let reference = unsafe { &*reference }; let upgraded = if let Some(weak) = reference.downcast_ref::() { weak.upgrade() - } else if let Some(proxy) = reference.downcast_ref::() { - proxy.get_weak().upgrade() } else { return Err(vm.new_type_error("expected a weakref")); }; @@ -65,7 +63,7 @@ pub unsafe extern "C" fn PyWeakref_NewRef( }) } -#[cfg(false)] +#[cfg(test)] mod tests { use pyo3::prelude::*; use pyo3::types::PyAnyMethods; diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index 031f3b96521..c43bf1bcb08 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -15,6 +15,7 @@ std = ["thiserror/std", "itertools/use_std"] [dependencies] rustpython-compiler-core = { workspace = true } rustpython-literal = {workspace = true } +rustpython-unicode = { workspace = true } rustpython-wtf8 = { workspace = true } ruff_python_ast = { workspace = true } ruff_text_size = { workspace = true } @@ -29,7 +30,6 @@ thiserror = { workspace = true } malachite-bigint = { workspace = true } memchr = { workspace = true } rapidhash = { workspace = true } -unicode_names2 = { workspace = true } [dev-dependencies] ruff_python_parser = { workspace = true } diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 476aa35e3ef..fe0a983187a 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -10,15 +10,15 @@ #![deny(clippy::cast_possible_truncation)] use crate::{ - IndexMap, IndexSet, ToPythonName, - error::{CodegenError, CodegenErrorType, InternalError, PatternUnreachableReason}, - ir::{self, BlockIdx}, + IndexMap, IndexSet, ToPythonName, ast_constant_value_to_constant_data, + error::{CodegenError, CodegenErrorType, InternalError}, + ir::{self, Block, BlockIdx, Blocks}, preprocess, symboltable::{self, CompilerScope, Symbol, SymbolFlags, SymbolScope, SymbolTable}, unparse::UnparseExpr, }; use alloc::borrow::Cow; -use core::mem; +use core::{mem, slice}; use malachite_bigint::BigInt; use num_complex::Complex; use num_traits::{Num, ToPrimitive, Zero}; @@ -33,18 +33,17 @@ use rustpython_compiler_core::{ SpecialMethod, UnpackExArgs, oparg, }, }; +use rustpython_literal::{ + complex as literal_complex, + escape::{AsciiEscape, UnicodeEscape}, + float as literal_float, +}; use rustpython_wtf8::Wtf8Buf; /// Extension trait for `ast::Expr` to add constant checking methods trait ExprExt { - /// Check if an expression is a constant literal + /// Returns true if the expression is a constant literal with no side effects. fn is_constant(&self) -> bool; - - /// Check if a slice expression has all constant elements - fn is_constant_slice(&self) -> bool; - - /// Check if we should use BINARY_SLICE/STORE_SLICE optimization - fn should_use_slice_optimization(&self) -> bool; } impl ExprExt for ast::Expr { @@ -54,33 +53,15 @@ impl ExprExt for ast::Expr { Self::NumberLiteral(_) | Self::StringLiteral(_) | Self::BytesLiteral(_) + | Self::Constant(_) | Self::NoneLiteral(_) | Self::BooleanLiteral(_) | Self::EllipsisLiteral(_) ) } - - fn is_constant_slice(&self) -> bool { - match self { - Self::Slice(s) => { - let lower_const = - s.lower.is_none() || s.lower.as_deref().is_some_and(|e| e.is_constant()); - let upper_const = - s.upper.is_none() || s.upper.as_deref().is_some_and(|e| e.is_constant()); - let step_const = - s.step.is_none() || s.step.as_deref().is_some_and(|e| e.is_constant()); - lower_const && upper_const && step_const - } - _ => false, - } - } - - fn should_use_slice_optimization(&self) -> bool { - !self.is_constant_slice() && matches!(self, Self::Slice(s) if s.step.is_none()) - } } -const MAXBLOCKS: usize = 20; +const CO_MAXBLOCKS: usize = 21; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FBlockType { @@ -143,6 +124,44 @@ pub struct FBlockInfo { pub(crate) type InternalResult = Result; type CompileResult = Result; +pub type SyntaxWarningHandler<'a> = + dyn FnMut(SourceLocation, String) -> Result<(), CodegenError> + 'a; + +fn warn_ast_preprocess_syntax( + source_file: &SourceFile, + handler: &mut SyntaxWarningHandler<'_>, + range: TextRange, + message: String, +) -> CompileResult<()> { + let location = source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8); + handler(location, message) +} + +fn checked_future_features( + ast: &ruff_python_ast::Mod, + source_file: &SourceFile, +) -> CompileResult { + preprocess::checked_future_features(ast).map_err(|err| { + let location = source_file + .to_source_code() + .source_location(err.range.start(), PositionEncoding::Utf8); + let error = match err.kind { + preprocess::FutureFeatureErrorKind::InvalidFeature(feature) => { + CodegenErrorType::InvalidFutureFeature(feature) + } + preprocess::FutureFeatureErrorKind::InvalidBraces => { + CodegenErrorType::InvalidFutureBraces + } + }; + CodegenError { + location: Some(location), + error, + source_path: source_file.name().to_owned(), + } + }) +} #[derive(PartialEq, Eq, Clone, Copy)] enum NameUsage { @@ -151,13 +170,14 @@ enum NameUsage { Delete, } /// Main structure holding the state of compilation. -struct Compiler { +struct Compiler<'a> { code_stack: Vec, symbol_table_stack: Vec, source_file: SourceFile, // current_source_location: SourceLocation, current_source_range: TextRange, done_with_future_stmts: DoneWithFuture, + future_features: bytecode::CodeFlags, future_annotations: bool, ctx: CompileContext, opts: CompileOpts, @@ -169,9 +189,9 @@ struct Compiler { /// When > 0, the compiler walks AST (consuming sub_tables) but emits no bytecode. /// Mirrors CPython's `c_do_not_emit_bytecode`. do_not_emit_bytecode: u32, - /// Disable constant tuple/list/set collection folding in contexts where - /// CPython keeps the builder form for later assignment lowering. - disable_const_collection_folding: bool, + /// Mirrors `c_disable_warning` while compiling FINALLY_END copies. + disable_warning: u32, + syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, } #[derive(Clone, Copy)] @@ -181,13 +201,120 @@ enum DoneWithFuture { Yes, } -#[derive(Clone, Copy, Debug)] +/// A Python `__future__` feature flag imported via `from __future__ import `. +/// +/// # See Also +/// +/// - [Python documentation on `__future__`](https://docs.python.org/3.14/library/__future__.html) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FutureFeature { + /// ```py + /// from __future__ import absolute_import + /// ``` + AbsoluteImport, + + /// ```py + /// from __future__ import annotations + /// ``` + Annotations, + + /// ```py + /// from __future__ import barry_as_FLUFL + /// ``` + BarryAsFLUFL, + + /// ```py + /// from __future__ import braces + /// ``` + Braces, + + /// ```py + /// from __future__ import division + /// ``` + Division, + + /// ```py + /// from __future__ import generator_stop + /// ``` + GeneratorStop, + + /// ```py + /// from __future__ import generators + /// ``` + Generators, + + /// ```py + /// from __future__ import nested_scopes + /// ``` + NestedScopes, + + /// ```py + /// from __future__ import print_function + /// ``` + PrintFunction, + + /// ```py + /// from __future__ import unicode_literals + /// ``` + UnicodeLiterals, + + /// ```py + /// from __future__ import with_statement + /// ``` + WithStatement, +} + +impl TryFrom<&str> for FutureFeature { + type Error = String; + + fn try_from(name: &str) -> Result { + Ok(match name { + "absolute_import" => Self::AbsoluteImport, + "annotations" => Self::Annotations, + "barry_as_FLUFL" => Self::BarryAsFLUFL, + "braces" => Self::Braces, + "division" => Self::Division, + "generator_stop" => Self::GeneratorStop, + "generators" => Self::Generators, + "nested_scopes" => Self::NestedScopes, + "print_function" => Self::PrintFunction, + "unicode_literals" => Self::UnicodeLiterals, + "with_statement" => Self::WithStatement, + _ => return Err(name.into()), + }) + } +} + +#[derive(Clone, Copy)] +enum ComprehensionSymbolSource { + Child, + Inlined, +} + +#[derive(Clone, Copy)] +struct SymbolTableCursors { + sub_table: usize, + hidden_annotation_block: usize, + inlined_comprehension_block: usize, +} + +#[derive(Clone, Debug)] pub struct CompileOpts { /// How optimized the bytecode output should be; any optimize > 0 does /// not emit assert statements pub optimize: u8, /// Include column info in bytecode (-X no_debug_ranges disables) pub debug_ranges: bool, + /// Maximum decimal integer literal digits, matching sys.int_info/default. + pub int_max_str_digits: usize, + /// Allow module-level await/async-for/async-with, matching PyCF_ALLOW_TOP_LEVEL_AWAIT. + pub allow_top_level_await: bool, + /// Future compiler flags passed explicitly to compile(), matching cf_flags merge. + pub future_features: bytecode::CodeFlags, + /// Keep single-input blocks incomplete until a terminating newline is seen. + pub dont_imply_dedent: bool, + /// Recursion limit used by compiler tree walks, matching Py_EnterRecursiveCall. + pub recursion_limit: usize, } impl Default for CompileOpts { @@ -195,6 +322,11 @@ impl Default for CompileOpts { Self { optimize: 0, debug_ranges: true, + int_max_str_digits: 4300, + allow_top_level_await: false, + future_features: bytecode::CodeFlags::empty(), + dont_imply_dedent: false, + recursion_limit: 1000, } } } @@ -259,19 +391,69 @@ fn validate_duplicate_params(params: &ast::Parameters) -> Result<(), CodegenErro /// Compile an Mod produced from ruff parser pub fn compile_top( - mut ast: ruff_python_ast::Mod, + ast: ruff_python_ast::Mod, source_file: SourceFile, mode: Mode, opts: CompileOpts, ) -> CompileResult { - preprocess::preprocess_mod(&mut ast); + compile_top_with_syntax_warning_handler(ast, source_file, mode, opts, None) +} + +pub fn compile_top_with_syntax_warning_handler<'a>( + mut ast: ruff_python_ast::Mod, + source_file: SourceFile, + mode: Mode, + mut opts: CompileOpts, + mut syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, +) -> CompileResult { + opts.future_features |= checked_future_features(&ast, &source_file)?; + let future_annotations = opts + .future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + if let Some(handler) = syntax_warning_handler.as_deref_mut() { + preprocess::warn_control_flow_in_finally(&ast, |range, message| { + warn_ast_preprocess_syntax(&source_file, handler, range, message) + })?; + } + if matches!(mode, Mode::Single) + && let ruff_python_ast::Mod::Module(module) = &mut ast + { + preprocess::preprocess_statements( + &mut module.body, + opts.optimize, + future_annotations, + false, + ); + } else { + preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false); + } match ast { ruff_python_ast::Mod::Module(module) => match mode { - Mode::Exec | Mode::Eval => compile_program(&module, source_file, opts), - Mode::Single => compile_program_single(&module, source_file, opts), - Mode::BlockExpr => compile_block_expression(&module, source_file, opts), + Mode::Exec | Mode::Eval => compile_program_with_syntax_warning_handler( + &module, + source_file, + opts, + syntax_warning_handler, + ), + Mode::Single => compile_program_single_with_syntax_warning_handler( + &module, + source_file, + opts, + syntax_warning_handler, + ), + Mode::BlockExpr => compile_block_expression_with_syntax_warning_handler( + &module, + source_file, + opts, + syntax_warning_handler, + ), }, - ruff_python_ast::Mod::Expression(expr) => compile_expression(&expr, source_file, opts), + ruff_python_ast::Mod::Expression(expr) => compile_expression_with_syntax_warning_handler( + &expr, + source_file, + opts, + syntax_warning_handler, + ), } } @@ -281,9 +463,54 @@ pub fn compile_program( source_file: SourceFile, opts: CompileOpts, ) -> CompileResult { - let symbol_table = SymbolTable::scan_program(ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned()))?; - let mut compiler = Compiler::new(opts, source_file, ""); + compile_program_with_syntax_warning_handler(ast, source_file, opts, None) +} + +fn scan_module_symbols( + ast: &ast::ModModule, + source_file: &SourceFile, + opts: &CompileOpts, +) -> CompileResult { + SymbolTable::scan_program_with_options( + ast, + source_file.clone(), + opts.allow_top_level_await, + opts.future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS), + opts.recursion_limit, + ) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) +} + +fn scan_expr_symbols( + ast: &ast::ModExpression, + source_file: &SourceFile, + opts: &CompileOpts, +) -> CompileResult { + SymbolTable::scan_expr_with_options( + ast, + source_file.clone(), + opts.allow_top_level_await, + opts.future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS), + opts.recursion_limit, + ) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) +} + +fn compile_program_with_syntax_warning_handler<'a>( + ast: &ast::ModModule, + source_file: SourceFile, + opts: CompileOpts, + syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, +) -> CompileResult { + let symbol_table = scan_module_symbols(ast, &source_file, &opts)?; + let mut compiler = Compiler::new_with_syntax_warning_handler( + opts, + source_file, + "", + syntax_warning_handler, + ); compiler.compile_program(ast, symbol_table)?; let code = compiler.exit_scope(); trace!("Compilation completed: {code:?}"); @@ -296,9 +523,22 @@ pub fn compile_program_single( source_file: SourceFile, opts: CompileOpts, ) -> CompileResult { - let symbol_table = SymbolTable::scan_program(ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned()))?; - let mut compiler = Compiler::new(opts, source_file, ""); + compile_program_single_with_syntax_warning_handler(ast, source_file, opts, None) +} + +fn compile_program_single_with_syntax_warning_handler<'a>( + ast: &ast::ModModule, + source_file: SourceFile, + opts: CompileOpts, + syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, +) -> CompileResult { + let symbol_table = scan_module_symbols(ast, &source_file, &opts)?; + let mut compiler = Compiler::new_with_syntax_warning_handler( + opts, + source_file, + "", + syntax_warning_handler, + ); compiler.compile_program_single(&ast.body, symbol_table)?; let code = compiler.exit_scope(); trace!("Compilation completed: {code:?}"); @@ -310,9 +550,22 @@ pub fn compile_block_expression( source_file: SourceFile, opts: CompileOpts, ) -> CompileResult { - let symbol_table = SymbolTable::scan_program(ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned()))?; - let mut compiler = Compiler::new(opts, source_file, ""); + compile_block_expression_with_syntax_warning_handler(ast, source_file, opts, None) +} + +fn compile_block_expression_with_syntax_warning_handler<'a>( + ast: &ast::ModModule, + source_file: SourceFile, + opts: CompileOpts, + syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, +) -> CompileResult { + let symbol_table = scan_module_symbols(ast, &source_file, &opts)?; + let mut compiler = Compiler::new_with_syntax_warning_handler( + opts, + source_file, + "", + syntax_warning_handler, + ); compiler.compile_block_expr(&ast.body, symbol_table)?; let code = compiler.exit_scope(); trace!("Compilation completed: {code:?}"); @@ -324,9 +577,22 @@ pub fn compile_expression( source_file: SourceFile, opts: CompileOpts, ) -> CompileResult { - let symbol_table = SymbolTable::scan_expr(ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned()))?; - let mut compiler = Compiler::new(opts, source_file, ""); + compile_expression_with_syntax_warning_handler(ast, source_file, opts, None) +} + +fn compile_expression_with_syntax_warning_handler<'a>( + ast: &ast::ModExpression, + source_file: SourceFile, + opts: CompileOpts, + syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>, +) -> CompileResult { + let symbol_table = scan_expr_symbols(ast, &source_file, &opts)?; + let mut compiler = Compiler::new_with_syntax_warning_handler( + opts, + source_file, + "", + syntax_warning_handler, + ); compiler.compile_eval(ast, symbol_table)?; let code = compiler.exit_scope(); Ok(code) @@ -354,7 +620,7 @@ macro_rules! emit { }; } -fn eprint_location(zelf: &Compiler) { +fn eprint_location(zelf: &Compiler<'_>) { let start = zelf .source_file .to_source_code() @@ -375,7 +641,7 @@ fn eprint_location(zelf: &Compiler) { /// Better traceback for internal error #[track_caller] -fn unwrap_internal(zelf: &Compiler, r: InternalResult) -> T { +fn unwrap_internal(zelf: &Compiler<'_>, r: InternalResult) -> T { if let Err(ref r_err) = r { eprintln!("=== CODEGEN PANIC INFO ==="); eprintln!("This IS an internal error: {r_err}"); @@ -385,7 +651,7 @@ fn unwrap_internal(zelf: &Compiler, r: InternalResult) -> T { r.unwrap() } -fn compiler_unwrap_option(zelf: &Compiler, o: Option) -> T { +fn compiler_unwrap_option(zelf: &Compiler<'_>, o: Option) -> T { if o.is_none() { eprintln!("=== CODEGEN PANIC INFO ==="); eprintln!("This IS an internal error, an option was unwrapped during codegen"); @@ -455,9 +721,58 @@ enum CollectionType { Set, } +#[derive(Clone, Copy, Eq, PartialEq)] +enum InferredType { + Tuple, + List, + Dict, + Set, + FrozenSet, + Generator, + Function, + Template, + Str, + Bytes, + Int, + Float, + Complex, + Bool, + NoneType, + Ellipsis, + Slice, +} + +impl InferredType { + const fn name(self) -> &'static str { + match self { + Self::Tuple => "tuple", + Self::List => "list", + Self::Dict => "dict", + Self::Set => "set", + Self::FrozenSet => "frozenset", + Self::Generator => "generator", + Self::Function => "function", + Self::Template => "string.templatelib.Template", + Self::Str => "str", + Self::Bytes => "bytes", + Self::Int => "int", + Self::Float => "float", + Self::Complex => "complex", + Self::Bool => "bool", + Self::NoneType => "NoneType", + Self::Ellipsis => "ellipsis", + Self::Slice => "slice", + } + } + + const fn is_long_subclass(self) -> bool { + matches!(self, Self::Int | Self::Bool) + } +} + const STACK_USE_GUIDELINE: u32 = 30; -impl Compiler { +impl<'warnings> Compiler<'warnings> { fn constant_truthiness(constant: &ConstantData) -> bool { match constant { ConstantData::Tuple { elements } | ConstantData::Frozenset { elements } => { @@ -474,7 +789,286 @@ impl Compiler { } } - fn new(opts: CompileOpts, source_file: SourceFile, code_name: &str) -> Self { + fn infer_type_constant(constant: &ConstantData) -> Option { + match constant { + ConstantData::Tuple { .. } => Some(InferredType::Tuple), + ConstantData::Frozenset { .. } => Some(InferredType::FrozenSet), + ConstantData::Integer { .. } => Some(InferredType::Int), + ConstantData::Float { .. } => Some(InferredType::Float), + ConstantData::Complex { .. } => Some(InferredType::Complex), + ConstantData::Boolean { .. } => Some(InferredType::Bool), + ConstantData::Str { .. } => Some(InferredType::Str), + ConstantData::Bytes { .. } => Some(InferredType::Bytes), + ConstantData::None => Some(InferredType::NoneType), + ConstantData::Ellipsis => Some(InferredType::Ellipsis), + ConstantData::Slice { .. } => Some(InferredType::Slice), + ConstantData::Code { .. } => None, + } + } + + fn infer_type(&self, expr: &ast::Expr) -> Option { + if let Some(constant) = self.ast_constant_value(expr) { + return Self::infer_type_constant(&constant); + } + match expr { + ast::Expr::Tuple(_) => Some(InferredType::Tuple), + ast::Expr::List(_) | ast::Expr::ListComp(_) => Some(InferredType::List), + ast::Expr::Dict(_) | ast::Expr::DictComp(_) => Some(InferredType::Dict), + ast::Expr::Set(_) | ast::Expr::SetComp(_) => Some(InferredType::Set), + ast::Expr::Generator(_) => Some(InferredType::Generator), + ast::Expr::Lambda(_) => Some(InferredType::Function), + ast::Expr::TString(_) => Some(InferredType::Template), + ast::Expr::FString(_) | ast::Expr::StringLiteral(_) => Some(InferredType::Str), + ast::Expr::BytesLiteral(_) => Some(InferredType::Bytes), + ast::Expr::NumberLiteral(number) => match number.value { + ast::Number::Int(_) => Some(InferredType::Int), + ast::Number::Float(_) => Some(InferredType::Float), + ast::Number::Complex { .. } => Some(InferredType::Complex), + }, + ast::Expr::BooleanLiteral(_) => Some(InferredType::Bool), + ast::Expr::NoneLiteral(_) => Some(InferredType::NoneType), + ast::Expr::EllipsisLiteral(_) => Some(InferredType::Ellipsis), + ast::Expr::Slice(_) => Some(InferredType::Slice), + _ => None, + } + } + + fn is_constant_expr(&self, expr: &ast::Expr) -> bool { + if self.ast_constant_value(expr).is_some() { + return true; + } + matches!( + expr, + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::NumberLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_) + | ast::Expr::EllipsisLiteral(_) + ) + } + + fn is_constant_slice(&self, slice: &ast::Expr) -> bool { + match slice { + ast::Expr::Slice(s) => { + let lower_const = s.lower.is_none() + || s.lower.as_deref().is_some_and(|e| self.is_constant_expr(e)); + let upper_const = s.upper.is_none() + || s.upper.as_deref().is_some_and(|e| self.is_constant_expr(e)); + let step_const = + s.step.is_none() || s.step.as_deref().is_some_and(|e| self.is_constant_expr(e)); + lower_const && upper_const && step_const + } + _ => false, + } + } + + fn should_apply_two_element_slice_optimization(&self, slice: &ast::Expr) -> bool { + !self.is_constant_slice(slice) && matches!(slice, ast::Expr::Slice(s) if s.step.is_none()) + } + + fn check_is_arg(&self, expr: &ast::Expr) -> bool { + if let Some(constant) = self.ast_constant_value(expr) { + return matches!( + constant, + ConstantData::None | ConstantData::Boolean { .. } | ConstantData::Ellipsis + ); + } + if let ast::Expr::Tuple(tuple) = expr { + return !tuple.elts.iter().all(|expr| self.is_constant_expr(expr)); + } + if !self.is_constant_expr(expr) { + return true; + } + matches!( + expr, + ast::Expr::NoneLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::EllipsisLiteral(_) + ) + } + + fn warn_syntax(&mut self, range: TextRange, message: String) -> CompileResult<()> { + if self.disable_warning > 0 { + return Ok(()); + } + let Some(handler) = self.syntax_warning_handler.as_deref_mut() else { + return Ok(()); + }; + let location = self + .source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8); + handler(location, message) + } + + fn check_caller(&mut self, func: &ast::Expr) -> CompileResult<()> { + let warns = self.ast_constant_value(func).is_some() + || matches!( + func, + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::NumberLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_) + | ast::Expr::EllipsisLiteral(_) + | ast::Expr::Tuple(_) + | ast::Expr::List(_) + | ast::Expr::ListComp(_) + | ast::Expr::Dict(_) + | ast::Expr::DictComp(_) + | ast::Expr::Set(_) + | ast::Expr::SetComp(_) + | ast::Expr::Generator(_) + | ast::Expr::FString(_) + | ast::Expr::TString(_) + ); + if warns && let Some(inferred) = self.infer_type(func) { + self.warn_syntax( + func.range(), + format!( + "'{}' object is not callable; perhaps you missed a comma?", + inferred.name() + ), + )?; + } + Ok(()) + } + + fn check_compare( + &mut self, + range: TextRange, + left: &ast::Expr, + ops: &[ast::CmpOp], + comparators: &[ast::Expr], + ) -> CompileResult<()> { + let mut left_is_arg = self.check_is_arg(left); + let mut left_expr = left; + for (op, right_expr) in ops.iter().zip(comparators.iter()) { + let right_is_arg = self.check_is_arg(right_expr); + if matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) && (!right_is_arg || !left_is_arg) { + let literal = if !left_is_arg { left_expr } else { right_expr }; + if let Some(inferred) = self.infer_type(literal) { + let is_op = matches!(op, ast::CmpOp::Is); + let op = if is_op { "\"is\"" } else { "\"is not\"" }; + let replacement = if is_op { "==" } else { "!=" }; + self.warn_syntax( + range, + format!( + "{op} with '{}' literal. Did you mean \"{replacement}\"?", + inferred.name() + ), + )?; + return Ok(()); + } + } + left_is_arg = right_is_arg; + left_expr = right_expr; + } + Ok(()) + } + + fn constant_warns_as_subscripter(constant: &ConstantData) -> bool { + matches!( + constant, + ConstantData::None + | ConstantData::Ellipsis + | ConstantData::Integer { .. } + | ConstantData::Float { .. } + | ConstantData::Complex { .. } + | ConstantData::Boolean { .. } + | ConstantData::Frozenset { .. } + ) + } + + fn check_subscripter(&mut self, value: &ast::Expr) -> CompileResult<()> { + let warns = self + .ast_constant_value(value) + .is_some_and(|constant| Self::constant_warns_as_subscripter(&constant)) + || matches!( + value, + ast::Expr::NoneLiteral(_) + | ast::Expr::EllipsisLiteral(_) + | ast::Expr::NumberLiteral(_) + | ast::Expr::BooleanLiteral(_) + | ast::Expr::Set(_) + | ast::Expr::SetComp(_) + | ast::Expr::Generator(_) + | ast::Expr::TString(_) + | ast::Expr::Lambda(_) + ); + if warns && let Some(inferred) = self.infer_type(value) { + self.warn_syntax( + value.range(), + format!( + "'{}' object is not subscriptable; perhaps you missed a comma?", + inferred.name() + ), + )?; + } + Ok(()) + } + + fn check_index(&mut self, value: &ast::Expr, slice: &ast::Expr) -> CompileResult<()> { + let Some(index_type) = self.infer_type(slice) else { + return Ok(()); + }; + if index_type.is_long_subclass() || index_type == InferredType::Slice { + return Ok(()); + } + + let constant_warns = self.ast_constant_value(value).is_some_and(|constant| { + matches!( + constant, + ConstantData::Str { .. } | ConstantData::Bytes { .. } | ConstantData::Tuple { .. } + ) + }); + let warns = constant_warns + || matches!( + value, + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::Tuple(_) + | ast::Expr::List(_) + | ast::Expr::ListComp(_) + | ast::Expr::FString(_) + ); + if warns && let Some(value_type) = self.infer_type(value) { + self.warn_syntax( + value.range(), + format!( + "{} indices must be integers or slices, not {}; perhaps you missed a comma?", + value_type.name(), + index_type.name() + ), + )?; + } + Ok(()) + } + + fn check_assert(&mut self, assert_stmt: &ast::StmtAssert) -> CompileResult<()> { + let warns = match &*assert_stmt.test { + ast::Expr::Tuple(tuple) => !tuple.elts.is_empty(), + _ => matches!( + self.ast_constant_value(&assert_stmt.test), + Some(ConstantData::Tuple { ref elements }) if !elements.is_empty() + ), + }; + if warns { + self.warn_syntax( + assert_stmt.range, + "assertion is always true, perhaps remove parentheses?".to_owned(), + )?; + } + Ok(()) + } + + fn new_with_syntax_warning_handler( + opts: CompileOpts, + source_file: SourceFile, + code_name: &str, + syntax_warning_handler: Option<&'warnings mut SyntaxWarningHandler<'warnings>>, + ) -> Self { let module_code = ir::CodeInfo { // CPython convention: top-level module / interactive / // expression code does not carry CO_NEWLOCALS or CO_OPTIMIZED. @@ -487,7 +1081,7 @@ impl Compiler { flags: bytecode::CodeFlags::empty(), source_path: source_file.name().to_owned(), private: None, - blocks: vec![ir::Block::default()], + blocks: Blocks::from([Block::default()]), current_block: BlockIdx::new(0), instr_sequence: ir::InstructionSequence::new(), instr_sequence_label_map: ir::InstructionSequenceLabelMap::new(), @@ -509,7 +1103,7 @@ impl Compiler { }, static_attributes: None, in_inlined_comp: false, - fblock: Vec::with_capacity(MAXBLOCKS), + fblock: Vec::with_capacity(CO_MAXBLOCKS), symbol_table_index: 0, // Module is always the first symbol table nparams: 0, in_conditional_block: 0, @@ -522,6 +1116,7 @@ impl Compiler { // current_source_location: SourceLocation::default(), current_source_range: TextRange::default(), done_with_future_stmts: DoneWithFuture::No, + future_features: opts.future_features, future_annotations: false, ctx: CompileContext { in_class: false, @@ -532,29 +1127,15 @@ impl Compiler { in_annotation: false, interactive: false, do_not_emit_bytecode: 0, - disable_const_collection_folding: false, + disable_warning: 0, + syntax_warning_handler, } } - fn compile_expression_without_const_collection_folding( + fn compile_module_annotation_setup_sequence( &mut self, - expression: &ast::Expr, - ) -> CompileResult<()> { - let previous = self.disable_const_collection_folding; - self.disable_const_collection_folding = true; - let result = self.compile_expression(expression); - self.disable_const_collection_folding = previous; - result.map(|_| ()) - } - - fn is_unpack_assignment_target(target: &ast::Expr) -> bool { - matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)) - } - - fn compile_module_annotation_setup_sequence( - &mut self, - body: &[ast::Stmt], - loc: TextRange, + body: &[ast::Stmt], + loc: TextRange, ) -> CompileResult<()> { let ( saved_blocks, @@ -564,8 +1145,9 @@ impl Compiler { saved_annotations_instr_sequence, ) = { let code = self.current_code_info(); + ( - mem::replace(&mut code.blocks, vec![ir::Block::default()]), + mem::replace(&mut code.blocks, Blocks::from([Block::default()])), mem::replace(&mut code.current_block, BlockIdx::new(0)), mem::replace(&mut code.instr_sequence, ir::InstructionSequence::new()), mem::replace( @@ -585,8 +1167,12 @@ impl Compiler { mem::replace(&mut code.instr_sequence, saved_instr_sequence); code.current_block = saved_current_block; code.instr_sequence_label_map = saved_instr_sequence_label_map; - code.annotations_instr_sequence = Some(annotations_instr_sequence); debug_assert!(saved_annotations_instr_sequence.is_none()); + if matches!(result, Ok(true)) { + code.annotations_instr_sequence = Some(annotations_instr_sequence); + } else { + code.annotations_instr_sequence = saved_annotations_instr_sequence; + } }; result.map(|_| ()) @@ -623,13 +1209,17 @@ impl Compiler { ) -> CompileResult<()> { // Save full subscript expression range (set by compile_expression before this call) let subscript_range = self.current_source_range; + if matches!(ctx, ast::ExprContext::Load) { + self.check_subscripter(value)?; + self.check_index(value, slice)?; + } // VISIT(c, expr, e->v.Subscript.value) self.compile_expression(value)?; // Handle two-element non-constant slice with BINARY_SLICE/STORE_SLICE let use_slice_opt = matches!(ctx, ast::ExprContext::Load | ast::ExprContext::Store) - && slice.should_use_slice_optimization(); + && self.should_apply_two_element_slice_optimization(slice); if use_slice_opt { match slice { ast::Expr::Slice(s) => self.compile_slice_two_parts(s)?, @@ -675,9 +1265,10 @@ impl Compiler { /// - collection_type: What type of collection to build (tuple, list, set) /// // = starunpack_helper in compile.c - fn starunpack_helper( + fn starunpack_helper_impl( &mut self, elts: &[ast::Expr], + injected_arg: Option<&str>, pushed: u32, collection_type: CollectionType, ) -> CompileResult<()> { @@ -685,46 +1276,23 @@ impl Compiler { let n = elts.len().to_u32(); let seen_star = elts.iter().any(|e| matches!(e, ast::Expr::Starred(_))); - let big = n + pushed > STACK_USE_GUIDELINE; - - // Match CPython's constant ordering by letting the late flowgraph-style - // folding passes introduce tuple-backed constants after their operands - // have first been emitted as constants. - let can_fold_const_collection = false; - if !self.disable_const_collection_folding - && !seen_star - && pushed == 0 - && can_fold_const_collection - && let Some(folded) = self.try_fold_constant_collection(elts, collection_type)? - { - match collection_type { - CollectionType::Tuple => { - self.emit_load_const(folded); - } - CollectionType::List => { - self.set_source_range(collection_range); - emit!(self, Instruction::BuildList { count: 0 }); - self.emit_load_const(folded); - self.set_source_range(collection_range); - emit!(self, Instruction::ListExtend { i: 1 }); - } - CollectionType::Set => { - self.set_source_range(collection_range); - emit!(self, Instruction::BuildSet { count: 0 }); - self.emit_load_const(folded); - self.set_source_range(collection_range); - emit!(self, Instruction::SetUpdate { i: 1 }); - } - } - return Ok(()); - } + let injected_count = u32::from(injected_arg.is_some()); + let big = n + pushed + injected_count > STACK_USE_GUIDELINE; + + // Constant collections are not folded here: the late flowgraph + // optimization passes introduce tuple-backed constants after their + // operands have first been emitted, matching the constant ordering. // If no stars and not too big, compile all elements and build once if !seen_star && !big { for elt in elts { self.compile_expression(elt)?; } - let total_size = n + pushed; + if let Some(injected_arg) = injected_arg { + self.set_source_range(collection_range); + self.load_name(injected_arg)?; + } + let total_size = n + injected_count + pushed; self.set_source_range(collection_range); match collection_type { CollectionType::List => { @@ -745,6 +1313,7 @@ impl Compiler { let mut i = 0u32; if big { + self.set_source_range(collection_range); match collection_type { CollectionType::List => { emit!(self, Instruction::BuildList { count: pushed }); @@ -819,22 +1388,22 @@ impl Compiler { } } - // If we never built sequence (all non-starred), build it now - if !sequence_built { + debug_assert!(sequence_built); + if let Some(injected_arg) = injected_arg { + self.set_source_range(collection_range); + self.load_name(injected_arg)?; self.set_source_range(collection_range); match collection_type { - CollectionType::List => { - emit!(self, Instruction::BuildList { count: i + pushed }); + CollectionType::List | CollectionType::Tuple => { + emit!(self, Instruction::ListAppend { i: 1 }); } CollectionType::Set => { - emit!(self, Instruction::BuildSet { count: i + pushed }); - } - CollectionType::Tuple => { - emit!(self, Instruction::BuildTuple { count: i + pushed }); + emit!(self, Instruction::SetAdd { i: 1 }); } } - } else if collection_type == CollectionType::Tuple { - // For tuples, convert the list to tuple + } + + if collection_type == CollectionType::Tuple { self.set_source_range(collection_range); emit!( self, @@ -847,6 +1416,15 @@ impl Compiler { Ok(()) } + fn starunpack_helper( + &mut self, + elts: &[ast::Expr], + pushed: u32, + collection_type: CollectionType, + ) -> CompileResult<()> { + self.starunpack_helper_impl(elts, None, pushed, collection_type) + } + fn error(&mut self, error: CodegenErrorType) -> CodegenError { self.error_ranged(error, self.current_source_range) } @@ -863,6 +1441,21 @@ impl Compiler { } } + fn error_optional_range( + &mut self, + error: CodegenErrorType, + range: Option, + ) -> CodegenError { + match range { + Some(range) => self.error_ranged(error, range), + None => CodegenError { + error, + location: None, + source_path: self.source_file.name().to_owned(), + }, + } + } + /// Get the SymbolTable for the current scope. fn current_symbol_table(&self) -> &SymbolTable { self.symbol_table_stack @@ -880,7 +1473,7 @@ impl Compiler { self.symbol_table_stack .first() .and_then(|table| table.symbols.get(name)) - .is_some_and(|sym| sym.flags.contains(SymbolFlags::IMPORTED)) + .is_some_and(|sym| sym.flags.contains(SymbolFlags::DEF_IMPORT)) } /// Get the cell-relative index of a free variable. @@ -945,6 +1538,20 @@ impl Compiler { )))); } + while current_table.next_sub_table < current_table.sub_tables.len() + && current_table.sub_tables[current_table.next_sub_table].typ + == CompilerScope::Annotation + { + current_table.next_sub_table += 1; + } + if current_table.next_sub_table >= current_table.sub_tables.len() { + let name = current_table.name.clone(); + let typ = current_table.typ; + return Err(self.error(CodegenErrorType::SyntaxError(format!( + "no symbol table available in {name} (type: {typ:?})" + )))); + } + let idx = current_table.next_sub_table; current_table.next_sub_table += 1; let table = current_table.sub_tables[idx].clone(); @@ -954,28 +1561,94 @@ impl Compiler { Ok(self.current_symbol_table()) } - /// Push the annotation symbol table from the next sub_table's annotation_block - /// The annotation_block is stored in the function's scope, which is the next sub_table - /// Returns true if annotation_block exists, false otherwise - fn push_annotation_symbol_table(&mut self) -> bool { + fn push_symbol_table_matching( + &mut self, + typ: CompilerScope, + table_name: &str, + ) -> CompileResult<&SymbolTable> { let current_table = self .symbol_table_stack .last_mut() .expect("no current symbol table"); - // The annotation_block is in the next sub_table (function scope) - let next_idx = current_table.next_sub_table; - if next_idx >= current_table.sub_tables.len() { - return false; + while current_table.next_sub_table < current_table.sub_tables.len() + && current_table.sub_tables[current_table.next_sub_table].typ + == CompilerScope::Annotation + { + current_table.next_sub_table += 1; } - let next_table = &mut current_table.sub_tables[next_idx]; - if let Some(annotation_block) = next_table.annotation_block.take() { - self.symbol_table_stack.push(*annotation_block); - true - } else { - false + let start = current_table.next_sub_table; + let Some(idx) = current_table.sub_tables[start..] + .iter() + .position(|table| table.typ == typ && table.name == table_name) + .map(|idx| start + idx) + else { + let name = current_table.name.clone(); + let current_typ = current_table.typ; + return Err(self.error(CodegenErrorType::SyntaxError(format!( + "no matching symbol table {table_name} ({typ:?}) available in {name} (type: {current_typ:?})" + )))); + }; + + let table = current_table.sub_tables[idx].clone(); + current_table.next_sub_table = idx + 1; + self.symbol_table_stack.push(table); + Ok(self.current_symbol_table()) + } + + /// Push the function annotation symbol table. + /// Signature annotation blocks are stored in st_blocks keyed by the + /// arguments AST node. Without future annotations they are also children; + /// with future annotations they are hidden from children and consumed here. + fn push_annotation_symbol_table(&mut self) -> bool { + let Some(annotation_table) = ({ + let current_table = self + .symbol_table_stack + .last_mut() + .expect("no current symbol table"); + + let next_idx = current_table.next_sub_table; + if next_idx < current_table.sub_tables.len() + && current_table.sub_tables[next_idx].typ == CompilerScope::Annotation + { + let next_table = current_table.sub_tables[next_idx].clone(); + current_table.next_sub_table += 1; + Some(next_table) + } else if current_table.next_hidden_annotation_block + < current_table.hidden_annotation_blocks.len() + { + let idx = current_table.next_hidden_annotation_block; + current_table.next_hidden_annotation_block += 1; + Some(current_table.hidden_annotation_blocks[idx].clone()) + } else { + None + } + }) else { + return false; + }; + + self.symbol_table_stack.push(annotation_table); + true + } + + fn next_function_annotation_symbol_table_uses_annotations(&self) -> bool { + let current_table = self + .symbol_table_stack + .last() + .expect("no current symbol table"); + let next_idx = current_table.next_sub_table; + if next_idx < current_table.sub_tables.len() + && current_table.sub_tables[next_idx].typ == CompilerScope::Annotation + { + return current_table.sub_tables[next_idx].annotations_used; } + + let hidden_idx = current_table.next_hidden_annotation_block; + current_table + .hidden_annotation_blocks + .get(hidden_idx) + .is_some_and(|table| table.annotations_used) } /// Push the annotation symbol table for module/class level annotations @@ -995,19 +1668,9 @@ impl Compiler { } } - /// Pop the annotation symbol table and restore it to the function scope's annotation_block + /// Pop the annotation symbol table. fn pop_annotation_symbol_table(&mut self) { - let annotation_table = self.symbol_table_stack.pop().expect("compiler bug"); - let current_table = self - .symbol_table_stack - .last_mut() - .expect("no current symbol table"); - - // Restore to the next sub_table (function scope) where it came from - let next_idx = current_table.next_sub_table; - if next_idx < current_table.sub_tables.len() { - current_table.sub_tables[next_idx].annotation_block = Some(Box::new(annotation_table)); - } + self.symbol_table_stack.pop().expect("compiler bug"); } /// Pop the current symbol table off the stack @@ -1048,28 +1711,22 @@ impl Compiler { return None; } - // 5. Must be inside a function (not at module level or class body) - if !self.ctx.in_func() { - return None; - } - - // 6. "super" must be GlobalImplicit (not redefined locally or at module level) + // 5. "super" must be GlobalImplicit in the current scope. let table = self.current_symbol_table(); if let Some(symbol) = table.lookup("super") && symbol.scope != SymbolScope::GlobalImplicit { return None; } - // Also check top-level scope to detect module-level shadowing. - // Only block if super is actually *bound* at module level (not just used). + // Then check the top-level scope and reject any statically + // visible symbol for "super", not just local bindings. if let Some(top_table) = self.symbol_table_stack.first() - && let Some(sym) = top_table.lookup("super") - && sym.scope != SymbolScope::GlobalImplicit + && top_table.lookup("super").is_some() { return None; } - // 7. Check argument pattern + // 6. Check argument pattern let args = &arguments.args; // No starred expressions allowed @@ -1094,16 +1751,14 @@ impl Compiler { } // Check if __class__ is available as a cell/free variable - // The scope must be Free (from enclosing class) or have FREE_CLASS flag - if let Some(symbol) = table.lookup("__class__") { + // The scope must be Free (from enclosing class) or have DEF_FREE_CLASS flag + { + let symbol = table.lookup("__class__")?; if symbol.scope != SymbolScope::Free - && !symbol.flags.contains(SymbolFlags::FREE_CLASS) + && !symbol.flags.contains(SymbolFlags::DEF_FREE_CLASS) { return None; } - } else { - // __class__ not in symbol table, optimization not possible - return None; } Some(SuperCallType::ZeroArg) @@ -1211,32 +1866,16 @@ impl Compiler { // Build cellvars using dictbytype (CELL scope or COMP_CELL flag, sorted) let mut cellvar_cache = IndexSet::default(); - // CPython ordering: parameter cells first (in parameter order), - // then non-parameter cells (alphabetically sorted) - let cell_symbols: Vec<_> = ste + let mut cell_names: Vec<_> = ste .symbols .iter() .filter(|(_, s)| { - s.scope == SymbolScope::Cell || s.flags.contains(SymbolFlags::COMP_CELL) + s.scope == SymbolScope::Cell || s.flags.contains(SymbolFlags::DEF_COMP_CELL) }) - .map(|(name, sym)| (name.clone(), sym.flags)) + .map(|(name, _)| name.clone()) .collect(); - let mut param_cells = Vec::new(); - let mut nonparam_cells = Vec::new(); - for (name, flags) in cell_symbols { - if flags.contains(SymbolFlags::PARAMETER) { - param_cells.push(name); - } else { - nonparam_cells.push(name); - } - } - // param_cells are already in parameter order (from varname_cache insertion order) - param_cells.sort_by_key(|n| varname_cache.get_index_of(n.as_str()).unwrap_or(usize::MAX)); - nonparam_cells.sort(); - for name in param_cells { - cellvar_cache.insert(name); - } - for name in nonparam_cells { + cell_names.sort(); + for name in cell_names { cellvar_cache.insert(name); } @@ -1279,9 +1918,9 @@ impl Compiler { .filter(|(_, s)| { s.scope == SymbolScope::Free || (scope_type != CompilerScope::Class - && s.flags.contains(SymbolFlags::FREE_CLASS)) + && s.flags.contains(SymbolFlags::DEF_FREE_CLASS)) || (scope_type == CompilerScope::Class - && s.flags.contains(SymbolFlags::FREE_CLASS) + && s.flags.contains(SymbolFlags::DEF_FREE_CLASS) && self.has_enclosing_non_module_code_scope()) }) .filter(|(name, symbol)| { @@ -1325,7 +1964,13 @@ impl Compiler { CompilerScope::Annotation => ( bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED, 1, // format is positional-only - 1, // annotation scope takes one argument (format) + 0, + 0, + ), + CompilerScope::TypeAlias | CompilerScope::TypeVariable => ( + bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED, + 1, // format is positional-only + 0, 0, ), }; @@ -1345,15 +1990,15 @@ impl Compiler { | CompilerScope::Lambda | CompilerScope::Comprehension | CompilerScope::Annotation + | CompilerScope::TypeAlias + | CompilerScope::TypeVariable | CompilerScope::TypeParams ) { flags | bytecode::CodeFlags::NESTED } else { flags }; - if self.future_annotations { - flags |= bytecode::CodeFlags::FUTURE_ANNOTATIONS; - } + flags |= self.future_features; // Get private name from parent scope let private = if !self.code_stack.is_empty() { @@ -1367,7 +2012,7 @@ impl Compiler { flags, source_path, private, - blocks: vec![ir::Block::default()], + blocks: Blocks::from([Block::default()]), current_block: BlockIdx::new(0), instr_sequence: ir::InstructionSequence::new(), instr_sequence_label_map: ir::InstructionSequenceLabelMap::new(), @@ -1393,7 +2038,7 @@ impl Compiler { None }, in_inlined_comp: false, - fblock: Vec::with_capacity(MAXBLOCKS), + fblock: Vec::with_capacity(CO_MAXBLOCKS), symbol_table_index: key, nparams, in_conditional_block: 0, @@ -1479,6 +2124,12 @@ impl Compiler { | (info.flags & (bytecode::CodeFlags::NESTED | bytecode::CodeFlags::METHOD + | 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)); info.metadata.argcount = arg_count; info.metadata.posonlyargcount = posonlyarg_count; @@ -1489,7 +2140,7 @@ impl Compiler { // compiler_exit_scope fn exit_scope(&mut self) -> CodeObject { - let _table = self.pop_symbol_table(); + self.pop_symbol_table(); // Various scopes can have sub_tables: // - ast::TypeParams scope can have sub_tables (the function body's symbol table) // - Module scope can have sub_tables (for TypeAlias scopes, nested functions, classes) @@ -1502,21 +2153,29 @@ impl Compiler { unwrap_internal(self, stack_top.finalize_code(&self.opts)) } - /// Exit annotation scope - similar to exit_scope but restores annotation_block to parent + fn expose_annotation_format_parameter(code: &mut CodeObject) { + if let Some(first) = code.varnames.first_mut() { + *first = String::from("format"); + } + } + + /// Exit a function signature annotation scope. fn exit_annotation_scope(&mut self, saved_ctx: CompileContext) -> CodeObject { self.pop_annotation_symbol_table(); self.ctx = saved_ctx; let pop = self.code_stack.pop(); let stack_top = compiler_unwrap_option(self, pop); - unwrap_internal(self, stack_top.finalize_code(&self.opts)) + let mut code = unwrap_internal(self, stack_top.finalize_code(&self.opts)); + Self::expose_annotation_format_parameter(&mut code); + code } - /// Enter annotation scope using the symbol table's annotation_block. - /// Returns None if no annotation_block exists. + /// Enter a function signature annotation scope. + /// Returns None if no matching annotation symbol table exists. /// On success, returns the saved CompileContext to pass to exit_annotation_scope. fn enter_annotation_scope( &mut self, - _func_name: &str, + func_name: &str, loc: TextRange, ) -> CompileResult> { if !self.push_annotation_symbol_table() { @@ -1541,12 +2200,18 @@ impl Compiler { lineno.to_u32(), )?; - // Override arg_count since enter_scope sets it to 1 but we need the varnames - // setup to be correct too + // enter_scope() qualified the scope by the enclosing scope only; redo it + // now that the annotated function is known. Only signature annotations + // get this treatment - deferred class and module annotations are + // compiled inside the scope they belong to and are already qualified. + self.set_annotation_qualname(func_name); + + // Keep the internal ".format" name; exit_annotation_scope() + // renames it to "format" on the final code object. self.current_code_info() .metadata .varnames - .insert("format".to_owned()); + .insert(".format".to_owned()); // Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError // VALUE_WITH_FAKE_GLOBALS = 2 (from annotationlib.Format) @@ -1609,12 +2274,15 @@ impl Compiler { fb_datum: FBlockDatum, ) -> CompileResult<()> { let fb_range = self.current_source_range; - let code = self.current_code_info(); - if code.fblock.len() >= MAXBLOCKS { + if self.current_code_info().fblock.len() >= CO_MAXBLOCKS { return Err(self.error(CodegenErrorType::SyntaxError( "too many statically nested blocks".to_owned(), ))); } + if matches!(fb_type, FBlockType::FinallyEnd) { + self.disable_warning += 1; + } + let code = self.current_code_info(); code.fblock.push(FBlockInfo { fb_type, fb_block, @@ -1631,16 +2299,49 @@ impl Compiler { expected_type: FBlockType, expected_block: ir::InstructionSequenceLabel, ) -> FBlockInfo { - let code = self.current_code_info(); - let fblock = code.fblock.pop().expect("fblock stack underflow"); + let fblock = { + let code = self.current_code_info(); + code.fblock.pop().expect("fblock stack underflow") + }; debug_assert_eq!(fblock.fb_type, expected_type); debug_assert_eq!( fblock.fb_block, expected_block, "CPython _PyCompile_PopFBlock asserts the popped fb_block label" ); + if matches!(expected_type, FBlockType::FinallyEnd) { + self.disable_warning -= 1; + } fblock } + /// `_PyCompile_PushFBlock()` call used by + /// `codegen_unwind_fblock_stack()` to restore the copied fblock after + /// recursive unwinding. + fn restore_fblock_info(&mut self, fblock: FBlockInfo) -> CompileResult<()> { + let FBlockInfo { + fb_type, + fb_block, + fb_exit, + fb_range, + fb_datum, + } = fblock; + let code = self.current_code_info(); + if code.fblock.len() >= CO_MAXBLOCKS { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError("too many statically nested blocks".to_owned()), + fb_range, + )); + } + code.fblock.push(FBlockInfo { + fb_type, + fb_block, + fb_exit, + fb_range, + fb_datum, + }); + Ok(()) + } + fn set_unwind_source_range(&mut self, loc: Option) { if let Some(range) = loc { self.set_source_range(range); @@ -1690,10 +2391,38 @@ impl Compiler { } FBlockType::FinallyTry => { - // FinallyTry is now handled specially in unwind_fblock_stack - // to avoid infinite recursion when the finally body contains return/break/continue. - // This branch should not be reached. - unreachable!("FinallyTry should be handled by unwind_fblock_stack"); + // codegen_unwind_fblock(FINALLY_TRY) + self.set_unwind_source_range(*loc); + emit!(self, PseudoInstruction::PopBlock); + self.mark_unwind_no_location(*loc); + + if preserve_tos { + self.push_fblock_labels( + FBlockType::PopValue, + ir::InstructionSequenceLabel::NO_LABEL, + ir::InstructionSequenceLabel::NO_LABEL, + FBlockDatum::None, + )?; + } + + if let FBlockDatum::FinallyBody(ref body) = info.fb_datum { + // This is an extra copy of the finally body, emitted for the + // path that leaves the try block early. The try statement + // emits its own copies afterwards, so rewind the symbol table + // cursors and leave the nested scopes for those copies. + let symbol_table_cursors = self.current_symbol_table_cursors(); + self.compile_statements(body)?; + self.set_symbol_table_cursors(symbol_table_cursors); + } + + if preserve_tos { + self.pop_fblock_label( + FBlockType::PopValue, + ir::InstructionSequenceLabel::NO_LABEL, + ); + } + + *loc = None; } FBlockType::FinallyEnd => { @@ -1825,96 +2554,40 @@ impl Compiler { preserve_tos: bool, stop_at_loop: bool, ) -> CompileResult<(Option, Option)> { - // Collect the info we need, with indices for FinallyTry blocks - #[derive(Clone)] - enum UnwindInfo { - Normal(FBlockInfo), - FinallyTry { - body: Vec, - fblock_idx: usize, - }, - } - let mut unwind_infos = Vec::new(); - let mut loop_fblock = None; - - { - let code = self.current_code_info(); - for i in (0..code.fblock.len()).rev() { - // Check for exception group handler (forbidden) - if matches!(code.fblock[i].fb_type, FBlockType::ExceptionGroupHandler) { - return Err(self.error(CodegenErrorType::BreakContinueReturnInExceptStar)); - } - - // Stop at loop if requested - if stop_at_loop - && matches!( - code.fblock[i].fb_type, - FBlockType::WhileLoop | FBlockType::ForLoop - ) - { - loop_fblock = Some(code.fblock[i].clone()); - break; - } - - if matches!(code.fblock[i].fb_type, FBlockType::FinallyTry) { - if let FBlockDatum::FinallyBody(ref body) = code.fblock[i].fb_datum { - unwind_infos.push(UnwindInfo::FinallyTry { - body: body.clone(), - fblock_idx: i, - }); - } - } else { - unwind_infos.push(UnwindInfo::Normal(code.fblock[i].clone())); - } - } - } - - // Process each fblock let mut unwind_loc = Some(self.current_source_range); - for info in unwind_infos { - match info { - UnwindInfo::Normal(fblock_info) => { - self.unwind_fblock(&fblock_info, preserve_tos, &mut unwind_loc)?; - } - UnwindInfo::FinallyTry { body, fblock_idx } => { - // codegen_unwind_fblock(FINALLY_TRY) - self.set_unwind_source_range(unwind_loc); - emit!(self, PseudoInstruction::PopBlock); - self.mark_unwind_no_location(unwind_loc); - - // Temporarily remove the FinallyTry fblock so nested return/break/continue - // in the finally body won't see it again - let code = self.current_code_info(); - let saved_fblock = code.fblock.remove(fblock_idx); - - // Push PopValue fblock if preserving tos - if preserve_tos { - self.push_fblock_labels( - FBlockType::PopValue, - ir::InstructionSequenceLabel::NO_LABEL, - ir::InstructionSequenceLabel::NO_LABEL, - FBlockDatum::None, - )?; - } - - self.compile_statements(&body)?; - unwind_loc = None; - - if preserve_tos { - self.pop_fblock_label( - FBlockType::PopValue, - ir::InstructionSequenceLabel::NO_LABEL, - ); - } + let loop_fblock = + self.unwind_fblock_stack_inner(preserve_tos, stop_at_loop, &mut unwind_loc)?; + Ok((unwind_loc, loop_fblock)) + } - // Restore the fblock - let code = self.current_code_info(); - code.fblock.insert(fblock_idx, saved_fblock); - } - } + fn unwind_fblock_stack_inner( + &mut self, + preserve_tos: bool, + stop_at_loop: bool, + unwind_loc: &mut Option, + ) -> CompileResult> { + let Some(top) = self.current_code_info().fblock.last().cloned() else { + return Ok(None); + }; + if matches!(top.fb_type, FBlockType::ExceptionGroupHandler) { + return Err(self.error_optional_range( + CodegenErrorType::BreakContinueReturnInExceptStar, + *unwind_loc, + )); + } + if stop_at_loop && matches!(top.fb_type, FBlockType::WhileLoop | FBlockType::ForLoop) { + return Ok(Some(top)); } - Ok((unwind_loc, loop_fblock)) + let copy = self + .current_code_info() + .fblock + .pop() + .expect("fblock stack underflow"); + self.unwind_fblock(©, preserve_tos, unwind_loc)?; + let loop_fblock = self.unwind_fblock_stack_inner(preserve_tos, stop_at_loop, unwind_loc)?; + self.restore_fblock_info(copy)?; + Ok(loop_fblock) } // could take impl Into>, but everything is borrowed from ast structs; we never @@ -1944,11 +2617,24 @@ impl Compiler { /// Set the qualified name for the current code object // = compiler_set_qualname fn set_qualname(&mut self) -> String { - let qualname = self.make_qualname(); + self.set_qualname_for_function(None) + } + + /// Set the qualname of an annotation scope, qualified by the function whose + /// signature it annotates. CPython records that name on the annotation + /// block's symbol table entry (`ste_function_name`) and folds it into the + /// qualname, so `f`'s annotation scope is named `f.__annotate__`. + fn set_annotation_qualname(&mut self, function_name: &str) { + self.set_qualname_for_function(Some(function_name)); + } + + fn set_qualname_for_function(&mut self, function_name: Option<&str>) -> String { + let qualname = self.make_qualname(function_name); self.current_code_info().metadata.qualname = Some(qualname.clone()); qualname } - fn make_qualname(&mut self) -> String { + + fn make_qualname(&mut self, function_name: Option<&str>) -> String { let stack_size = self.code_stack.len(); assert!(stack_size >= 1); @@ -1972,7 +2658,12 @@ impl Compiler { // when building qualnames for the contained function/class code object. if matches!( parent_scope, - Some(CompilerScope::TypeParams | CompilerScope::Annotation) + Some( + CompilerScope::TypeParams + | CompilerScope::Annotation + | CompilerScope::TypeAlias + | CompilerScope::TypeVariable, + ) ) || parent.metadata.name.starts_with(" to parent qualname - // Use parent's qualname if available, otherwise use parent_obj_name - let parent_qualname = parent.metadata.qualname.as_ref().unwrap_or(parent_obj_name); - format!("{parent_qualname}..{current_obj_name}") + Some(format!("{parent_qualname}.")) + } else if parent_qualname == "" { + // Module level, nothing to qualify by + None } else { // For classes and other scopes, use parent's qualname directly - // Use parent's qualname if available, otherwise use parent_obj_name - let parent_qualname = parent.metadata.qualname.as_ref().unwrap_or(parent_obj_name); - if parent_qualname == "" { - // Module level, just use the name - current_obj_name - } else { - // Concatenate parent qualname with current name - format!("{parent_qualname}.{current_obj_name}") - } + Some(parent_qualname.clone()) } + }; + + // An annotation scope is compiled in the scope enclosing the function it + // annotates, so the function itself is missing from the prefix above. + let base = match (base, function_name) { + (Some(base), Some(function_name)) => Some(format!("{base}.{function_name}")), + (None, Some(function_name)) => Some(function_name.to_owned()), + (base, None) => base, + }; + + match base { + Some(base) => format!("{base}.{current_obj_name}"), + None => current_obj_name, } } @@ -2071,11 +2771,20 @@ impl Compiler { let size_before = self.code_stack.len(); // Set future_annotations from symbol table (detected during symbol table scan) self.future_annotations = symbol_table.future_annotations; + let future_features = self.future_features; + self.current_code_info().flags |= future_features; if self.future_annotations { + self.future_features + .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); self.current_code_info() .flags .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); } + if symbol_table.is_coroutine { + self.current_code_info() + .flags + .insert(bytecode::CodeFlags::COROUTINE); + } // Module-level __conditional_annotations__ cell let has_module_cond_ann = Self::scope_needs_conditional_annotations_cell(&symbol_table); @@ -2093,18 +2802,18 @@ impl Compiler { let (doc, statements) = split_doc_with_range(&body.body, &self.opts); let module_start_loc = self.module_start_location(&body.body); + let annotations_used = self.current_symbol_table().annotations_used; // Handle annotation bookkeeping before the docstring assignment, as // codegen_body() does after _PyCodegen_Module() inserts the prefix set. - if Self::find_ann(statements) { + if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { self.set_source_range(module_start_loc); - if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { - emit!(self, Instruction::BuildSet { count: 0 }); - self.store_name("__conditional_annotations__")?; - } + emit!(self, Instruction::BuildSet { count: 0 }); + self.store_name("__conditional_annotations__")?; + } - if self.future_annotations { - emit!(self, Instruction::SetupAnnotations); - } + if self.future_annotations && annotations_used { + self.set_source_range(module_start_loc); + emit!(self, Instruction::SetupAnnotations); } if let Some((value, range)) = doc { @@ -2115,13 +2824,14 @@ impl Compiler { }); let doc = self.name("__doc__"); emit!(self, Instruction::StoreName { namei: doc }); + self.set_no_location(); self.set_source_range(saved_range); } // Compile all statements self.compile_statements(statements)?; - if Self::find_ann(statements) && !self.future_annotations { + if annotations_used && !self.future_annotations { self.compile_module_annotation_setup_sequence(statements, module_start_loc)?; } @@ -2142,76 +2852,47 @@ impl Compiler { self.interactive = true; // Set future_annotations from symbol table (detected during symbol table scan) self.future_annotations = symbol_table.future_annotations; + let future_features = self.future_features; + self.current_code_info().flags |= future_features; if self.future_annotations { + self.future_features + .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); self.current_code_info() .flags .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); } + if symbol_table.is_coroutine { + self.current_code_info() + .flags + .insert(bytecode::CodeFlags::COROUTINE); + } self.symbol_table_stack.push(symbol_table); let module_start_loc = self.module_start_location(body); self.emit_resume_for_scope(CompilerScope::Module, 1); emit!(self, PseudoInstruction::AnnotationsPlaceholder); + let annotations_used = self.current_symbol_table().annotations_used; // Handle annotations based on future_annotations flag - if Self::find_ann(body) { + if self.current_symbol_table().has_conditional_annotations { self.set_source_range(module_start_loc); - if self.future_annotations { - // PEP 563: Initialize __annotations__ dict - emit!(self, Instruction::SetupAnnotations); - } else { - // PEP 649: Initialize __conditional_annotations__ before the body. - // CPython generates __annotate__ after the body in codegen_body(). - if self.current_symbol_table().has_conditional_annotations { - emit!(self, Instruction::BuildSet { count: 0 }); - self.store_name("__conditional_annotations__")?; - } - } + emit!(self, Instruction::BuildSet { count: 0 }); + self.store_name("__conditional_annotations__")?; } - if let Some((last, body)) = body.split_last() { - for statement in body { - if let ast::Stmt::Expr(ast::StmtExpr { value, .. }) = &statement { - self.compile_expression(value)?; - emit!( - self, - Instruction::CallIntrinsic1 { - func: bytecode::IntrinsicFunction1::Print - } - ); - - emit!(self, Instruction::PopTop); - self.set_no_location(); - } else { - self.compile_statement(statement)?; - } - } - - if let ast::Stmt::Expr(ast::StmtExpr { value, .. }) = &last { - self.compile_expression(value)?; - emit!(self, Instruction::Copy { i: 1 }); - emit!( - self, - Instruction::CallIntrinsic1 { - func: bytecode::IntrinsicFunction1::Print - } - ); + if self.future_annotations && annotations_used { + self.set_source_range(module_start_loc); + // PEP 563: Initialize __annotations__ dict + emit!(self, Instruction::SetupAnnotations); + } - emit!(self, Instruction::PopTop); - self.set_no_location(); - } else { - self.compile_statement(last)?; - self.emit_load_const(ConstantData::None); - } - } else { - self.emit_load_const(ConstantData::None); - }; + self.compile_statements(body)?; - if Self::find_ann(body) && !self.future_annotations { + if annotations_used && !self.future_annotations { self.compile_module_annotation_setup_sequence(body, module_start_loc)?; } - self.emit_return_value(); + self.emit_return_const_no_location(ConstantData::None); Ok(()) } @@ -2220,6 +2901,13 @@ impl Compiler { body: &[ast::Stmt], symbol_table: SymbolTable, ) -> CompileResult<()> { + let future_features = self.future_features; + self.current_code_info().flags |= future_features; + if symbol_table.is_coroutine { + self.current_code_info() + .flags + .insert(bytecode::CodeFlags::COROUTINE); + } self.symbol_table_stack.push(symbol_table); self.emit_resume_for_scope(CompilerScope::Module, 1); @@ -2287,6 +2975,13 @@ impl Compiler { expression: &ast::ModExpression, symbol_table: SymbolTable, ) -> CompileResult<()> { + let future_features = self.future_features; + self.current_code_info().flags |= future_features; + if symbol_table.is_coroutine { + self.current_code_info() + .flags + .insert(bytecode::CodeFlags::COROUTINE); + } self.symbol_table_stack.push(symbol_table); self.emit_resume_for_scope(CompilerScope::Module, 1); @@ -2388,7 +3083,6 @@ impl Compiler { fn emit_no_location_exception_name_cleanup(&mut self, name: &str) -> CompileResult<()> { // CPython codegen_try_except() emits `name = None; del name` // with NO_LOCATION for `except ... as name` cleanup. - self.set_no_location(); self.emit_load_const(ConstantData::None); self.set_no_location(); self.store_name(name)?; @@ -2446,7 +3140,10 @@ impl Compiler { let current_idx = self.symbol_table_stack.len() - 1; let current_table = &self.symbol_table_stack[current_idx]; let is_typeparams = current_table.typ == CompilerScope::TypeParams; - let is_annotation = current_table.typ == CompilerScope::Annotation; + let is_annotation = matches!( + current_table.typ, + CompilerScope::Annotation | CompilerScope::TypeAlias | CompilerScope::TypeVariable + ); let can_see_class = current_table.can_see_class_scope; // First try to find in current table @@ -2467,7 +3164,7 @@ impl Compiler { .rev() .find(|table| table.typ == CompilerScope::Class) .and_then(|table| table.lookup(name.as_ref())) - .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::GLOBAL)); + .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::DEF_GLOBAL)); ( symbol.map(|s| s.scope), @@ -2506,7 +3203,10 @@ impl Compiler { let current_table = self.current_symbol_table(); if matches!( current_table.typ, - CompilerScope::Annotation | CompilerScope::TypeParams + CompilerScope::Annotation + | CompilerScope::TypeAlias + | CompilerScope::TypeVariable + | CompilerScope::TypeParams ) { SymbolScope::GlobalImplicit } else if matches!( @@ -2560,7 +3260,7 @@ impl Compiler { // to check classdict first before globals if class_declared_global { NameOp::Global - } else if can_see_class_scope { + } else if can_see_class_scope && usage == NameUsage::Load { NameOp::DictOrGlobals } else if is_function_like { NameOp::Global @@ -2572,7 +3272,7 @@ impl Compiler { // A global declared in the owning class body must bypass the // classdict, but an explicit global inherited from an outer // function still participates in DictOrGlobals lookup. - if can_see_class_scope && !class_declared_global { + if can_see_class_scope && !class_declared_global && usage == NameUsage::Load { NameOp::DictOrGlobals } else { NameOp::Global @@ -2656,21 +3356,10 @@ impl Compiler { NameOp::DictOrGlobals => { // PEP 649: First check classdict (from __classdict__ freevar), then globals let idx = self.get_global_name_index(&name); - match usage { - NameUsage::Load => { - // Load __classdict__ first (it's a free variable in annotation scope) - let classdict_idx = self.get_free_var_index("__classdict__"); - emit!(self, Instruction::LoadDeref { i: classdict_idx }); - emit!(self, Instruction::LoadFromDictOrGlobals { i: idx }); - } - // Store/Delete in annotation scope should use Name ops - NameUsage::Store => { - emit!(self, Instruction::StoreName { namei: idx }); - } - NameUsage::Delete => { - emit!(self, Instruction::DeleteName { namei: idx }); - } - } + debug_assert!(usage == NameUsage::Load); + let classdict_idx = self.get_free_var_index("__classdict__"); + emit!(self, Instruction::LoadDeref { i: classdict_idx }); + emit!(self, Instruction::LoadFromDictOrGlobals { i: idx }); } } @@ -2685,14 +3374,17 @@ impl Compiler { match &statement { // we do this here because `from __future__` still executes that `from` statement at runtime, // we still need to compile the ImportFrom down below - ast::Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) - if module.as_ref().map(|id| id.as_str()) == Some("__future__") => - { + ast::Stmt::ImportFrom(ast::StmtImportFrom { + module, + names, + level, + .. + }) if *level == 0 && module.as_ref().map(|id| id.as_str()) == Some("__future__") => { self.compile_future_features(names)? } // ignore module-level doc comments ast::Stmt::Expr(ast::StmtExpr { value, .. }) - if matches!(&**value, ast::Expr::StringLiteral(..)) + if is_docstring_expr(value) && matches!(self.done_with_future_stmts, DoneWithFuture::No) => { self.done_with_future_stmts = DoneWithFuture::DoneWithDoc @@ -2737,24 +3429,14 @@ impl Compiler { names, .. }) => { - let import_star = names.iter().any(|n| &n.name == "*"); + let import_star = names.first().is_some_and(|n| &n.name == "*"); - let from_list = if import_star { - if self.ctx.in_func() { - return Err(self.error_ranged( - CodegenErrorType::FunctionImportStar, - statement.range(), - )); - } - vec![ConstantData::Str { value: "*".into() }] - } else { - names - .iter() - .map(|n| ConstantData::Str { - value: n.name.as_str().into(), - }) - .collect() - }; + let from_list = names + .iter() + .map(|n| ConstantData::Str { + value: n.name.as_str().into(), + }) + .collect(); // from .... import (*fromlist) self.emit_load_const(ConstantData::Integer { @@ -2804,7 +3486,7 @@ impl Compiler { // In interactive mode, always compile (to print the result). let dominated_by_interactive = self.interactive && !self.ctx.in_func() && !self.ctx.in_class; - if !dominated_by_interactive && Self::is_const_expression(value) { + if !dominated_by_interactive && value.is_constant() { emit!(self, Instruction::Nop); } else { self.compile_expression(value)?; @@ -2832,13 +3514,17 @@ impl Compiler { .. }) => { self.enter_conditional_block(); - self.compile_if(test, body, elif_else_clauses, test.range())?; + self.compile_if(test, body, elif_else_clauses, statement.range())?; self.leave_conditional_block(); self.set_source_range(statement.range()); } ast::Stmt::While(ast::StmtWhile { - test, body, orelse, .. - }) => self.compile_while(test, body, orelse)?, + test, + body, + orelse, + range, + .. + }) => self.compile_while(test, body, orelse, *range)?, ast::Stmt::With(ast::StmtWith { items, body, @@ -2930,13 +3616,15 @@ impl Compiler { arguments.as_deref(), false, )?, - ast::Stmt::Assert(ast::StmtAssert { - test, msg, range, .. - }) => { + ast::Stmt::Assert(assert_stmt) => { + let ast::StmtAssert { + test, msg, range, .. + } = assert_stmt; + self.check_assert(assert_stmt)?; // if some flag, ignore all assert statements! if self.opts.optimize == 0 { let after_block = self.new_block(); - self.compile_jump_if(test, true, after_block)?; + self.compile_jump_if_inner(test, true, after_block, Some(*range))?; self.set_source_range(*range); emit!( self, @@ -2994,7 +3682,13 @@ impl Compiler { statement.range(), )); } - let folded_constant = if v.is_constant() { + let debug_constant = matches!( + &**v, + ast::Expr::Name(ast::ExprName { id, ctx, .. }) + if matches!(ctx, ast::ExprContext::Load) + && id.as_str() == "__debug__" + ); + let folded_constant = if self.is_constant_expr(v) || debug_constant { self.try_fold_constant_expr(v)? } else { None @@ -3021,18 +3715,17 @@ impl Compiler { let unwind_loc = self.unwind_fblock_stack(preserve_tos, false)?; if let Some(loc) = unwind_loc { self.set_source_range(loc); - } - match folded_constant { - Some(constant) if unwind_loc.is_none() => { - self.emit_return_const_no_location(constant); - } - Some(constant) => { - self.emit_load_const(constant); - self.emit_return_value(); + match folded_constant { + Some(constant) => self.emit_return_const(constant), + None => { + self.emit_return_value(); + } } - None => { - self.emit_return_value(); - if unwind_loc.is_none() { + } else { + match folded_constant { + Some(constant) => self.emit_return_const_no_location(constant), + None => { + self.emit_return_value(); self.set_no_location(); } } @@ -3059,11 +3752,7 @@ impl Compiler { range, .. }) => { - if targets.len() == 1 && Self::is_unpack_assignment_target(&targets[0]) { - self.compile_expression_without_const_collection_folding(value)?; - } else { - self.compile_expression(value)?; - } + self.compile_expression(value)?; for (i, target) in targets.iter().enumerate() { if i + 1 != targets.len() { @@ -3075,7 +3764,7 @@ impl Compiler { } ast::Stmt::AugAssign(ast::StmtAugAssign { target, op, value, .. - }) => self.compile_augassign(target, op, value)?, + }) => self.compile_augassign(target, *op, value)?, ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, annotation, @@ -3120,6 +3809,7 @@ impl Compiler { let name_string = name.id.to_string(); if let Some(type_params) = type_params { + self.set_source_range(*range); self.push_symbol_table()?; let key = self.symbol_table_stack.len() - 1; let lineno = self.get_source_line_number().get().to_u32(); @@ -3134,11 +3824,13 @@ impl Compiler { in_async_scope: false, }; + self.set_source_range(*range); self.emit_load_const(ConstantData::Str { value: name_string.clone().into(), }); self.compile_type_params(type_params)?; self.compile_typealias_value_closure(&name_string, value, *range)?; + self.set_source_range(*range); emit!(self, Instruction::BuildTuple { count: 3 }); emit!( self, @@ -3150,15 +3842,19 @@ impl Compiler { let code = self.exit_scope(); self.ctx = prev_ctx; + self.set_source_range(*range); self.make_closure(code, bytecode::MakeFunctionFlags::new())?; + self.set_source_range(*range); emit!(self, Instruction::PushNull); emit!(self, Instruction::Call { argc: 0 }); } else { + self.set_source_range(*range); self.emit_load_const(ConstantData::Str { value: name_string.clone().into(), }); self.emit_load_const(ConstantData::None); self.compile_typealias_value_closure(&name_string, value, *range)?; + self.set_source_range(*range); emit!(self, Instruction::BuildTuple { count: 3 }); emit!( self, @@ -3168,9 +3864,15 @@ impl Compiler { ); } + self.set_source_range(*range); self.store_name(&name_string)?; } - ast::Stmt::IpyEscapeCommand(_) => todo!(), + ast::Stmt::IpyEscapeCommand(stmt) => { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError("invalid syntax".to_owned()), + stmt.range, + )); + } } Ok(()) } @@ -3216,21 +3918,10 @@ impl Compiler { } fn enter_function(&mut self, name: &str, parameters: &ast::Parameters) -> CompileResult<()> { - // TODO: partition_in_place - let mut kw_without_defaults = vec![]; - let mut kw_with_defaults = vec![]; - for kwonlyarg in ¶meters.kwonlyargs { - if let Some(default) = &kwonlyarg.default { - kw_with_defaults.push((&kwonlyarg.parameter, default)); - } else { - kw_without_defaults.push(&kwonlyarg.parameter); - } - } - self.push_output( bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED, parameters.posonlyargs.len().to_u32(), - (parameters.posonlyargs.len() + parameters.args.len()).to_u32(), + parameters.args.len().to_u32(), parameters.kwonlyargs.len().to_u32(), name, )?; @@ -3239,8 +3930,7 @@ impl Compiler { .chain(¶meters.posonlyargs) .chain(¶meters.args) .map(|arg| &arg.parameter) - .chain(kw_without_defaults) - .chain(kw_with_defaults.into_iter().map(|(arg, _)| arg)); + .chain(parameters.kwonlyargs.iter().map(|arg| &arg.parameter)); for name in args_iter { self.varname(name.name.as_str()); } @@ -3296,7 +3986,7 @@ impl Compiler { let lineno = self.get_source_line_number().get().to_u32(); // Enter scope with the type parameter name - self.enter_scope(name, CompilerScope::Annotation, key, lineno)?; + self.enter_scope(name, CompilerScope::TypeVariable, key, lineno)?; self.current_code_info() .metadata @@ -3327,6 +4017,7 @@ impl Compiler { // Return value self.set_source_range(expr_range); emit!(self, Instruction::ReturnValue); + self.emit_return_const_no_location(ConstantData::None); // Exit scope and create closure let code = self.exit_scope(); @@ -3355,7 +4046,7 @@ impl Compiler { self.push_symbol_table()?; let key = self.symbol_table_stack.len() - 1; let lineno = self.get_source_line_number().get().to_u32(); - self.enter_scope(alias_name, CompilerScope::Annotation, key, lineno)?; + self.enter_scope(alias_name, CompilerScope::TypeAlias, key, lineno)?; self.current_code_info() .metadata .varnames @@ -3387,6 +4078,7 @@ impl Compiler { /// Store each type parameter so it is accessible to the current scope, and leave a tuple of /// all the type parameters on the stack. Handles default values per PEP 695. fn compile_type_params(&mut self, type_params: &ast::TypeParams) -> CompileResult<()> { + let mut seen_default = false; // First, compile each type parameter and store it for type_param in &type_params.type_params { match type_param { @@ -3422,6 +4114,7 @@ impl Compiler { } if let Some(default_expr) = default { + seen_default = true; self.compile_type_param_bound_or_default( default_expr, name.as_str(), @@ -3434,6 +4127,13 @@ impl Compiler { func: bytecode::IntrinsicFunction2::SetTypeparamDefault } ); + } else if seen_default { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "non-default type parameter '{name}' follows default type parameter" + )), + *range, + )); } self.set_source_range(*range); @@ -3458,6 +4158,7 @@ impl Compiler { ); if let Some(default_expr) = default { + seen_default = true; self.compile_type_param_bound_or_default( default_expr, name.as_str(), @@ -3470,6 +4171,13 @@ impl Compiler { func: bytecode::IntrinsicFunction2::SetTypeparamDefault } ); + } else if seen_default { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "non-default type parameter '{name}' follows default type parameter" + )), + *range, + )); } self.set_source_range(*range); @@ -3507,6 +4215,14 @@ impl Compiler { func: bytecode::IntrinsicFunction2::SetTypeparamDefault } ); + seen_default = true; + } else if seen_default { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "non-default type parameter '{name}' follows default type parameter" + )), + *range, + )); } self.set_source_range(*range); @@ -3561,7 +4277,6 @@ impl Compiler { if handlers.is_empty() { self.compile_statements(body)?; - self.compile_statements(orelse)?; } else { self.compile_try_except_no_finally(body, handlers, orelse)?; } @@ -3570,7 +4285,7 @@ impl Compiler { self.set_no_location(); self.pop_fblock_label(FBlockType::FinallyTry, body_label); - let sub_table_cursor = self.symbol_table_stack.last().map(|t| t.next_sub_table); + let symbol_table_cursors = self.current_symbol_table_cursors(); self.compile_statements(finalbody)?; emit!( @@ -3579,11 +4294,7 @@ impl Compiler { ); self.set_no_location(); - if let Some(cursor) = sub_table_cursor - && let Some(current_table) = self.symbol_table_stack.last_mut() - { - current_table.next_sub_table = cursor; - } + self.set_symbol_table_cursors(symbol_table_cursors); self.use_cpython_label_block(finally_except_block); emit!( @@ -3647,7 +4358,16 @@ impl Compiler { self.pop_fblock_label(FBlockType::TryExcept, body_label); emit!(self, PseudoInstruction::PopBlock); self.set_no_location(); + + // The symtable stores child scopes in AST visit order + // (body, handlers, orelse), while codegen_try_except() emits orelse + // before the exception handlers. Keep the symbol table in symtable order + // and only move the codegen cursor while compiling orelse. + let handler_symbol_table_cursors = self.current_symbol_table_cursors(); + self.consume_skipped_nested_scopes_in_except_handlers(handlers)?; self.compile_statements(orelse)?; + let after_orelse_symbol_table_cursors = self.current_symbol_table_cursors(); + self.set_symbol_table_cursors(handler_symbol_table_cursors); emit!( self, PseudoInstruction::JumpNoInterrupt { delta: end_block } @@ -3766,13 +4486,14 @@ impl Compiler { self.use_cpython_label_block(next_handler); } + self.set_symbol_table_cursors(after_orelse_symbol_table_cursors); - emit!(self, Instruction::Reraise { depth: 0 }); - self.set_no_location(); self.pop_fblock_label( FBlockType::ExceptionHandler, ir::InstructionSequenceLabel::NO_LABEL, ); + emit!(self, Instruction::Reraise { depth: 0 }); + self.set_no_location(); self.use_cpython_label_block(cleanup_block); emit!(self, Instruction::Copy { i: 3 }); @@ -3828,7 +4549,7 @@ impl Compiler { self.set_no_location(); self.pop_fblock_label(FBlockType::FinallyTry, body_label); - let sub_table_cursor = self.symbol_table_stack.last().map(|t| t.next_sub_table); + let symbol_table_cursors = self.current_symbol_table_cursors(); self.compile_statements(finalbody)?; emit!( @@ -3837,11 +4558,7 @@ impl Compiler { ); self.set_no_location(); - if let Some(cursor) = sub_table_cursor - && let Some(current_table) = self.symbol_table_stack.last_mut() - { - current_table.next_sub_table = cursor; - } + self.set_symbol_table_cursors(symbol_table_cursors); self.use_cpython_label_block(finally_except_block); emit!( @@ -3908,9 +4625,9 @@ impl Compiler { FBlockDatum::None, )?; self.compile_statements(body)?; + self.pop_fblock_label(FBlockType::TryExcept, body_label); emit!(self, PseudoInstruction::PopBlock); self.set_no_location(); - self.pop_fblock_label(FBlockType::TryExcept, body_label); emit!( self, PseudoInstruction::JumpNoInterrupt { delta: else_block } @@ -3965,30 +4682,29 @@ impl Compiler { emit!(self, Instruction::Copy { i: 2 }); } - // Compile exception type + // Compile exception type. The public-AST validator allows a + // NULL type here, so codegen only emits CHECK_EG_MATCH when present. if let Some(exc_type) = type_ { self.compile_expression(exc_type)?; self.set_source_range(*handler_range); - } else { - return Err(self.error(CodegenErrorType::SyntaxError( - "except* must specify an exception type".to_owned(), - ))); } - // Stack: [prev_exc, orig, list, rest, type] - // ADDOP(c, loc, CHECK_EG_MATCH); - emit!(self, Instruction::CheckEgMatch); - // Stack: [prev_exc, orig, list, new_rest, match] + if type_.is_some() { + // Stack: [prev_exc, orig, list, rest, type] + // ADDOP(c, loc, CHECK_EG_MATCH); + emit!(self, Instruction::CheckEgMatch); + // Stack: [prev_exc, orig, list, new_rest, match] - // ADDOP_I(c, loc, COPY, 1); - // ADDOP_JUMP(c, loc, POP_JUMP_IF_NONE, no_match); - emit!(self, Instruction::Copy { i: 1 }); - emit!( - self, - Instruction::PopJumpIfNone { - delta: no_match_block - } - ); + // ADDOP_I(c, loc, COPY, 1); + // ADDOP_JUMP(c, loc, POP_JUMP_IF_NONE, no_match); + emit!(self, Instruction::Copy { i: 1 }); + emit!( + self, + Instruction::PopJumpIfNone { + delta: no_match_block + } + ); + } // Handler matched // Stack: [prev_exc, orig, list, new_rest, match] @@ -4028,9 +4744,9 @@ impl Compiler { self.compile_statements(body)?; // Handler body completed normally + self.pop_fblock_label(FBlockType::HandlerCleanup, cleanup_body_label); emit!(self, PseudoInstruction::PopBlock); self.set_no_location(); - self.pop_fblock_label(FBlockType::HandlerCleanup, cleanup_body_label); // Cleanup name binding if let Some(alias) = name { @@ -4327,19 +5043,8 @@ impl Compiler { // Compile body statements self.compile_statements(body)?; - // Emit implicit `return None` if the body doesn't end with return. - // Also ensure None is in co_consts even when not emitting return - // (matching CPython: functions without explicit constants always - // have None in co_consts). - match body.last() { - Some(ast::Stmt::Return(_)) => {} - _ => { - self.emit_return_const_no_location(ConstantData::None); - } - } - // Functions with no other constants should still have None in co_consts - if self.current_code_info().metadata.consts.is_empty() { - self.arg_constant(ConstantData::None); + if stop_iteration_block.is_some() { + self.emit_return_const_no_location(ConstantData::None); } // Close StopIteration handler and emit handler code @@ -4356,6 +5061,7 @@ impl Compiler { emit!(self, Instruction::Reraise { depth: 1u32 }); self.set_no_location(); } + self.emit_return_const_no_location(ConstantData::None); // Exit scope and create function object let code = self.exit_scope(); @@ -4374,7 +5080,7 @@ impl Compiler { /// Compile function annotations as a closure (PEP 649) /// Returns true if an __annotate__ closure was created - /// Uses symbol table's annotation_block for proper scoping. + /// Uses the matching annotation symbol table for proper scoping. fn compile_annotations_closure( &mut self, func_name: &str, @@ -4382,21 +5088,19 @@ impl Compiler { returns: Option<&ast::Expr>, func_range: TextRange, ) -> CompileResult { - let has_signature_annotations = parameters - .args - .iter() - .map(|x| &x.parameter) - .chain(parameters.posonlyargs.iter().map(|x| &x.parameter)) - .chain(parameters.vararg.as_deref()) - .chain(parameters.kwonlyargs.iter().map(|x| &x.parameter)) - .chain(parameters.kwarg.as_deref()) - .any(|param| param.annotation.is_some()) - || returns.is_some(); - if !has_signature_annotations { + if !self.next_function_annotation_symbol_table_uses_annotations() { + // CPython creates a hidden AnnotationBlock for every function + // signature under `from __future__ import annotations`, including + // an unannotated one. It still belongs to this function: consume + // it so the next function sees its own block rather than remaining + // pinned to this unused entry. + if self.push_annotation_symbol_table() { + self.pop_annotation_symbol_table(); + } return Ok(false); } - // Try to enter annotation scope - returns None if no annotation_block exists + // Try to enter annotation scope - returns None if no matching symbol table exists. let Some(saved_ctx) = self.enter_annotation_scope(func_name, func_range)? else { return Ok(false); }; @@ -4454,6 +5158,7 @@ impl Compiler { } ); emit!(self, Instruction::ReturnValue); + self.emit_return_const_no_location(ConstantData::None); // Exit the annotation scope and get the code object let annotate_code = self.exit_annotation_scope(saved_ctx); @@ -4469,27 +5174,39 @@ impl Compiler { /// (including nested conditional blocks). This preserves the same walk /// order as symbol-table construction so the annotation scope's /// `sub_tables` cursor stays aligned. - fn collect_annotations(body: &[ast::Stmt]) -> Vec<&ast::StmtAnnAssign> { - fn walk<'a>(stmts: &'a [ast::Stmt], out: &mut Vec<&'a ast::StmtAnnAssign>) { + fn collect_annotations( + body: &[ast::Stmt], + parent_scope_type: CompilerScope, + ) -> Vec<(&ast::StmtAnnAssign, bool)> { + fn walk<'a>( + stmts: &'a [ast::Stmt], + out: &mut Vec<(&'a ast::StmtAnnAssign, bool)>, + in_conditional_block: bool, + module_scope: bool, + ) { for stmt in stmts { match stmt { - ast::Stmt::AnnAssign(stmt) => out.push(stmt), + ast::Stmt::AnnAssign(stmt) => { + out.push((stmt, module_scope || in_conditional_block)); + } ast::Stmt::If(ast::StmtIf { body, elif_else_clauses, .. }) => { - walk(body, out); + walk(body, out, true, module_scope); for clause in elif_else_clauses { - walk(&clause.body, out); + walk(&clause.body, out, true, module_scope); } } ast::Stmt::For(ast::StmtFor { body, orelse, .. }) | ast::Stmt::While(ast::StmtWhile { body, orelse, .. }) => { - walk(body, out); - walk(orelse, out); + walk(body, out, true, module_scope); + walk(orelse, out, true, module_scope); + } + ast::Stmt::With(ast::StmtWith { body, .. }) => { + walk(body, out, true, module_scope); } - ast::Stmt::With(ast::StmtWith { body, .. }) => walk(body, out), ast::Stmt::Try(ast::StmtTry { body, handlers, @@ -4497,19 +5214,19 @@ impl Compiler { finalbody, .. }) => { - walk(body, out); + walk(body, out, true, module_scope); for handler in handlers { let ast::ExceptHandler::ExceptHandler( ast::ExceptHandlerExceptHandler { body, .. }, ) = handler; - walk(body, out); + walk(body, out, true, module_scope); } - walk(orelse, out); - walk(finalbody, out); + walk(orelse, out, true, module_scope); + walk(finalbody, out, true, module_scope); } ast::Stmt::Match(ast::StmtMatch { cases, .. }) => { for case in cases { - walk(&case.body, out); + walk(&case.body, out, true, module_scope); } } _ => {} @@ -4517,7 +5234,12 @@ impl Compiler { } } let mut annotations = Vec::new(); - walk(body, &mut annotations); + walk( + body, + &mut annotations, + false, + parent_scope_type == CompilerScope::Module, + ); annotations } @@ -4536,10 +5258,12 @@ impl Compiler { loc: Option, ) -> CompileResult { let loc = loc.unwrap_or(self.current_source_range); - let annotations = Self::collect_annotations(body); + // Get parent scope type BEFORE pushing annotation symbol table. + let parent_scope_type = self.current_symbol_table().typ; + let annotations = Self::collect_annotations(body, parent_scope_type); let simple_annotation_count = annotations .iter() - .filter(|stmt| stmt.simple && matches!(stmt.target.as_ref(), ast::Expr::Name(_))) + .filter(|(stmt, _)| stmt.simple && matches!(stmt.target.as_ref(), ast::Expr::Name(_))) .count(); if simple_annotation_count == 0 { @@ -4549,8 +5273,6 @@ impl Compiler { // Check if we have conditional annotations let has_conditional = self.current_symbol_table().has_conditional_annotations; - // Get parent scope type BEFORE pushing annotation symbol table - let parent_scope_type = self.current_symbol_table().typ; // Try to push annotation symbol table from current scope if !self.push_current_annotation_symbol_table() { return Ok(false); @@ -4575,11 +5297,12 @@ impl Compiler { lineno.to_u32(), )?; - // Add 'format' parameter to varnames + // Keep the internal ".format" name; the final code object + // exposes this parameter as "format". self.current_code_info() .metadata .varnames - .insert("format".to_owned()); + .insert(".format".to_owned()); // Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError self.emit_format_validation(); @@ -4587,8 +5310,8 @@ impl Compiler { self.set_source_range(loc); emit!(self, Instruction::BuildMap { count: 0 }); - let mut simple_idx = 0usize; - for stmt in annotations { + let mut conditional_idx = 0usize; + for (stmt, is_conditional) in annotations { let ast::StmtAnnAssign { target, annotation, @@ -4612,16 +5335,17 @@ impl Compiler { continue; } - let not_set_block = has_conditional.then(|| self.new_block()); - let not_set_label = - (!has_conditional).then(|| self.current_code_info().new_instr_sequence_label()); + let not_set_block = (has_conditional && is_conditional).then(|| self.new_block()); + let not_set_label = (!has_conditional || !is_conditional) + .then(|| self.current_code_info().new_instr_sequence_label()); let name = simple_name.expect("missing simple annotation name"); - if has_conditional { + if let Some(not_set_block) = not_set_block { self.set_source_range(*range); self.emit_load_const(ConstantData::Integer { - value: simple_idx.into(), + value: conditional_idx.into(), }); + conditional_idx += 1; if parent_scope_type == CompilerScope::Class { let idx = self.get_free_var_index("__conditional_annotations__"); emit!(self, Instruction::LoadDeref { i: idx }); @@ -4638,7 +5362,7 @@ impl Compiler { emit!( self, Instruction::PopJumpIfFalse { - delta: not_set_block.expect("missing not_set block") + delta: not_set_block } ); } @@ -4651,7 +5375,6 @@ impl Compiler { }); self.set_source_range(loc); emit!(self, Instruction::StoreSubscr); - simple_idx += 1; if let Some(not_set_block) = not_set_block { self.use_cpython_label_block(not_set_block); @@ -4665,6 +5388,7 @@ impl Compiler { self.set_source_range(loc); emit!(self, Instruction::ReturnValue); + self.emit_return_const_no_location(ConstantData::None); // Exit annotation scope - pop symbol table, restore to parent's annotation_block, and get code let annotation_table = self.pop_symbol_table(); @@ -4677,10 +5401,11 @@ impl Compiler { self.ctx = saved_ctx; // Exit code scope let pop = self.code_stack.pop(); - let annotate_code = unwrap_internal( + let mut annotate_code = unwrap_internal( self, compiler_unwrap_option(self, pop).finalize_code(&self.opts), ); + Self::expose_annotation_format_parameter(&mut annotate_code); // Make a closure from the code object self.set_source_range(loc); @@ -4720,14 +5445,25 @@ impl Compiler { if is_async { "async def " } else { "def " }, ); + // The symtable visits defaults before decorators, but + // codegen_function() emits decorators first. Keep the symbol table in + // symtable order and only move the codegen cursor while compiling + // decorators. + let defaults_symbol_table_cursors = self.current_symbol_table_cursors(); + self.consume_skipped_nested_scopes_in_parameter_defaults(parameters)?; self.prepare_decorators(decorator_list)?; + let after_decorators_symbol_table_cursors = self.current_symbol_table_cursors(); + self.set_symbol_table_cursors(defaults_symbol_table_cursors); + + // The first decorator line is used for code objects created by + // this definition, but LOC(s) for the surrounding instructions. + let firstlineno_range = decorator_list + .first() + .map_or(stmt_source_range, |decorator| decorator.expression.range()); // compile defaults and return funcflags let funcflags = self.compile_default_arguments(parameters, def_source_range)?; - - // Restore the `def` line range so that enter_function → push_output → get_source_line_number() - // records the `def` keyword's line as co_firstlineno, not the last default-argument line. - self.set_source_range(def_source_range); + self.set_symbol_table_cursors(after_decorators_symbol_table_cursors); let is_generic = type_params.is_some(); let mut num_typeparam_args = 0u32; @@ -4744,11 +5480,13 @@ impl Compiler { num_typeparam_args += 1; } if num_typeparam_args == 2 { + self.set_source_range(def_source_range); emit!(self, Instruction::Swap { i: 2 }); } // Enter type params scope let type_params_name = format!(""); + self.set_source_range(firstlineno_range); self.push_output( bytecode::CodeFlags::OPTIMIZED | bytecode::CodeFlags::NEWLOCALS, 0, @@ -4784,6 +5522,7 @@ impl Compiler { self.compile_type_params(type_params.unwrap())?; // Load defaults/kwdefaults with LOAD_FAST + self.set_source_range(def_source_range); for i in 0..num_typeparam_args { let var_num = oparg::VarNum::from(i); emit!(self, Instruction::LoadFast { var_num }); @@ -4796,8 +5535,9 @@ impl Compiler { annotations_flag.insert(bytecode::MakeFunctionFlag::Annotate); } - // Compile function body - self.set_source_range(stmt_source_range); + // Compile function body. codegen_function() uses the first + // decorator line for co_firstlineno, but LOC(s) for MAKE_FUNCTION. + self.set_source_range(firstlineno_range); let final_funcflags = funcflags | annotations_flag; self.compile_function_body( name, @@ -4812,9 +5552,11 @@ impl Compiler { if is_generic { // SWAP to get function on top // Stack: [type_params_tuple, function] -> [function, type_params_tuple] + self.set_source_range(def_source_range); emit!(self, Instruction::Swap { i: 2 }); // Call INTRINSIC_SET_FUNCTION_TYPE_PARAMS + self.set_source_range(def_source_range); emit!( self, Instruction::CallIntrinsic2 { @@ -4824,6 +5566,7 @@ impl Compiler { // Return the function object from type params scope emit!(self, Instruction::ReturnValue); + self.set_no_location(); // Set argcount for type params scope self.current_code_info().metadata.argcount = num_typeparam_args; @@ -4834,15 +5577,18 @@ impl Compiler { self.ctx = saved_ctx; // Make closure for type params code + self.set_source_range(def_source_range); self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?; if num_typeparam_args > 0 { + self.set_source_range(def_source_range); emit!( self, Instruction::Swap { i: num_typeparam_args + 1 } ); + self.set_source_range(def_source_range); emit!( self, Instruction::Call { @@ -4851,8 +5597,10 @@ impl Compiler { ); } else { // Stack: [closure] + self.set_source_range(def_source_range); emit!(self, Instruction::PushNull); // Stack: [closure, NULL] + self.set_source_range(def_source_range); emit!(self, Instruction::Call { argc: 0 }); } } @@ -4889,7 +5637,7 @@ impl Compiler { Some(symbol) => match symbol.scope { SymbolScope::Cell => Ok(SymbolScope::Cell), SymbolScope::Free => Ok(SymbolScope::Free), - _ if symbol.flags.contains(SymbolFlags::FREE_CLASS) => Ok(SymbolScope::Free), + _ if symbol.flags.contains(SymbolFlags::DEF_FREE_CLASS) => Ok(SymbolScope::Free), _ => Err(CodegenErrorType::SyntaxError(format!( "get_ref_type: invalid scope for '{name}'" ))), @@ -5035,16 +5783,6 @@ impl Compiler { ); } - // Set type_params if present - if flags.contains(&bytecode::MakeFunctionFlag::TypeParams) { - emit!( - self, - Instruction::SetFunctionAttribute { - flag: bytecode::MakeFunctionFlag::TypeParams - } - ); - } - Ok(()) } @@ -5067,55 +5805,6 @@ impl Compiler { } } - // Python/compile.c find_ann - fn find_ann(body: &[ast::Stmt]) -> bool { - for statement in body { - let res = match &statement { - ast::Stmt::AnnAssign(_) => true, - ast::Stmt::For(ast::StmtFor { body, orelse, .. }) => { - Self::find_ann(body) || Self::find_ann(orelse) - } - ast::Stmt::If(ast::StmtIf { - body, - elif_else_clauses, - .. - }) => { - Self::find_ann(body) - || elif_else_clauses.iter().any(|x| Self::find_ann(&x.body)) - } - ast::Stmt::While(ast::StmtWhile { body, orelse, .. }) => { - Self::find_ann(body) || Self::find_ann(orelse) - } - ast::Stmt::With(ast::StmtWith { body, .. }) => Self::find_ann(body), - ast::Stmt::Match(ast::StmtMatch { cases, .. }) => { - cases.iter().any(|case| Self::find_ann(&case.body)) - } - ast::Stmt::Try(ast::StmtTry { - body, - handlers, - orelse, - finalbody, - .. - }) => { - Self::find_ann(body) - || handlers.iter().any(|h| { - let ast::ExceptHandler::ExceptHandler( - ast::ExceptHandlerExceptHandler { body, .. }, - ) = h; - Self::find_ann(body) - }) - || Self::find_ann(orelse) - || Self::find_ann(finalbody) - } - _ => false, - }; - if res { - return true; - } - } - false - } - /// Compile the class body into a code object // = compiler_class_body fn compile_class_body( @@ -5127,7 +5816,7 @@ impl Compiler { ) -> CompileResult { // 1. Enter class scope let key = self.symbol_table_stack.len(); - self.push_symbol_table()?; + self.push_symbol_table_matching(CompilerScope::Class, name)?; self.enter_scope(name, CompilerScope::Class, key, firstlineno)?; // Set qualname using the new method @@ -5173,15 +5862,14 @@ impl Compiler { } // Handle class annotation bookkeeping in CPython order. - if Self::find_ann(body) { - if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { - emit!(self, Instruction::BuildSet { count: 0 }); - self.store_name("__conditional_annotations__")?; - } + let annotations_used = self.current_symbol_table().annotations_used; + if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) { + emit!(self, Instruction::BuildSet { count: 0 }); + self.store_name("__conditional_annotations__")?; + } - if self.future_annotations { - emit!(self, Instruction::SetupAnnotations); - } + if self.future_annotations && annotations_used { + emit!(self, Instruction::SetupAnnotations); } // Store __doc__ only if there's an explicit docstring. @@ -5190,13 +5878,14 @@ impl Compiler { self.set_source_range(range); self.emit_load_const(ConstantData::Str { value: doc.into() }); self.store_name("__doc__")?; + self.set_no_location(); self.set_source_range(saved_range); } // 3. Compile the class body self.compile_statements(body)?; - if Self::find_ann(body) && !self.future_annotations { + if annotations_used && !self.future_annotations { self.compile_module_annotate(body, Some(class_body_prefix_range))?; } @@ -5261,6 +5950,7 @@ impl Compiler { // Return the class namespace self.emit_return_value(); self.set_no_location(); + self.emit_return_const_no_location(ConstantData::None); // Exit scope and return the code object Ok(self.exit_scope()) @@ -5283,6 +5973,9 @@ impl Compiler { self.prepare_decorators(decorator_list)?; let is_generic = type_params.is_some(); + let firstlineno_range = decorator_list + .first() + .map_or(stmt_source_range, |decorator| decorator.expression.range()); #[expect(clippy::map_unwrap_or, reason = "Changing this will not compile")] let firstlineno = decorator_list .first() @@ -5301,6 +5994,7 @@ impl Compiler { // Step 1: If generic, enter type params scope and compile type params if is_generic { let type_params_name = format!(""); + self.set_source_range(firstlineno_range); self.push_output( bytecode::CodeFlags::OPTIMIZED | bytecode::CodeFlags::NEWLOCALS, 0, @@ -5333,7 +6027,10 @@ impl Compiler { in_class: true, in_async_scope: false, }; + let pre_class_body_symbol_table_cursors = self.current_symbol_table_cursors(); let class_code = self.compile_class_body(name, body, type_params, firstlineno)?; + let post_class_body_symbol_table_cursors = self.current_symbol_table_cursors(); + self.set_symbol_table_cursors(pre_class_body_symbol_table_cursors); self.ctx = prev_ctx; self.set_source_range(class_source_range); @@ -5361,150 +6058,37 @@ impl Compiler { self.set_source_range(class_source_range); self.store_name(".generic_base")?; - // Compile bases and call __build_class__ - // Check for starred bases or **kwargs - let has_starred = arguments.is_some_and(|args| { - args.args - .iter() - .any(|arg| matches!(arg, ast::Expr::Starred(_))) + let (bases, keywords) = arguments.map_or((&[][..], &[][..]), |args| { + (&args.args[..], &args.keywords[..]) }); - let has_double_star = - arguments.is_some_and(|args| args.keywords.iter().any(|kw| kw.arg.is_none())); - - if has_starred { - // Use CallFunctionEx for *bases or **kwargs - // Stack has: [__build_class__, NULL, class_func, name] - // Need to build: args tuple = (class_func, name, *bases, .generic_base) - - // Build a list starting with class_func and name (2 elements already on stack) - emit!(self, Instruction::BuildList { count: 2 }); - - // Add bases to the list - if let Some(arguments) = arguments { - for arg in &arguments.args { - if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = arg { - // Starred: compile and extend - self.compile_expression(value)?; - emit!(self, Instruction::ListExtend { i: 1 }); - } else { - // Non-starred: compile and append - self.compile_expression(arg)?; - emit!(self, Instruction::ListAppend { i: 1 }); - } - } - } - - // Add .generic_base as final element - self.set_source_range(class_source_range); - self.load_name(".generic_base")?; - self.set_source_range(class_source_range); - emit!(self, Instruction::ListAppend { i: 1 }); - - // Convert list to tuple - self.set_source_range(class_source_range); - emit!( - self, - Instruction::CallIntrinsic1 { - func: IntrinsicFunction1::ListToTuple - } - ); + self.codegen_call_helper_impl( + 2, + bases, + keywords, + class_source_range, + None, + Some(".generic_base"), + )?; - self.compile_call_function_ex_keywords( - arguments.map_or(&[][..], |args| &args.keywords[..]), - class_source_range, - )?; - emit!(self, Instruction::CallFunctionEx); - } else if has_double_star { - if let Some(arguments) = arguments { - for arg in &arguments.args { - self.compile_expression(arg)?; - } - } - self.set_source_range(class_source_range); - self.load_name(".generic_base")?; - self.set_source_range(class_source_range); - emit!( - self, - Instruction::BuildTuple { - count: 3 + arguments - .map_or(0, |args| u32::try_from(args.args.len()).unwrap()) - } - ); - self.compile_call_function_ex_keywords( - &arguments.unwrap().keywords[..], - class_source_range, - )?; - emit!(self, Instruction::CallFunctionEx); - } else { - // Simple case: no starred bases, no **kwargs - // Compile bases normally - let base_count = if let Some(arguments) = arguments { - for arg in &arguments.args { - self.compile_expression(arg)?; - } - arguments.args.len() - } else { - 0 - }; + // Return the created class + self.emit_return_value(); + self.set_no_location(); - // Load .generic_base as the last base - self.set_source_range(class_source_range); - self.load_name(".generic_base")?; + // Exit type params scope and wrap in function + let type_params_code = self.exit_scope(); + self.ctx = saved_ctx; - let nargs = 2 + u32::try_from(base_count).expect("too many base classes") + 1; - - // Handle keyword arguments (no **kwargs here) - if let Some(arguments) = arguments - && !arguments.keywords.is_empty() - { - let mut kwarg_names = vec![]; - for keyword in &arguments.keywords { - let name = keyword.arg.as_ref().expect( - "keyword argument name must be set (no **kwargs in this branch)", - ); - kwarg_names.push(ConstantData::Str { - value: name.as_str().into(), - }); - self.compile_expression(&keyword.value)?; - } - self.set_source_range(class_source_range); - self.emit_load_const(ConstantData::Tuple { - elements: kwarg_names, - }); - self.set_source_range(class_source_range); - emit!( - self, - Instruction::CallKw { - argc: nargs - + u32::try_from(arguments.keywords.len()) - .expect("too many keyword arguments") - } - ); - } else { - self.set_source_range(class_source_range); - emit!(self, Instruction::Call { argc: nargs }); - } - } - - // Return the created class - self.set_source_range(class_source_range); - self.emit_return_value(); - - // Exit type params scope and wrap in function - let type_params_code = self.exit_scope(); - self.ctx = saved_ctx; - - // Execute the type params function - self.set_source_range(class_source_range); - self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?; - self.set_source_range(class_source_range); - emit!(self, Instruction::PushNull); - self.set_source_range(class_source_range); - emit!(self, Instruction::Call { argc: 0 }); - } else { - // Non-generic class: standard path - emit!(self, Instruction::LoadBuildClass); - emit!(self, Instruction::PushNull); + // Execute the type params function + self.set_source_range(class_source_range); + self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?; + self.set_source_range(class_source_range); + emit!(self, Instruction::PushNull); + self.set_source_range(class_source_range); + emit!(self, Instruction::Call { argc: 0 }); + } else { + // Non-generic class: standard path + emit!(self, Instruction::LoadBuildClass); + emit!(self, Instruction::PushNull); // Create class function with closure self.make_closure(class_code, bytecode::MakeFunctionFlags::new())?; @@ -5516,6 +6100,7 @@ impl Compiler { self.set_source_range(class_source_range); emit!(self, Instruction::Call { argc: 2 }); } + self.set_symbol_table_cursors(post_class_body_symbol_table_cursors); } // Step 4: Apply decorators and store (common to both paths) @@ -5534,7 +6119,7 @@ impl Compiler { test: &ast::Expr, body: &[ast::Stmt], elif_else_clauses: &[ast::ElifElseClause], - _stmt_range: TextRange, + stmt_range: TextRange, ) -> CompileResult<()> { let end_block = self.new_block(); let next_block = if elif_else_clauses.is_empty() { @@ -5543,7 +6128,7 @@ impl Compiler { self.new_block() }; - self.compile_jump_if(test, false, next_block)?; + self.compile_jump_if_inner(test, false, next_block, Some(stmt_range))?; self.compile_statements(body)?; let Some((clause, rest)) = elif_else_clauses.split_first() else { @@ -5559,7 +6144,7 @@ impl Compiler { self.use_cpython_label_block(next_block); if let Some(test) = &clause.test { - self.compile_if(test, &clause.body, rest, test.range())?; + self.compile_if(test, &clause.body, rest, clause.range)?; } else { debug_assert!(rest.is_empty()); self.compile_statements(&clause.body)?; @@ -5573,6 +6158,7 @@ impl Compiler { test: &ast::Expr, body: &[ast::Stmt], orelse: &[ast::Stmt], + while_range: TextRange, ) -> CompileResult<()> { self.enter_conditional_block(); @@ -5588,7 +6174,7 @@ impl Compiler { end_label, FBlockDatum::None, )?; - self.compile_jump_if(test, false, anchor_block)?; + self.compile_jump_if_inner(test, false, anchor_block, Some(while_range))?; self.compile_loop_body_statements(body)?; emit!(self, PseudoInstruction::Jump { delta: loop_block }); @@ -5610,7 +6196,17 @@ impl Compiler { is_async: bool, ) -> CompileResult<()> { self.enter_conditional_block(); + let result = self.compile_with_inner(items, body, is_async); + self.leave_conditional_block(); + result + } + fn compile_with_inner( + &mut self, + items: &[ast::WithItem], + body: &[ast::Stmt], + is_async: bool, + ) -> CompileResult<()> { // Python 3.12+ style with statement: // // BEFORE_WITH # TOS: ctx_mgr -> [__exit__, __enter__ result] @@ -5656,7 +6252,9 @@ impl Compiler { emit!(self, Instruction::Copy { i: 1 }); // [cm, cm] if is_async { - if self.ctx.func != FunctionContext::AsyncFunction { + if self.ctx.func != FunctionContext::AsyncFunction + && !self.allows_top_level_await_in_current_context() + { return Err(self.error(CodegenErrorType::InvalidAsyncWith)); } // Load __aexit__ and __aenter__, then call __aenter__ @@ -5741,7 +6339,7 @@ impl Compiler { self.compile_with_body_statements(body)?; } else { self.set_source_range(items[0].context_expr.range()); - self.compile_with(items, body, is_async)?; + self.compile_with_inner(items, body, is_async)?; } // CPython pops the async-with fblock before emitting POP_BLOCK, but @@ -5765,7 +6363,6 @@ impl Compiler { } emit!(self, Instruction::PopTop); // Pop __exit__ result emit!(self, PseudoInstruction::Jump { delta: after_block }); - self.set_no_location(); // ===== Exception handler path ===== // Stack at entry: [..., exit_func, self_exit, lasti, exc] @@ -5795,7 +6392,6 @@ impl Compiler { self.use_cpython_label_block(after_block); - self.leave_conditional_block(); Ok(()) } @@ -5836,10 +6432,12 @@ impl Compiler { } // The thing iterated: - self.compile_for_iterable_expression(iter, is_async)?; + self.compile_expression(iter)?; if is_async { - if self.ctx.func != FunctionContext::AsyncFunction { + if self.ctx.func != FunctionContext::AsyncFunction + && !self.allows_top_level_await_in_current_context() + { return Err(self.error(CodegenErrorType::InvalidAsyncFor)); } self.set_source_range(iter.range()); @@ -5871,6 +6469,7 @@ impl Compiler { self.compile_store(target)?; } else { // Retrieve Iterator + self.set_source_range(iter.range()); emit!(self, Instruction::GetIter); self.use_cpython_label_block(for_block); @@ -5892,12 +6491,19 @@ impl Compiler { emit!(self, PseudoInstruction::Jump { delta: for_block }); self.set_no_location(); + if is_async { + // codegen_async_for() pops the loop fblock before the + // END_ASYNC_FOR exception block. Sync codegen_for() keeps the + // fblock through END_FOR/POP_ITER and pops below. + self.pop_fblock_label(FBlockType::ForLoop, for_label); + } + self.use_cpython_label_block(else_block); // Except block for __anext__ / end of sync for if is_async { // codegen_async_for emits END_ASYNC_FOR at the iterator location, - // then pops the for-loop fblock before the else block. + // after the for-loop fblock has already been popped. let saved_range = self.current_source_range; self.set_source_range(iter.range()); self.emit_end_async_for(end_async_for_target); @@ -5909,9 +6515,8 @@ impl Compiler { self.set_no_location(); emit!(self, Instruction::PopIter); self.set_no_location(); + self.pop_fblock_label(FBlockType::ForLoop, for_label); } - // No PopBlock here - for async, POP_BLOCK is already in for_block - self.pop_fblock_label(FBlockType::ForLoop, for_label); self.compile_statements(orelse)?; self.use_cpython_label_block(after_block); @@ -5923,39 +6528,9 @@ impl Compiler { Ok(()) } - fn compile_for_iterable_expression( - &mut self, - iter: &ast::Expr, - is_async: bool, - ) -> CompileResult<()> { - // Match CPython's iterable lowering for `for`/comprehension fronts: - // a non-starred list literal used only for iteration is emitted as a tuple. - // Skip async-for/async comprehension iteration because GET_AITER expects - // the original object semantics. - if !is_async - && let ast::Expr::List(ast::ExprList { elts, .. }) = iter - && elts.len() <= usize::try_from(STACK_USE_GUIDELINE).unwrap() - && !elts.iter().any(|e| matches!(e, ast::Expr::Starred(_))) - { - for elt in elts { - self.compile_expression(elt)?; - } - self.set_source_range(iter.range()); - emit!( - self, - Instruction::BuildList { - count: u32::try_from(elts.len()).expect("too many elements"), - } - ); - return Ok(()); - } - - self.compile_expression(iter) - } - fn compile_comprehension_iter(&mut self, generator: &ast::Comprehension) -> CompileResult<()> { let saved_range = self.current_source_range; - self.compile_for_iterable_expression(&generator.iter, generator.is_async)?; + self.compile_expression(&generator.iter)?; self.set_source_range(generator.iter.range()); if generator.is_async { emit!(self, Instruction::GetAiter); @@ -5978,24 +6553,6 @@ impl Compiler { } } - fn forbidden_name(&mut self, name: &str, ctx: NameUsage) -> CompileResult { - if ctx == NameUsage::Store && name == "__debug__" { - return Err(self.error(CodegenErrorType::Assign("__debug__"))); - // return Ok(true); - } - if ctx == NameUsage::Delete && name == "__debug__" { - return Err(self.error(CodegenErrorType::Delete("__debug__"))); - // return Ok(true); - } - Ok(false) - } - - fn compile_error_forbidden_name(&mut self, name: &str) -> CodegenError { - self.error(CodegenErrorType::SyntaxError(format!( - "cannot use forbidden name '{name}' in pattern" - ))) - } - /// Ensures that `pc.fail_pop` has at least `n + 1` entries. /// If not, new labels are generated and pushed until the required size is reached. fn ensure_fail_pop(&mut self, pc: &mut PatternContext, n: usize) { @@ -6038,7 +6595,7 @@ impl Compiler { /// Emits the necessary POP instructions for all failure targets in the pattern context, /// then resets the fail_pop vector. - fn emit_and_reset_fail_pop(&mut self, pc: &mut PatternContext) { + fn emit_and_reset_fail_pop(&mut self, pc: &mut PatternContext, loc: TextRange) { // If the fail_pop vector is empty, nothing needs to be done. if pc.fail_pop.is_empty() { debug_assert!(pc.fail_pop.is_empty()); @@ -6049,6 +6606,7 @@ impl Compiler { // CPython emit_and_reset_fail_pop() uses USE_LABEL here. self.use_cpython_label_block(label); // Emit the POP instruction. + self.set_source_range(loc); emit!(self, Instruction::PopTop); } // Finally, use the first label. @@ -6060,7 +6618,7 @@ impl Compiler { } /// Duplicate the effect of Python 3.10's ROT_* instructions using SWAPs. - fn pattern_helper_rotate(&mut self, mut count: usize) { + fn pattern_helper_rotate(&mut self, loc: TextRange, mut count: usize) { // Rotate TOS (top of stack) to position `count` down // This is done by a series of swaps // For count=1, no rotation needed (already at top) @@ -6068,6 +6626,7 @@ impl Compiler { // For count=3, swap TOS with item 2 positions down, then with item 1 position down while count > 1 { // Emit a SWAP instruction with the current count. + self.set_source_range(loc); emit!( self, Instruction::Swap { @@ -6086,32 +6645,30 @@ impl Compiler { /// to the list of captured names. fn pattern_helper_store_name( &mut self, + loc: TextRange, n: Option<&ast::Identifier>, pc: &mut PatternContext, ) -> CompileResult<()> { match n { // If no name is provided, simply pop the top of the stack. None => { + self.set_source_range(loc); emit!(self, Instruction::PopTop); Ok(()) } Some(name) => { - // Check if the name is forbidden for storing. - if self.forbidden_name(name.as_str(), NameUsage::Store)? { - return Err(self.compile_error_forbidden_name(name.as_str())); - } - // Ensure we don't store the same name twice. // TODO: maybe pc.stores should be a set? if pc.stores.contains(&name.to_string()) { - return Err( - self.error(CodegenErrorType::DuplicateStore(name.as_str().to_string())) - ); + return Err(self.error_ranged( + CodegenErrorType::DuplicateStore(name.as_str().to_string()), + loc, + )); } // Calculate how many items to rotate: let rotations = pc.on_top + pc.stores.len() + 1; - self.pattern_helper_rotate(rotations); + self.pattern_helper_rotate(loc, rotations); // Append the name to the captured stores. pc.stores.push(name.to_string()); @@ -6120,30 +6677,51 @@ impl Compiler { } } - fn pattern_unpack_helper(&mut self, elts: &[ast::Pattern]) -> CompileResult<()> { + fn pattern_wildcard_check(pattern: &ast::Pattern) -> bool { + matches!( + pattern, + ast::Pattern::MatchAs(ast::PatternMatchAs { name: None, .. }) + ) + } + + fn pattern_wildcard_star_check(pattern: &ast::Pattern) -> bool { + matches!( + pattern, + ast::Pattern::MatchStar(ast::PatternMatchStar { name: None, .. }) + ) + } + + fn pattern_unpack_helper( + &mut self, + loc: TextRange, + elts: &[ast::Pattern], + ) -> CompileResult<()> { let n = elts.len(); let mut seen_star = false; for (i, elt) in elts.iter().enumerate() { - if elt.is_match_star() { - if !seen_star { - if i >= (1 << 8) || (n - i - 1) >= ((i32::MAX as usize) >> 8) { - todo!(); - // return self.compiler_error(loc, "too many expressions in star-unpacking sequence pattern"); - } - let counts = UnpackExArgs { - before: u8::try_from(i).unwrap(), - after: u8::try_from(n - i - 1).unwrap(), - }; - emit!(self, Instruction::UnpackEx { counts }); - seen_star = true; - } else { - // TODO: Fix error msg - return Err(self.error(CodegenErrorType::MultipleStarArgs)); - // return self.compiler_error(loc, "multiple starred expressions in sequence pattern"); + if elt.is_match_star() && !seen_star { + if i >= (1 << 8) || (n - i - 1) >= ((i32::MAX as usize) >> 8) { + return Err(self.error_ranged( + CodegenErrorType::TooManyExpressionsInStarUnpackingSequencePattern, + loc, + )); } + let counts = UnpackExArgs { + before: u8::try_from(i).unwrap(), + after: u32::try_from(n - i - 1).unwrap(), + }; + self.set_source_range(loc); + emit!(self, Instruction::UnpackEx { counts }); + seen_star = true; + } else if elt.is_match_star() { + return Err(self.error_ranged( + CodegenErrorType::MultipleStarredExpressionsInSequencePattern, + loc, + )); } } if !seen_star { + self.set_source_range(loc); emit!( self, Instruction::UnpackSequence { @@ -6156,12 +6734,13 @@ impl Compiler { fn pattern_helper_sequence_unpack( &mut self, + loc: TextRange, patterns: &[ast::Pattern], _star: Option, pc: &mut PatternContext, ) -> CompileResult<()> { // Unpack the sequence into individual subjects. - self.pattern_unpack_helper(patterns)?; + self.pattern_unpack_helper(loc, patterns)?; let size = patterns.len(); // Increase the on_top counter for the newly unpacked subjects. pc.on_top += size; @@ -6176,6 +6755,7 @@ impl Compiler { fn pattern_helper_sequence_subscr( &mut self, + loc: TextRange, patterns: &[ast::Pattern], star: usize, pc: &mut PatternContext, @@ -6183,35 +6763,32 @@ impl Compiler { // Keep the subject around for extracting elements. pc.on_top += 1; for (i, pattern) in patterns.iter().enumerate() { - let is_true_wildcard = matches!( - pattern, - ast::Pattern::MatchAs(ast::PatternMatchAs { - pattern: None, - name: None, - .. - }) - ); - if is_true_wildcard { + if Self::pattern_wildcard_check(pattern) { continue; } if i == star { // This must be a starred wildcard. - // assert!(pattern.is_star_wildcard()); + debug_assert!(Self::pattern_wildcard_star_check(pattern)); continue; } // Duplicate the subject. + self.set_source_range(loc); emit!(self, Instruction::Copy { i: 1 }); if i < star { // For indices before the star, use a nonnegative index equal to i. + self.set_source_range(loc); self.emit_load_const(ConstantData::Integer { value: i.into() }); } else { // For indices after the star, compute a nonnegative index: // index = len(subject) - (size - i) + self.set_source_range(loc); emit!(self, Instruction::GetLen); + self.set_source_range(loc); self.emit_load_const(ConstantData::Integer { value: (patterns.len() - i).into(), }); // Subtract to compute the correct index. + self.set_source_range(loc); emit!( self, Instruction::BinaryOp { @@ -6220,6 +6797,7 @@ impl Compiler { ); } // Use BINARY_OP/NB_SUBSCR to extract the element. + self.set_source_range(loc); emit!( self, Instruction::BinaryOp { @@ -6231,6 +6809,7 @@ impl Compiler { } // Pop the subject off the stack. pc.on_top -= 1; + self.set_source_range(loc); emit!(self, Instruction::PopTop); Ok(()) } @@ -6259,31 +6838,31 @@ impl Compiler { // If there is no sub-pattern, then it's an irrefutable match. if p.pattern.is_none() { if !pc.allow_irrefutable { - if let Some(_name) = p.name.as_ref() { - // TODO: This error message does not match cpython exactly - // A name capture makes subsequent patterns unreachable. - return Err(self.error(CodegenErrorType::UnreachablePattern( - PatternUnreachableReason::NameCapture, - ))); + if let Some(name) = p.name.as_ref() { + return Err(self.error_ranged( + CodegenErrorType::UnreachableNameCapturePattern(name.to_string()), + p.range, + )); } // A wildcard makes remaining patterns unreachable. - return Err(self.error(CodegenErrorType::UnreachablePattern( - PatternUnreachableReason::Wildcard, - ))); + return Err( + self.error_ranged(CodegenErrorType::UnreachableWildcardPattern, p.range) + ); } // If irrefutable matches are allowed, store the name (if any). - return self.pattern_helper_store_name(p.name.as_ref(), pc); + return self.pattern_helper_store_name(p.range, p.name.as_ref(), pc); } // Otherwise, there is a sub-pattern. Duplicate the object on top of the stack. pc.on_top += 1; + self.set_source_range(p.range); emit!(self, Instruction::Copy { i: 1 }); // Compile the sub-pattern. self.compile_pattern(p.pattern.as_ref().unwrap(), pc)?; // After success, decrement the on_top counter. pc.on_top -= 1; // Store the captured name (if any). - self.pattern_helper_store_name(p.name.as_ref(), pc)?; + self.pattern_helper_store_name(p.range, p.name.as_ref(), pc)?; Ok(()) } @@ -6292,7 +6871,7 @@ impl Compiler { p: &ast::PatternMatchStar, pc: &mut PatternContext, ) -> CompileResult<()> { - self.pattern_helper_store_name(p.name.as_ref(), pc)?; + self.pattern_helper_store_name(p.range, p.name.as_ref(), pc)?; Ok(()) } @@ -6301,21 +6880,19 @@ impl Compiler { fn validate_kwd_attrs( &mut self, attrs: &[ast::Identifier], - _patterns: &[ast::Pattern], + patterns: &[ast::Pattern], ) -> CompileResult<()> { let n_attrs = attrs.len(); for i in 0..n_attrs { let attr = attrs[i].as_str(); - // Check if the attribute name is forbidden in a Store context. - if self.forbidden_name(attr, NameUsage::Store)? { - // Return an error if the name is forbidden. - return Err(self.compile_error_forbidden_name(attr)); - } // Check for duplicates: compare with every subsequent attribute. - for ident in attrs.iter().take(n_attrs).skip(i + 1) { + for (j, ident) in attrs.iter().enumerate().take(n_attrs).skip(i + 1) { let other = ident.as_str(); if attr == other { - return Err(self.error(CodegenErrorType::RepeatedAttributePattern)); + return Err(self.error_ranged( + CodegenErrorType::RepeatedAttributePattern(attr.to_owned()), + patterns[j].range(), + )); } } } @@ -6342,12 +6919,27 @@ impl Compiler { let nargs = patterns.len(); let n_attrs = kwd_attrs.len(); + let n_kwd_patterns = kwd_patterns.len(); + if n_attrs != n_kwd_patterns { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "kwd_attrs ({n_attrs}) / kwd_patterns ({n_kwd_patterns}) length mismatch in class pattern" + )), + p.range, + )); + } // Check for too many sub-patterns. - if nargs > u32::MAX as usize || (nargs + n_attrs).saturating_sub(1) > i32::MAX as usize { - return Err(self.error(CodegenErrorType::SyntaxError( - "too many sub-patterns in class pattern".to_owned(), - ))); + if nargs > i32::MAX as usize + || nargs.saturating_add(n_attrs).saturating_sub(1) > i32::MAX as usize + { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "too many sub-patterns in class pattern {}", + UnparseExpr::new(&match_class.cls, &self.source_file) + )), + p.range, + )); } // Validate keyword attributes if any. @@ -6394,6 +6986,7 @@ impl Compiler { // At this point the TOS is a tuple of (nargs + n_attrs) attributes (or None). pc.on_top += 1; + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); // Unpack the tuple into (nargs + n_attrs) items. @@ -6404,24 +6997,19 @@ impl Compiler { count: u32::try_from(total).unwrap() } ); - pc.on_top += total; - pc.on_top -= 1; + if total == 0 { + pc.on_top -= 1; + } else { + pc.on_top += total - 1; + } // Process each sub-pattern. for subpattern in patterns.iter().chain(kwd_patterns.iter()) { - // Check if this is a true wildcard (underscore pattern without name binding) - let is_true_wildcard = match subpattern { - ast::Pattern::MatchAs(match_as) => { - // Only consider it wildcard if both pattern and name are None (i.e., "_") - match_as.pattern.is_none() && match_as.name.is_none() - } - _ => subpattern.is_wildcard(), - }; - // Decrement the on_top counter for each sub-pattern pc.on_top -= 1; - if is_true_wildcard { + if Self::pattern_wildcard_check(subpattern) { + self.set_source_range(p.range); emit!(self, Instruction::PopTop); continue; // Don't compile wildcard patterns } @@ -6445,27 +7033,36 @@ impl Compiler { // Validate pattern count matches key count if keys.len() != patterns.len() { - return Err(self.error(CodegenErrorType::SyntaxError(format!( - "keys ({}) / patterns ({}) length mismatch in mapping pattern", - keys.len(), - patterns.len() - )))); + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "keys ({}) / patterns ({}) length mismatch in mapping pattern", + keys.len(), + patterns.len() + )), + p.range, + )); } - // Validate rest pattern: '_' cannot be used as a rest target + // `case {**_}:` is rejected before codegen. RustPython's parser + // currently lets it through, so keep the compiler boundary equivalent. if let Some(rest) = star_target && rest.as_str() == "_" { - return Err(self.error(CodegenErrorType::SyntaxError("invalid syntax".to_string()))); + return Err(self.error_ranged( + CodegenErrorType::SyntaxError("invalid syntax".to_string()), + rest.range, + )); } // Step 1: Check if subject is a mapping // Stack: [subject] pc.on_top += 1; + self.set_source_range(p.range); emit!(self, Instruction::MatchMapping); // Stack: [subject, is_mapping] + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); // Stack: [subject] @@ -6480,64 +7077,40 @@ impl Compiler { // Length check for patterns with keys if size > 0 { // Check if the mapping has at least 'size' keys + self.set_source_range(p.range); emit!(self, Instruction::GetLen); + self.set_source_range(p.range); self.emit_load_const(ConstantData::Integer { value: size.into() }); // Stack: [subject, len, size] + self.set_source_range(p.range); emit!( self, Instruction::CompareOp { opname: ComparisonOperator::GreaterOrEqual } ); + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); // Stack: [subject] } // Check for overflow (INT_MAX < size - 1) - let size = u32::try_from(size).map_err(|_| { - self.error(CodegenErrorType::SyntaxError( - "too many sub-patterns in mapping pattern".to_string(), - )) - })?; - - // Step 2: If we have keys to match - if size > 0 { - // Validate and compile keys - let mut seen = IndexSet::default(); - for key in keys { - let is_attribute = matches!(key, ast::Expr::Attribute(_)); - let is_literal = matches!( - key, - ast::Expr::NumberLiteral(_) - | ast::Expr::StringLiteral(_) - | ast::Expr::BytesLiteral(_) - | ast::Expr::BooleanLiteral(_) - | ast::Expr::NoneLiteral(_) - ); - let key_repr = if is_literal { - UnparseExpr::new(key, &self.source_file).to_string() - } else if is_attribute { - String::new() - } else { - return Err(self.error(CodegenErrorType::SyntaxError( - "mapping pattern keys may only match literals and attribute lookups" - .to_string(), - ))); - }; - - if !key_repr.is_empty() && seen.contains(&key_repr) { - return Err(self.error(CodegenErrorType::SyntaxError(format!( - "mapping pattern checks duplicate key ({key_repr})" - )))); - } - if !key_repr.is_empty() { - seen.insert(key_repr); - } + if size.saturating_sub(1) > i32::MAX as usize { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError( + "too many sub-patterns in mapping pattern".to_string(), + ), + p.range, + )); + } + let size = size.to_u32(); - self.compile_match_pattern_expr(key)?; - } - self.set_source_range(p.range); + // Step 2: Validate and compile all keys. + let mut seen = Vec::new(); + for key in keys { + self.compile_pattern_mapping_key(&mut seen, p.range, key)?; } + self.set_source_range(p.range); // Stack: [subject, key1, key2, ..., key_n] // Build tuple of keys (empty tuple if size==0) @@ -6550,11 +7123,14 @@ impl Compiler { pc.on_top += 2; // subject and keys_tuple are underneath // Check if match succeeded + self.set_source_range(p.range); emit!(self, Instruction::Copy { i: 1 }); // Stack: [subject, keys_tuple, values_tuple, values_tuple_copy] // Check if copy is None (consumes the copy like POP_JUMP_IF_NONE) + self.set_source_range(p.range); self.emit_load_const(ConstantData::None); + self.set_source_range(p.range); emit!( self, Instruction::IsOp { @@ -6563,14 +7139,18 @@ impl Compiler { ); // Stack: [subject, keys_tuple, values_tuple, bool] + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); // Stack: [subject, keys_tuple, values_tuple] // Unpack values (the original values_tuple) emit!(self, Instruction::UnpackSequence { count: size }); // Stack after unpack: [subject, keys_tuple, ...unpacked values...] - pc.on_top += size as usize; // Unpacked size values, tuple replaced by values - pc.on_top -= 1; + if size == 0 { + pc.on_top -= 1; + } else { + pc.on_top += size as usize - 1; + } // Step 3: Process matched values for i in 0..size { @@ -6589,15 +7169,19 @@ impl Compiler { // Stack: [subject, keys_tuple] // Build rest dict exactly + self.set_source_range(p.range); emit!(self, Instruction::BuildMap { count: 0 }); // Stack: [subject, keys_tuple, {}] + self.set_source_range(p.range); emit!(self, Instruction::Swap { i: 3 }); // Stack: [{}, keys_tuple, subject] + self.set_source_range(p.range); emit!(self, Instruction::DictUpdate { i: 2 }); // Stack after DICT_UPDATE: [rest_dict, keys_tuple] // DICT_UPDATE consumes source (subject) and leaves dict in place // Unpack keys and delete from rest_dict + self.set_source_range(p.range); emit!(self, Instruction::UnpackSequence { count: size }); // Stack: [rest_dict, k1, k2, ..., kn] (if size==0, nothing pushed) @@ -6606,10 +7190,13 @@ impl Compiler { let mut remaining = size; while remaining > 0 { // Copy rest_dict which is at position (1 + remaining) from TOS + self.set_source_range(p.range); emit!(self, Instruction::Copy { i: 1 + remaining }); // Stack: [rest_dict, k1, ..., kn, rest_dict] + self.set_source_range(p.range); emit!(self, Instruction::Swap { i: 2 }); // Stack: [rest_dict, k1, ..., kn-1, rest_dict, kn] + self.set_source_range(p.range); emit!(self, Instruction::DeleteSubscr); // Stack: [rest_dict, k1, ..., kn-1] (removed kn from rest_dict) remaining -= 1; @@ -6618,82 +7205,281 @@ impl Compiler { // pattern_helper_store_name will handle the rotation correctly // Store the rest dict - self.pattern_helper_store_name(Some(rest_name), pc)?; - - // After storing all values, pc.on_top should be 0 - // The values are rotated to the bottom for later storage - pc.on_top = 0; + self.pattern_helper_store_name(p.range, Some(rest_name), pc)?; } else { // Non-rest pattern: just clean up the stack // Pop them as we're not using them + self.set_source_range(p.range); emit!(self, Instruction::PopTop); // Pop keys_tuple + self.set_source_range(p.range); emit!(self, Instruction::PopTop); // Pop subject } Ok(()) } - fn compile_pattern_or( + fn compile_pattern_mapping_key( &mut self, - p: &ast::PatternMatchOr, - pc: &mut PatternContext, + seen: &mut Vec, + pattern_range: TextRange, + key: &ast::Expr, ) -> CompileResult<()> { - // Ensure the pattern is a MatchOr. - let end = self.new_block(); // Create a new jump target label. - let size = p.patterns.len(); - if size <= 1 { - return Err(self.error(CodegenErrorType::SyntaxError( - "MatchOr requires at least 2 patterns".to_owned(), - ))); + let is_attribute = matches!(key, ast::Expr::Attribute(_)); + let constant = match self.try_compile_match_mapping_key_constant(key)? { + Some(constant) => Some(constant), + None if is_attribute => None, + None => { + if Self::is_unexpected_match_literal_constant(key) { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError( + "unexpected constant inside of a literal pattern".to_string(), + ), + pattern_range, + )); + } + return Err(self.error_ranged( + CodegenErrorType::SyntaxError( + "mapping pattern keys may only match literals and attribute lookups" + .to_string(), + ), + pattern_range, + )); + } + }; + + if let Some(constant) = constant { + if seen + .iter() + .any(|seen| Self::match_mapping_keys_equal(seen, &constant)) + { + let key_repr = Self::match_mapping_key_repr(&constant); + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!( + "mapping pattern checks duplicate key ({key_repr})" + )), + pattern_range, + )); + } + seen.push(constant); } - // Save the current pattern context. - let old_pc = pc.clone(); - // Simulate Py_INCREF on pc.stores by cloning it. - pc.stores = pc.stores.clone(); - let mut control: Option> = None; // Will hold the capture list of the first alternative. + self.compile_match_pattern_expr(key) + } - // Process each alternative. - for (i, alt) in p.patterns.iter().enumerate() { - // Create a fresh empty store for this alternative. - pc.stores = Vec::new(); - // An irrefutable subpattern must be last (if allowed). - pc.allow_irrefutable = (i == size - 1) && old_pc.allow_irrefutable; - // Reset failure targets and the on_top counter. - pc.fail_pop.clear(); - pc.on_top = 0; - // Emit a COPY(1) instruction before compiling the alternative. - self.set_source_range(alt.range()); - emit!(self, Instruction::Copy { i: 1 }); - self.compile_pattern(alt, pc)?; + fn try_compile_match_mapping_key_constant( + &mut self, + key: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.try_fold_match_pattern_const_expr(key)? { + return Ok(Some(constant)); + } + self.try_compile_match_mapping_key_direct_constant(key) + } - let n_stores = pc.stores.len(); - if i == 0 { - // Save the captured names from the first alternative. - control = Some(pc.stores.clone()); - } else { - let control_vec = control.as_ref().unwrap(); - if n_stores != control_vec.len() { - return Err(self.error(CodegenErrorType::ConflictingNameBindPattern)); - } else if n_stores > 0 { - // Check that the names occur in the same order. - for i_control in (0..n_stores).rev() { - let name = &control_vec[i_control]; - // Find the index of `name` in the current stores. - let i_stores = - pc.stores.iter().position(|n| n == name).ok_or_else(|| { - self.error(CodegenErrorType::ConflictingNameBindPattern) - })?; - if i_control != i_stores { - // The orders differ; we must reorder. - assert!(i_stores < i_control, "expected i_stores < i_control"); - let rotations = i_stores + 1; - // Rotate pc.stores: take a slice of the first `rotations` items... - let rotated = pc.stores[0..rotations].to_vec(); - // Remove those elements. - for _ in 0..rotations { - pc.stores.remove(0); + fn try_compile_match_value_constant( + &mut self, + value: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.try_fold_match_pattern_const_expr(value)? { + return Ok(Some(constant)); + } + self.try_compile_match_pattern_direct_literal(value) + } + + fn match_mapping_keys_equal(left: &ConstantData, right: &ConstantData) -> bool { + use ConstantData::{Bytes, Ellipsis, None, Str}; + + if Self::match_mapping_numeric_keys_equal(left, right).unwrap_or(false) { + return true; + } + + match (left, right) { + (Str { value: left }, Str { value: right }) => left == right, + (Bytes { value: left }, Bytes { value: right }) => left == right, + (None, None) | (Ellipsis, Ellipsis) => true, + _ => false, + } + } + + fn match_mapping_key_repr(key: &ConstantData) -> String { + match key { + ConstantData::Integer { value } => value.to_string(), + ConstantData::Float { value } => literal_float::to_string(*value), + ConstantData::Complex { value } => literal_complex::to_string(value.re, value.im), + ConstantData::Boolean { value } => { + if *value { + "True".to_owned() + } else { + "False".to_owned() + } + } + ConstantData::Str { value } => UnicodeEscape::new_repr(value.as_ref()) + .str_repr() + .to_string() + .unwrap_or_else(|| value.to_string()), + ConstantData::Bytes { value } => AsciiEscape::new_repr(value) + .bytes_repr() + .to_string() + .unwrap_or_else(|| format!(r#"b"{}""#, value.escape_ascii())), + ConstantData::None => "None".to_owned(), + ConstantData::Ellipsis => "...".to_owned(), + other => other.to_string(), + } + } + + fn match_mapping_numeric_keys_equal(left: &ConstantData, right: &ConstantData) -> Option { + use ConstantData::{Boolean, Complex, Float, Integer}; + + match (left, right) { + (Integer { value: left }, Integer { value: right }) => Some(left == right), + (Boolean { value: left }, Boolean { value: right }) => Some(left == right), + (Boolean { value }, Integer { value: int }) + | (Integer { value: int }, Boolean { value }) => { + Some(BigInt::from(u8::from(*value)) == *int) + } + (Float { value: left }, Float { value: right }) => Some(left == right), + (Integer { value: int }, Float { value: float }) + | (Float { value: float }, Integer { value: int }) => { + Some(Self::match_mapping_float_integer_equal(*float, int)) + } + (Boolean { value }, Float { value: float }) + | (Float { value: float }, Boolean { value }) => Some( + Self::match_mapping_float_integer_equal(*float, &BigInt::from(u8::from(*value))), + ), + (Complex { value: left }, Complex { value: right }) => { + Some(left.re == right.re && left.im == right.im) + } + (Complex { value: complex }, other) | (other, Complex { value: complex }) => Some( + complex.im == 0.0 + && Self::match_mapping_float_real_constant_equal(complex.re, other) + .unwrap_or(false), + ), + _ => Option::None, + } + } + + fn match_mapping_float_real_constant_equal(float: f64, other: &ConstantData) -> Option { + match other { + ConstantData::Integer { value } => { + Some(Self::match_mapping_float_integer_equal(float, value)) + } + ConstantData::Boolean { value } => Some(Self::match_mapping_float_integer_equal( + float, + &BigInt::from(u8::from(*value)), + )), + ConstantData::Float { value } => Some(float == *value), + _ => None, + } + } + + fn match_mapping_float_integer_equal(float: f64, int: &BigInt) -> bool { + Self::match_mapping_float_to_integer(float).is_some_and(|float_int| &float_int == int) + } + + fn match_mapping_float_to_integer(value: f64) -> Option { + if !value.is_finite() { + return None; + } + if value == 0.0 { + return Some(BigInt::from(0)); + } + + let bits = value.to_bits(); + let negative = (bits >> 63) != 0; + let exponent_bits = i32::try_from((bits >> 52) & 0x7ff).ok()?; + let fraction = bits & ((1_u64 << 52) - 1); + let (mantissa, exponent) = if exponent_bits == 0 { + (fraction, -1074) + } else { + ((1_u64 << 52) | fraction, exponent_bits - 1023 - 52) + }; + + let mut integer = if exponent >= 0 { + BigInt::from(mantissa) << u32::try_from(exponent).ok()? + } else { + let shift = u32::try_from(-exponent).ok()?; + if shift >= u64::BITS { + return None; + } + let mask = (1_u64 << shift) - 1; + if mantissa & mask != 0 { + return None; + } + BigInt::from(mantissa >> shift) + }; + + if negative { + integer = -integer; + } + Some(integer) + } + + fn compile_pattern_or( + &mut self, + p: &ast::PatternMatchOr, + pc: &mut PatternContext, + ) -> CompileResult<()> { + // Ensure the pattern is a MatchOr. + let end = self.new_block(); // Create a new jump target label. + let size = p.patterns.len(); + if size <= 1 { + return Err(self.error(CodegenErrorType::SyntaxError( + "MatchOr requires at least 2 patterns".to_owned(), + ))); + } + + // Save the current pattern context. + let old_pc = pc.clone(); + // Simulate Py_INCREF on pc.stores by cloning it. + pc.stores = pc.stores.clone(); + let mut control: Option> = None; // Will hold the capture list of the first alternative. + + // Process each alternative. + for (i, alt) in p.patterns.iter().enumerate() { + // Create a fresh empty store for this alternative. + pc.stores = Vec::new(); + // An irrefutable subpattern must be last (if allowed). + pc.allow_irrefutable = (i == size - 1) && old_pc.allow_irrefutable; + // Reset failure targets and the on_top counter. + pc.fail_pop.clear(); + pc.on_top = 0; + // Emit a COPY(1) instruction before compiling the alternative. + self.set_source_range(alt.range()); + emit!(self, Instruction::Copy { i: 1 }); + self.compile_pattern(alt, pc)?; + + let n_stores = pc.stores.len(); + if i == 0 { + // Save the captured names from the first alternative. + control = Some(pc.stores.clone()); + } else { + let control_vec = control.as_ref().unwrap(); + if n_stores != control_vec.len() { + return Err( + self.error_ranged(CodegenErrorType::ConflictingNameBindPattern, p.range()) + ); + } else if n_stores > 0 { + // Check that the names occur in the same order. + for i_control in (0..n_stores).rev() { + let name = &control_vec[i_control]; + // Find the index of `name` in the current stores. + let i_stores = + pc.stores.iter().position(|n| n == name).ok_or_else(|| { + self.error_ranged( + CodegenErrorType::ConflictingNameBindPattern, + p.range(), + ) + })?; + if i_control != i_stores { + // The orders differ; we must reorder. + assert!(i_stores < i_control, "expected i_stores < i_control"); + let rotations = i_stores + 1; + // Rotate pc.stores: take a slice of the first `rotations` items... + let rotated = pc.stores[0..rotations].to_vec(); + // Remove those elements. + for _ in 0..rotations { + pc.stores.remove(0); } // Insert the rotated slice at the appropriate index. let insert_pos = i_control - i_stores; @@ -6703,7 +7489,7 @@ impl Compiler { // Also perform the same rotation on the evaluation stack. self.set_source_range(alt.range()); for _ in 0..=i_stores { - self.pattern_helper_rotate(i_control + 1); + self.pattern_helper_rotate(alt.range(), i_control + 1); } } } @@ -6713,7 +7499,7 @@ impl Compiler { self.set_source_range(alt.range()); emit!(self, PseudoInstruction::Jump { delta: end }); self.set_source_range(alt.range()); - self.emit_and_reset_fail_pop(pc); + self.emit_and_reset_fail_pop(pc, alt.range()); } // Restore the original pattern context. @@ -6738,11 +7524,14 @@ impl Compiler { for i in 0..n_stores { // Rotate the capture to its proper place. self.set_source_range(p.range()); - self.pattern_helper_rotate(n_rots); + self.pattern_helper_rotate(p.range(), n_rots); let name = &control.as_ref().unwrap()[i]; // Check for duplicate binding. if pc.stores.contains(name) { - return Err(self.error(CodegenErrorType::DuplicateStore(name.to_string()))); + return Err(self.error_ranged( + CodegenErrorType::DuplicateStore(name.to_string()), + p.range(), + )); } pc.stores.push(name.clone()); } @@ -6770,47 +7559,59 @@ impl Compiler { for (i, pattern) in patterns.iter().enumerate() { if pattern.is_match_star() { if star.is_some() { - // TODO: Fix error msg - return Err(self.error(CodegenErrorType::MultipleStarArgs)); + return Err(self.error_ranged( + CodegenErrorType::MultipleStarredNamesInSequencePattern, + p.range, + )); } // star wildcard check - star_wildcard = pattern.as_match_star().is_some_and(|m| m.name.is_none()); + star_wildcard = Self::pattern_wildcard_star_check(pattern); only_wildcard &= star_wildcard; star = Some(i); continue; } // wildcard check - only_wildcard &= pattern.as_match_as().is_some_and(|m| m.name.is_none()); + only_wildcard &= Self::pattern_wildcard_check(pattern); } // Keep the subject on top during the sequence and length checks. pc.on_top += 1; + self.set_source_range(p.range); emit!(self, Instruction::MatchSequence); + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); if star.is_none() { // No star: len(subject) == size + self.set_source_range(p.range); emit!(self, Instruction::GetLen); + self.set_source_range(p.range); self.emit_load_const(ConstantData::Integer { value: size.into() }); + self.set_source_range(p.range); emit!( self, Instruction::CompareOp { opname: ComparisonOperator::Equal } ); + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); } else if size > 1 { // Star exists: len(subject) >= size - 1 + self.set_source_range(p.range); emit!(self, Instruction::GetLen); + self.set_source_range(p.range); self.emit_load_const(ConstantData::Integer { value: (size - 1).into(), }); + self.set_source_range(p.range); emit!( self, Instruction::CompareOp { opname: ComparisonOperator::GreaterOrEqual } ); + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); } @@ -6818,11 +7619,12 @@ impl Compiler { pc.on_top -= 1; if only_wildcard { // ast::Patterns like: [] / [_] / [_, _] / [*_] / [_, *_] / [_, _, *_] / etc. + self.set_source_range(p.range); emit!(self, Instruction::PopTop); } else if star_wildcard { - self.pattern_helper_sequence_subscr(patterns, star.unwrap(), pc)?; + self.pattern_helper_sequence_subscr(p.range, patterns, star.unwrap(), pc)?; } else { - self.pattern_helper_sequence_unpack(patterns, star, pc)?; + self.pattern_helper_sequence_unpack(p.range, patterns, star, pc)?; } Ok(()) } @@ -6835,14 +7637,37 @@ impl Compiler { // Match CPython codegen_pattern_value(): compare, then normalize to bool // before the fail jump. Late IR folding will collapse COMPARE_OP+TO_BOOL // into COMPARE_OP bool(...) when applicable. - self.compile_match_pattern_expr(&p.value)?; + if let Some(constant) = self.try_compile_match_value_constant(&p.value)? { + self.set_source_range(p.value.range()); + self.emit_load_const(constant); + } else if matches!(*p.value, ast::Expr::Attribute(_)) { + self.compile_expression(&p.value)?; + } else { + if Self::is_unexpected_match_literal_constant(&p.value) { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError( + "unexpected constant inside of a literal pattern".to_string(), + ), + p.range, + )); + } + return Err(self.error_ranged( + CodegenErrorType::SyntaxError( + "patterns may only match literals and attribute lookups".to_string(), + ), + p.range, + )); + } + self.set_source_range(p.range); emit!( self, Instruction::CompareOp { opname: bytecode::ComparisonOperator::Equal } ); + self.set_source_range(p.range); emit!(self, Instruction::ToBool); + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); Ok(()) } @@ -6853,14 +7678,17 @@ impl Compiler { pc: &mut PatternContext, ) { // Load the singleton constant value. + self.set_source_range(p.range); self.emit_load_const(match p.value { ast::Singleton::None => ConstantData::None, ast::Singleton::False => ConstantData::Boolean { value: false }, ast::Singleton::True => ConstantData::Boolean { value: true }, }); // Compare using the "Is" operator. + self.set_source_range(p.range); emit!(self, Instruction::IsOp { invert: Invert::No }); // Jump to the failure label if the comparison is false. + self.set_source_range(p.range); self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse); } @@ -6908,22 +7736,13 @@ impl Compiler { cases: &[ast::MatchCase], pattern_context: &mut PatternContext, ) -> CompileResult<()> { - fn is_trailing_wildcard_default(pattern: &ast::Pattern) -> bool { - match pattern { - ast::Pattern::MatchAs(match_as) => { - match_as.pattern.is_none() && match_as.name.is_none() - } - _ => false, - } - } - self.compile_expression(subject)?; let end = self.new_block(); let num_cases = cases.len(); assert!(num_cases > 0); let has_default = - num_cases > 1 && is_trailing_wildcard_default(&cases.last().unwrap().pattern); + num_cases > 1 && Self::pattern_wildcard_check(&cases.last().unwrap().pattern); let case_count = num_cases - usize::from(has_default); for (i, m) in cases.iter().enumerate().take(case_count) { @@ -6941,8 +7760,8 @@ impl Compiler { self.compile_pattern(&m.pattern, pattern_context)?; assert_eq!(pattern_context.on_top, 0); - self.set_source_range(m.pattern.range()); for name in &pattern_context.stores { + self.set_source_range(m.pattern.range()); self.compile_name(name, NameUsage::Store)?; } @@ -6970,7 +7789,7 @@ impl Compiler { emit!(self, PseudoInstruction::Jump { delta: end }); self.set_no_location(); self.set_source_range(m.pattern.range()); - self.emit_and_reset_fail_pop(pattern_context); + self.emit_and_reset_fail_pop(pattern_context, m.pattern.range()); } if has_default { @@ -6982,7 +7801,7 @@ impl Compiler { emit!(self, Instruction::Nop); } if let Some(ref guard) = m.guard { - self.compile_jump_if(guard, false, end)?; + self.compile_jump_if_inner(guard, false, end, Some(m.pattern.range()))?; } self.compile_statements(&m.body)?; } @@ -7004,7 +7823,7 @@ impl Compiler { } /// [CPython `compiler_addcompare`](https://github.com/python/cpython/blob/627894459a84be3488a1789919679c997056a03c/Python/compile.c#L2880-L2924) - fn compile_addcompare(&mut self, op: &ast::CmpOp) { + fn compile_addcompare(&mut self, op: ast::CmpOp) { match op { ast::CmpOp::Eq => emit!( self, @@ -7087,6 +7906,7 @@ impl Compiler { ) -> CompileResult<()> { // Save the full Compare expression range for COMPARE_OP positions let compare_range = self.current_source_range; + self.check_compare(compare_range, left, ops, comparators)?; let (last_op, mid_ops) = ops.split_last().unwrap(); let (last_comparator, mid_comparators) = comparators.split_last().unwrap(); @@ -7096,7 +7916,7 @@ impl Compiler { if mid_comparators.is_empty() { self.compile_expression(last_comparator)?; self.set_source_range(compare_range); - self.compile_addcompare(last_op); + self.compile_addcompare(*last_op); return Ok(()); } @@ -7112,7 +7932,7 @@ impl Compiler { emit!(self, Instruction::Swap { i: 2 }); emit!(self, Instruction::Copy { i: 2 }); - self.compile_addcompare(op); + self.compile_addcompare(*op); // if comparison result is false, we break with this value; if true, try the next one. emit!(self, Instruction::Copy { i: 1 }); @@ -7123,7 +7943,7 @@ impl Compiler { self.compile_expression(last_comparator)?; self.set_source_range(compare_range); - self.compile_addcompare(last_op); + self.compile_addcompare(*last_op); let end = self.new_block(); emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end }); @@ -7147,6 +7967,7 @@ impl Compiler { target_block: BlockIdx, ) -> CompileResult<()> { let compare_range = self.current_source_range; + self.check_compare(compare_range, left, ops, comparators)?; let (last_op, mid_ops) = ops.split_last().unwrap(); let (last_comparator, mid_comparators) = comparators.split_last().unwrap(); @@ -7154,7 +7975,7 @@ impl Compiler { self.compile_expression(left)?; self.compile_expression(last_comparator)?; self.set_source_range(compare_range); - self.compile_addcompare(last_op); + self.compile_addcompare(*last_op); self.emit_pop_jump_by_condition(condition, target_block); return Ok(()); } @@ -7167,14 +7988,14 @@ impl Compiler { self.set_source_range(compare_range); emit!(self, Instruction::Swap { i: 2 }); emit!(self, Instruction::Copy { i: 2 }); - self.compile_addcompare(op); + self.compile_addcompare(*op); emit!(self, Instruction::ToBool); emit!(self, Instruction::PopJumpIfFalse { delta: cleanup }); } self.compile_expression(last_comparator)?; self.set_source_range(compare_range); - self.compile_addcompare(last_op); + self.compile_addcompare(*last_op); emit!(self, Instruction::ToBool); self.emit_pop_jump_by_condition(condition, target_block); let end = self.new_block(); @@ -7184,13 +8005,13 @@ impl Compiler { self.use_cpython_label_block(cleanup); emit!(self, Instruction::PopTop); if !condition { - self.set_no_location(); emit!( self, PseudoInstruction::JumpNoInterrupt { delta: target_block } ); + self.set_no_location(); } self.use_cpython_label_block(end); @@ -7232,7 +8053,9 @@ impl Compiler { ast::Expr::Starred(ast::ExprStarred { value, .. }) => { // *args: *Ts (where Ts is a TypeVarTuple). // Do [annotation_value] = [*Ts]. + let saved_range = self.current_source_range; self.compile_expression(value)?; + self.set_source_range(saved_range); emit!(self, Instruction::UnpackSequence { count: 1 }); Ok(()) } @@ -7247,6 +8070,7 @@ impl Compiler { fn compile_check_annotation_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> { self.compile_expression(expression)?; + self.set_source_range(expression.range()); emit!(self, Instruction::PopTop); Ok(()) } @@ -7319,23 +8143,24 @@ impl Compiler { } else { // PEP 649: Handle conditional annotations if self.current_symbol_table().has_conditional_annotations { - // Allocate an index for every annotation when has_conditional_annotations - // This keeps indices aligned with compile_module_annotate's enumeration - let code_info = self.current_code_info(); - let annotation_index = code_info.next_conditional_annotation_index; - code_info.next_conditional_annotation_index += 1; - - // Determine if this annotation is conditional - // Module and Class scopes both need all annotations tracked let scope_type = self.current_symbol_table().typ; let in_conditional_block = self.current_code_info().in_conditional_block > 0; let is_conditional = - matches!(scope_type, CompilerScope::Module | CompilerScope::Class) - || in_conditional_block; + matches!(scope_type, CompilerScope::Module) || in_conditional_block; - // Only add to __conditional_annotations__ set if actually conditional if is_conditional { - self.load_name("__conditional_annotations__")?; + let code_info = self.current_code_info(); + let annotation_index = code_info.next_conditional_annotation_index; + code_info.next_conditional_annotation_index += 1; + + self.set_source_range(loc); + if matches!(scope_type, CompilerScope::Class) { + let i = self.get_cell_var_index("__conditional_annotations__"); + emit!(self, Instruction::LoadDeref { i }); + } else { + let namei = self.name("__conditional_annotations__"); + emit!(self, Instruction::LoadName { namei }); + } self.emit_load_const(ConstantData::Integer { value: annotation_index.into(), }); @@ -7390,23 +8215,27 @@ impl Compiler { // Scan for star args: for (i, element) in elts.iter().enumerate() { - if let ast::Expr::Starred(_) = &element { - if seen_star { - return Err(self.error(CodegenErrorType::MultipleStarArgs)); - } - - seen_star = true; + if matches!(element, ast::Expr::Starred(_)) && !seen_star { let before = i; let after = elts.len() - i - 1; - let (before, after) = (|| Some((before.to_u8()?, after.to_u8()?)))() - .ok_or_else(|| { - self.error_ranged( - CodegenErrorType::TooManyStarUnpack, - target.range(), - ) - })?; + if before >= (1 << 8) || after >= ((i32::MAX as usize) >> 8) { + return Err(self.error_ranged( + CodegenErrorType::TooManyStarUnpack, + target.range(), + )); + } + let before = before.to_u8().ok_or_else(|| { + self.error_ranged( + CodegenErrorType::TooManyStarUnpack, + target.range(), + ) + })?; + let after = after.to_u32(); let counts = bytecode::UnpackExArgs { before, after }; emit!(self, Instruction::UnpackEx { counts }); + seen_star = true; + } else if matches!(element, ast::Expr::Starred(_)) { + return Err(self.error(CodegenErrorType::MultipleStarArgs)); } } @@ -7446,7 +8275,7 @@ impl Compiler { fn compile_augassign( &mut self, target: &ast::Expr, - op: &ast::Operator, + op: ast::Operator, value: &ast::Expr, ) -> CompileResult<()> { let stmt_range = self.current_source_range; @@ -7471,13 +8300,8 @@ impl Compiler { self.compile_name(id, NameUsage::Load)?; AugAssignKind::Name { id } } - ast::Expr::Subscript(ast::ExprSubscript { - value, - slice, - ctx: _, - .. - }) => { - let use_slice_opt = slice.should_use_slice_optimization(); + ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + let use_slice_opt = self.should_apply_two_element_slice_optimization(slice); self.compile_expression(value)?; self.set_source_range(target_range); if use_slice_opt { @@ -7511,7 +8335,7 @@ impl Compiler { self.compile_expression(value)?; let attr_range = self.update_start_location_to_match_attr(target_range, target_range, attr); - self.set_source_range(attr_range); + self.set_source_range(target_range); emit!(self, Instruction::Copy { i: 1 }); let idx = self.name(attr); self.set_source_range(attr_range); @@ -7559,7 +8383,7 @@ impl Compiler { Ok(()) } - fn compile_op(&mut self, op: &ast::Operator, inplace: bool) { + fn compile_op(&mut self, op: ast::Operator, inplace: bool) { let bin_op = match op { ast::Operator::Add => BinaryOperator::Add, ast::Operator::Sub => BinaryOperator::Subtract, @@ -7646,6 +8470,7 @@ impl Compiler { comparators, .. }) if ops.len() > 1 => { + self.set_source_range(expression.range()); self.compile_jump_if_compare(left, ops, comparators, condition, target_block) } _ => { @@ -7687,7 +8512,7 @@ impl Compiler { /// Compile a boolean operation as an expression. /// This means, that the last value remains on the stack. - fn compile_bool_op(&mut self, op: &ast::BoolOp, values: &[ast::Expr]) -> CompileResult<()> { + fn compile_bool_op(&mut self, op: ast::BoolOp, values: &[ast::Expr]) -> CompileResult<()> { let boolop_range = self.current_source_range; let after_block = self.new_block(); let (last_value, prefix_values) = values.split_last().unwrap(); @@ -7707,7 +8532,7 @@ impl Compiler { /// Emit CPython-style pseudo conditional jump for short-circuit evaluation. /// flowgraph.c lowers it to `COPY 1; TO_BOOL; POP_JUMP_IF_*`. - fn emit_short_circuit_test(&mut self, op: &ast::BoolOp, target: BlockIdx) { + fn emit_short_circuit_test(&mut self, op: ast::BoolOp, target: BlockIdx) { match op { ast::BoolOp::And => { emit!(self, PseudoInstruction::JumpIfFalse { delta: target }); @@ -7718,130 +8543,50 @@ impl Compiler { } } - fn compile_dict(&mut self, items: &[ast::DictItem], range: TextRange) -> CompileResult<()> { - let has_unpacking = items.iter().any(|item| item.key.is_none()); - - if !has_unpacking { - // Match CPython's compiler_subdict chunking strategy: - // - n≤15: BUILD_MAP n (all pairs on stack) - // - n>15: BUILD_MAP 0 + MAP_ADD chunks of 17, last chunk uses - // BUILD_MAP n (if ≤15) or BUILD_MAP 0 + MAP_ADD - const STACK_LIMIT: usize = 15; - const BIG_MAP_CHUNK: usize = 17; - - if items.len() <= STACK_LIMIT { - for item in items { - self.compile_expression(item.key.as_ref().unwrap())?; - self.compile_expression(&item.value)?; - } - self.set_source_range(range); - emit!( - self, - Instruction::BuildMap { - count: u32::try_from(items.len()).expect("too many dict items"), - } - ); - } else { - // Split: leading full chunks of BIG_MAP_CHUNK via MAP_ADD, - // remainder via BUILD_MAP n or MAP_ADD depending on size - let n = items.len(); - let remainder = n % BIG_MAP_CHUNK; - let n_big_chunks = n / BIG_MAP_CHUNK; - // If remainder fits on stack (≤15), use BUILD_MAP n for it. - // Otherwise it becomes another MAP_ADD chunk. - let (big_count, tail_count) = if remainder > 0 && remainder <= STACK_LIMIT { - (n_big_chunks, remainder) - } else { - // remainder is 0 or >15: all chunks are MAP_ADD chunks - let total_map_add = if remainder == 0 { - n_big_chunks - } else { - n_big_chunks + 1 - }; - (total_map_add, 0usize) - }; - + fn compile_subdict( + &mut self, + items: &[ast::DictItem], + begin: usize, + end: usize, + range: TextRange, + ) -> CompileResult<()> { + let n = end - begin; + let big = n * 2 > STACK_USE_GUIDELINE as usize; + if big { + self.set_source_range(range); + emit!(self, Instruction::BuildMap { count: 0 }); + } + for item in &items[begin..end] { + self.compile_expression(item.key.as_ref().unwrap())?; + self.compile_expression(&item.value)?; + if big { self.set_source_range(range); - emit!(self, Instruction::BuildMap { count: 0 }); - - let mut idx = 0; - for chunk_i in 0..big_count { - if chunk_i > 0 { - self.set_source_range(range); - emit!(self, Instruction::BuildMap { count: 0 }); - } - let chunk_size = if idx + BIG_MAP_CHUNK <= n - tail_count { - BIG_MAP_CHUNK - } else { - n - tail_count - idx - }; - for item in &items[idx..idx + chunk_size] { - self.compile_expression(item.key.as_ref().unwrap())?; - self.compile_expression(&item.value)?; - self.set_source_range(range); - emit!(self, Instruction::MapAdd { i: 1 }); - } - if chunk_i > 0 { - self.set_source_range(range); - emit!(self, Instruction::DictUpdate { i: 1 }); - } - idx += chunk_size; - } - - // Tail: remaining pairs via BUILD_MAP n + DICT_UPDATE - if tail_count > 0 { - for item in &items[idx..idx + tail_count] { - self.compile_expression(item.key.as_ref().unwrap())?; - self.compile_expression(&item.value)?; - } - self.set_source_range(range); - emit!( - self, - Instruction::BuildMap { - count: tail_count.to_u32(), - } - ); - self.set_source_range(range); - emit!(self, Instruction::DictUpdate { i: 1 }); - } + emit!(self, Instruction::MapAdd { i: 1 }); } - return Ok(()); } + if !big { + self.set_source_range(range); + emit!(self, Instruction::BuildMap { count: n.to_u32() }); + } + Ok(()) + } - // Complex case with ** unpacking: preserve insertion order. - // Collect runs of regular k:v pairs and emit BUILD_MAP + DICT_UPDATE - // for each run, and DICT_UPDATE for each ** entry. + fn compile_dict(&mut self, items: &[ast::DictItem], range: TextRange) -> CompileResult<()> { + let n = items.len(); let mut have_dict = false; - let mut elements: u32 = 0; - - // Flush pending regular pairs as a BUILD_MAP, merging into the - // accumulator dict via DICT_UPDATE when one already exists. - macro_rules! flush_pending { - () => { - #[allow(unused_assignments)] - if elements > 0 { - self.set_source_range(range); - emit!(self, Instruction::BuildMap { count: elements }); + let mut elements = 0usize; + + for (i, item) in items.iter().enumerate() { + if item.key.is_none() { + if elements != 0 { + self.compile_subdict(items, i - elements, i, range)?; if have_dict { self.set_source_range(range); emit!(self, Instruction::DictUpdate { i: 1 }); - } else { - have_dict = true; } + have_dict = true; elements = 0; } - }; - } - - for item in items { - if let Some(key) = &item.key { - // Regular key: value pair - self.compile_expression(key)?; - self.compile_expression(&item.value)?; - elements += 1; - } else { - // ** unpacking entry - flush_pending!(); if !have_dict { self.set_source_range(range); emit!(self, Instruction::BuildMap { count: 0 }); @@ -7850,10 +8595,27 @@ impl Compiler { self.compile_expression(&item.value)?; self.set_source_range(range); emit!(self, Instruction::DictUpdate { i: 1 }); + } else if elements * 2 > STACK_USE_GUIDELINE as usize { + self.compile_subdict(items, i - elements, i + 1, range)?; + if have_dict { + self.set_source_range(range); + emit!(self, Instruction::DictUpdate { i: 1 }); + } + have_dict = true; + elements = 0; + } else { + elements += 1; } } - flush_pending!(); + if elements != 0 { + self.compile_subdict(items, n - elements, n, range)?; + if have_dict { + self.set_source_range(range); + emit!(self, Instruction::DictUpdate { i: 1 }); + } + have_dict = true; + } if !have_dict { self.set_source_range(range); emit!(self, Instruction::BuildMap { count: 0 }); @@ -7932,17 +8694,20 @@ impl Compiler { send_block } - /// Returns true if the expression is a constant with no side effects. - fn is_const_expression(expr: &ast::Expr) -> bool { - matches!( - expr, - ast::Expr::StringLiteral(_) - | ast::Expr::BytesLiteral(_) - | ast::Expr::NumberLiteral(_) - | ast::Expr::BooleanLiteral(_) - | ast::Expr::NoneLiteral(_) - | ast::Expr::EllipsisLiteral(_) - ) + fn ast_constant_value(&self, expr: &ast::Expr) -> Option { + expr.as_constant_expr() + .map(|expr| ast_constant_value_to_constant_data(expr.value.clone())) + } + + fn single_runtime_interpolation( + expr_tstring: &ast::ExprTString, + ) -> Option<(&ast::ConstantValue, Option<&ast::Expr>)> { + let tstring = expr_tstring.as_single_part_tstring()?; + let interpolation = tstring.elements.first()?.as_interpolation()?; + Some(( + interpolation.runtime_str.as_ref()?, + interpolation.runtime_interpolation_format_spec.as_deref(), + )) } fn compile_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> { @@ -7950,9 +8715,7 @@ impl Compiler { let range = expression.range(); self.set_source_range(range); - if matches!(expression, ast::Expr::BinOp(_)) - && let Some(constant) = self.try_fold_constant_expr(expression)? - { + if let Some(constant) = self.ast_constant_value(expression) { self.emit_load_const(constant); return Ok(()); } @@ -7962,7 +8725,7 @@ impl Compiler { func, arguments, .. }) => self.compile_call(func, arguments)?, ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => { - self.compile_bool_op(op, values)? + self.compile_bool_op(*op, values)? } ast::Expr::BinOp(ast::ExprBinOp { left, op, right, .. @@ -7972,7 +8735,7 @@ impl Compiler { // Restore full expression range before emitting the operation self.set_source_range(range); - self.compile_op(op, false); + self.compile_op(*op, false); } ast::Expr::Subscript(ast::ExprSubscript { value, slice, ctx, .. @@ -7980,22 +8743,7 @@ impl Compiler { self.compile_subscript(value, slice, *ctx)?; } ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => { - if let ( - ast::UnaryOp::Not, - ast::Expr::Compare(ast::ExprCompare { - left, - ops, - comparators, - .. - }), - ) = (op, operand.as_ref()) - && ops.len() == 1 - { - self.set_source_range(range); - self.compile_compare(left, ops, comparators)?; - } else { - self.compile_expression(operand)?; - } + self.compile_expression(operand)?; // Restore full expression range before emitting the operation self.set_source_range(range); @@ -8026,6 +8774,8 @@ impl Compiler { unreachable!("can_optimize_super_call only accepts calls"); }; self.load_args_for_super(&super_type, super_func.range(), value.range())?; + let attr_access_range = + self.update_start_location_to_match_attr(range, range, attr.as_str()); self.set_source_range(range); let idx = self.name(attr.as_str()); match super_type { @@ -8036,6 +8786,8 @@ impl Compiler { self.emit_load_zero_super_attr(idx); } } + self.set_source_range(attr_access_range); + emit!(self, Instruction::Nop); } else { // Normal attribute access self.compile_expression(value)?; @@ -8056,9 +8808,9 @@ impl Compiler { }) => { self.compile_compare(left, ops, comparators)?; } - // ast::Expr::Constant(ExprConstant { value, .. }) => { - // self.emit_load_const(compile_constant(value)); - // } + ast::Expr::Constant(ast::ExprConstant { value, .. }) => { + self.emit_load_const(ast_constant_value_to_constant_data(value.clone())); + } ast::Expr::List(ast::ExprList { elts, range, .. }) => { self.set_source_range(*range); self.starunpack_helper(elts, 0, CollectionType::List)?; @@ -8140,7 +8892,9 @@ impl Compiler { ); } ast::Expr::Await(ast::ExprAwait { value, .. }) => { - if self.ctx.func != FunctionContext::AsyncFunction { + if self.ctx.func != FunctionContext::AsyncFunction + && !self.allows_top_level_await_in_current_context() + { return Err(self.error(CodegenErrorType::InvalidAwait)); } self.compile_expression(value)?; @@ -8244,15 +8998,20 @@ impl Compiler { }; self.compile_expression(body)?; - self.set_source_range(body.range()); - self.emit_return_value(); - // _PyCodegen_AddReturnAtEnd() appends a no-location - // return-None epilogue even after lambda's explicit - // RETURN_VALUE. It is later removed as unreachable, but - // remove_unused_consts() keeps None when it was the first - // constant in an otherwise constant-free lambda. - if self.current_code_info().metadata.consts.is_empty() { - self.arg_constant(ConstantData::None); + let is_generator = self + .current_code_info() + .flags + .contains(bytecode::CodeFlags::GENERATOR); + if is_generator { + // codegen_lambda() calls OptimizeAndAssemble with + // addNone=0, so AddReturnAtEnd appends RETURN_VALUE without + // adding None to co_consts. + emit!(self, Instruction::ReturnValue); + self.set_no_location(); + } else { + self.set_source_range(body.range()); + self.emit_return_value(); + self.emit_return_const_no_location(ConstantData::None); } let code = self.exit_scope(); @@ -8335,6 +9094,7 @@ impl Compiler { range, .. }) => { + let key = key.as_ref(); self.compile_comprehension( "", Some( @@ -8434,9 +9194,20 @@ impl Compiler { self.set_source_range(target.range()); } ast::Expr::FString(fstring) => { + if let Some(joined_str) = fstring.runtime_joined_str.as_ref() { + return self.compile_runtime_joined_str(fstring, joined_str); + } self.compile_expr_fstring(fstring)?; } ast::Expr::TString(tstring) => { + if let Some(template_str) = tstring.runtime_template_str.as_ref() { + return self.compile_runtime_template_str(tstring, template_str); + } + if let Some(interpolation) = Self::single_runtime_interpolation(tstring) + && self.compile_runtime_interpolation(tstring, interpolation)? + { + return Ok(()); + } self.compile_expr_tstring(tstring)?; } ast::Expr::StringLiteral(string) => { @@ -8471,8 +9242,11 @@ impl Compiler { ast::Expr::EllipsisLiteral(_) => { self.emit_load_const(ConstantData::Ellipsis); } - ast::Expr::IpyEscapeCommand(_) => { - panic!("unexpected ipy escape command"); + ast::Expr::IpyEscapeCommand(expr) => { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError("invalid syntax".to_owned()), + expr.range, + )); } } Ok(()) @@ -8486,14 +9260,7 @@ impl Compiler { let ast::Expr::Name(ast::ExprName { id, .. }) = func else { return None; }; - let [ - ast::Expr::Generator(ast::ExprGenerator { - elt: _, - generators: _, - .. - }), - ] = &args.args[..] - else { + let [ast::Expr::Generator(ast::ExprGenerator { .. })] = &args.args[..] else { return None; }; if !args.keywords.is_empty() || { @@ -8564,17 +9331,13 @@ impl Compiler { emit!(self, Instruction::BuildList { count: 0 }); } - let sub_table_cursor = self.symbol_table_stack.last().map(|t| t.next_sub_table); + let symbol_table_cursors = self.current_symbol_table_cursors(); if let Some(range) = self.cpython_implicit_call_generator_range(generator_expr) { self.compile_expression_with_generator_range(generator_expr, range)?; } else { self.compile_expression(generator_expr)?; } - if let Some(cursor) = sub_table_cursor - && let Some(current_table) = self.symbol_table_stack.last_mut() - { - current_table.next_sub_table = cursor; - } + self.set_symbol_table_cursors(symbol_table_cursors); let loop_block = self.new_block(); let cleanup = self.new_block(); @@ -8593,9 +9356,9 @@ impl Compiler { self.set_source_range(loc); emit!(self, Instruction::ToBool); emit!(self, Instruction::PopJumpIfTrue { delta: loop_block }); - self.set_source_range(loc); emit!(self, Instruction::PopIter); self.set_no_location(); + self.set_source_range(loc); self.emit_load_const(ConstantData::Boolean { value: false }); self.set_source_range(loc); emit!(self, PseudoInstruction::Jump { delta: end }); @@ -8604,9 +9367,9 @@ impl Compiler { self.set_source_range(loc); emit!(self, Instruction::ToBool); emit!(self, Instruction::PopJumpIfFalse { delta: loop_block }); - self.set_source_range(loc); emit!(self, Instruction::PopIter); self.set_no_location(); + self.set_source_range(loc); self.emit_load_const(ConstantData::Boolean { value: true }); self.set_source_range(loc); emit!(self, PseudoInstruction::Jump { delta: end }); @@ -8614,10 +9377,8 @@ impl Compiler { } self.use_cpython_label_block(cleanup); - self.set_source_range(loc); emit!(self, Instruction::EndFor); self.set_no_location(); - self.set_source_range(loc); emit!(self, Instruction::PopIter); self.set_no_location(); match kind { @@ -8646,20 +9407,100 @@ impl Compiler { Ok(()) } + fn can_use_cpython_method_call(&self, value: &ast::Expr, args: &ast::Arguments) -> bool { + let is_import = matches!(value, ast::Expr::Name(ast::ExprName { id, .. }) + if self.is_name_imported(id.as_str())); + if is_import { + return false; + } + + if args.args.len() + args.keywords.len() + usize::from(!args.keywords.is_empty()) + >= STACK_USE_GUIDELINE as usize + { + return false; + } + + !args + .args + .iter() + .any(|arg| matches!(arg, ast::Expr::Starred(_))) + && args.keywords.iter().all(|kw| kw.arg.is_some()) + } + + fn compile_method_call_arguments( + &mut self, + args: &ast::Arguments, + call_range: TextRange, + kw_names_range: TextRange, + ) -> CompileResult<()> { + let implicit_generator_range = if args.args.len() == 1 && args.keywords.is_empty() { + self.cpython_implicit_call_generator_range(&args.args[0]) + } else { + None + }; + for arg in &args.args { + if let Some(range) = implicit_generator_range { + self.compile_expression_with_generator_range(arg, range)?; + } else { + self.compile_expression(arg)?; + } + } + + if args.keywords.is_empty() { + self.set_source_range(call_range); + emit!( + self, + Instruction::Call { + argc: args.args.len().to_u32() + } + ); + return Ok(()); + } + + let mut kwarg_names = Vec::with_capacity(args.keywords.len()); + for keyword in &args.keywords { + kwarg_names.push(ConstantData::Str { + value: keyword.arg.as_ref().unwrap().as_str().into(), + }); + self.compile_expression(&keyword.value)?; + } + self.set_source_range(kw_names_range); + self.emit_load_const(ConstantData::Tuple { + elements: kwarg_names, + }); + self.set_source_range(call_range); + emit!( + self, + Instruction::CallKw { + argc: (args.args.len() + args.keywords.len()).to_u32() + } + ); + Ok(()) + } + fn compile_call(&mut self, func: &ast::Expr, args: &ast::Arguments) -> CompileResult<()> { // Save the call expression's source range so CALL instructions use the // call start line, not the last argument's line. let call_range = self.current_source_range; + self.validate_keywords(&args.keywords)?; let uses_ex_call = self.call_uses_ex_call(args); // Method call: obj → LOAD_ATTR_METHOD → [method, self_or_null] → args → CALL // Regular call: func → PUSH_NULL → args → CALL if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = &func { + if !self.can_use_cpython_method_call(value, args) { + self.check_caller(func)?; + self.compile_expression(func)?; + self.set_source_range(func.range()); + emit!(self, Instruction::PushNull); + self.codegen_call_helper(0, args, call_range, None)?; + return Ok(()); + } + // Check for super() method call optimization if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) { // super().method() or super(cls, self).method() optimization // CALL path: [global_super, class, self] → LOAD_SUPER_METHOD → [method, self] - // CALL_FUNCTION_EX path: [global_super, class, self] → LOAD_SUPER_ATTR → [attr] let ast::Expr::Call(ast::ExprCall { func: super_func, .. }) = value.as_ref() @@ -8677,41 +9518,21 @@ impl Compiler { func.range(), attr.as_str(), ); - self.set_source_range(attr_access_range); + self.set_source_range(func.range()); let idx = self.name(attr.as_str()); - if uses_ex_call { - self.set_source_range(func.range()); - match super_type { - SuperCallType::TwoArg { .. } => { - self.emit_load_super_attr(idx); - } - SuperCallType::ZeroArg => { - self.emit_load_zero_super_attr(idx); - } + match super_type { + SuperCallType::TwoArg { .. } => { + self.emit_load_super_method(idx); } - // CPython's Attribute_kind super path emits an attr-line - // NOP after LOAD_SUPER_ATTR, even when the call later uses - // CALL_FUNCTION_EX for starred arguments. - self.set_source_range(attr_access_range); - emit!(self, Instruction::Nop); - self.set_source_range(func.range()); - emit!(self, Instruction::PushNull); - self.codegen_call_helper(0, args, call_range, None)?; - } else { - match super_type { - SuperCallType::TwoArg { .. } => { - self.emit_load_super_method(idx); - } - SuperCallType::ZeroArg => { - self.emit_load_zero_super_method(idx); - } + SuperCallType::ZeroArg => { + self.emit_load_zero_super_method(idx); } - // NOP for line tracking at .method( line - self.set_source_range(attr_access_range); - emit!(self, Instruction::Nop); - // CALL at .method( line (not the full expression line) - self.codegen_call_helper(0, args, method_call_range, Some(attr_access_range))?; } + // NOP for line tracking at .method( line + self.set_source_range(attr_access_range); + emit!(self, Instruction::Nop); + // CALL at .method( line (not the full expression line) + self.compile_method_call_arguments(args, method_call_range, attr_access_range)?; } else { self.compile_expression(value)?; let idx = self.name(attr.as_str()); @@ -8726,28 +9547,15 @@ impl Compiler { attr.as_str(), ); self.set_source_range(attr_access_range); - // Imported names and CALL_FUNCTION_EX-style calls use plain - // LOAD_ATTR + PUSH_NULL; other names use method-call mode. - // Check current scope and enclosing scopes for IMPORTED flag. - let is_import = matches!(value.as_ref(), ast::Expr::Name(ast::ExprName { id, .. }) - if self.is_name_imported(id.as_str())); - if is_import || uses_ex_call { - self.emit_load_attr(idx); - emit!(self, Instruction::PushNull); - } else { - self.emit_load_attr_method(idx); - } - if is_import || uses_ex_call { - self.codegen_call_helper(0, args, call_range, None)?; - } else { - self.codegen_call_helper(0, args, method_call_range, Some(attr_access_range))?; - } + self.emit_load_attr_method(idx); + self.compile_method_call_arguments(args, method_call_range, attr_access_range)?; } } else if let Some(kind) = (!uses_ex_call) .then(|| self.detect_builtin_generator_call(func, args)) .flatten() { let skip_normal_call = self.new_block(); + self.check_caller(func)?; self.compile_expression(func)?; self.optimize_builtin_generator_call( kind, @@ -8770,6 +9578,7 @@ impl Compiler { .then(|| self.cpython_sync_genexpr_call_name(func, args)) .flatten() .is_some(); + self.check_caller(func)?; self.compile_expression(func)?; if sync_genexpr_call_name { // CPython `maybe_optimize_function_call()` creates and uses @@ -8781,6 +9590,7 @@ impl Compiler { .use_raw_instr_sequence_label(skip_optimization); unwrap_internal(self, result); } + self.set_source_range(func.range()); emit!(self, Instruction::PushNull); self.codegen_call_helper(0, args, call_range, None)?; let result = self @@ -8802,6 +9612,24 @@ impl Compiler { has_starred || has_double_star || too_big } + /// Reject duplicate keyword-argument names in a call. + fn validate_keywords(&mut self, keywords: &[ast::Keyword]) -> CompileResult<()> { + for (i, keyword) in keywords.iter().enumerate() { + let Some(arg) = &keyword.arg else { + continue; + }; + for other in &keywords[i + 1..] { + if other.arg.as_ref() == Some(arg) { + return Err(self.error_ranged( + CodegenErrorType::SyntaxError(format!("keyword argument repeated: {arg}")), + other.range, + )); + } + } + } + Ok(()) + } + /// Compile subkwargs: emit key-value pairs for BUILD_MAP fn codegen_subkwargs( &mut self, @@ -8817,8 +9645,8 @@ impl Compiler { let big = n * 2 > STACK_USE_GUIDELINE as usize; if big { - self.set_source_range(call_range); emit!(self, Instruction::BuildMap { count: 0 }); + self.set_no_location(); } for kw in &keywords[begin..end] { @@ -8830,8 +9658,8 @@ impl Compiler { self.compile_expression(&kw.value)?; if big { - self.set_source_range(call_range); emit!(self, Instruction::MapAdd { i: 1 }); + self.set_no_location(); } } @@ -8853,15 +9681,33 @@ impl Compiler { call_range: TextRange, kw_names_range: Option, ) -> CompileResult<()> { - let nelts = arguments.args.len(); - let nkwelts = arguments.keywords.len(); + self.codegen_call_helper_impl( + additional_positional, + &arguments.args, + &arguments.keywords, + call_range, + kw_names_range, + None, + ) + } + + fn codegen_call_helper_impl( + &mut self, + additional_positional: u32, + args: &[ast::Expr], + keywords: &[ast::Keyword], + call_range: TextRange, + kw_names_range: Option, + injected_arg: Option<&str>, + ) -> CompileResult<()> { + self.validate_keywords(keywords)?; + + let nelts = args.len(); + let nkwelts = keywords.len(); // Check if we have starred args or **kwargs - let has_starred = arguments - .args - .iter() - .any(|arg| matches!(arg, ast::Expr::Starred(_))); - let has_double_star = arguments.keywords.iter().any(|k| k.arg.is_none()); + let has_starred = args.iter().any(|arg| matches!(arg, ast::Expr::Starred(_))); + let has_double_star = keywords.iter().any(|k| k.arg.is_none()); // Check if exceeds CPython's stack-use guideline. // With CALL_KW, kwargs values go on stack but keys go in a const tuple, @@ -8872,22 +9718,29 @@ impl Compiler { // Simple call path: no * or ** args let implicit_generator_range = if additional_positional == 0 && nelts == 1 && nkwelts == 0 { - self.cpython_implicit_call_generator_range(&arguments.args[0]) + self.cpython_implicit_call_generator_range(&args[0]) } else { None }; - for arg in &arguments.args { + for arg in args { if let Some(range) = implicit_generator_range { self.compile_expression_with_generator_range(arg, range)?; } else { self.compile_expression(arg)?; } } + let injected_count = if let Some(injected_arg) = injected_arg { + self.set_source_range(call_range); + self.load_name(injected_arg)?; + 1 + } else { + 0 + }; if nkwelts > 0 { // Compile keyword values and build kwnames tuple let mut kwarg_names = Vec::with_capacity(nkwelts); - for keyword in &arguments.keywords { + for keyword in keywords { kwarg_names.push(ConstantData::Str { value: keyword.arg.as_ref().unwrap().as_str().into(), }); @@ -8901,24 +9754,23 @@ impl Compiler { }); self.set_source_range(call_range); - let argc = additional_positional + nelts.to_u32() + nkwelts.to_u32(); + let argc = + additional_positional + nelts.to_u32() + injected_count + nkwelts.to_u32(); emit!(self, Instruction::CallKw { argc }); } else { self.set_source_range(call_range); - let argc = additional_positional + nelts.to_u32(); + let argc = additional_positional + nelts.to_u32() + injected_count; emit!(self, Instruction::Call { argc }); } } else { // ex_call path: has * or ** args // Compile positional arguments - if additional_positional == 0 - && nelts == 1 - && matches!(arguments.args[0], ast::Expr::Starred(_)) + if additional_positional == 0 && nelts == 1 && matches!(args[0], ast::Expr::Starred(_)) { // Single starred arg: pass value directly to CallFunctionEx. // Runtime will convert to tuple and validate with function name. - if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = &arguments.args[0] { + if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = &args[0] { self.compile_expression(value)?; } } else { @@ -8928,14 +9780,15 @@ impl Compiler { // LIST_EXTEND, tuple=1)`, even when the only reason for the // ex-call path is too many non-starred positional arguments. self.set_source_range(call_range); - self.starunpack_helper( - &arguments.args, + self.starunpack_helper_impl( + args, + injected_arg, additional_positional, CollectionType::Tuple, )?; } - self.compile_call_function_ex_keywords(&arguments.keywords, call_range)?; + self.compile_call_function_ex_keywords(keywords, call_range)?; self.set_source_range(call_range); emit!(self, Instruction::CallFunctionEx); @@ -9180,7 +10033,22 @@ impl Compiler { { let _ = self.push_symbol_table()?; } - let _ = self.pop_symbol_table(); + self.pop_symbol_table(); + Ok(()) + } + + fn consume_function_annotation_symbol_table_if_used(&mut self) -> CompileResult<()> { + if !self.next_function_annotation_symbol_table_uses_annotations() { + return Ok(()); + } + if !self.push_annotation_symbol_table() { + let current_table = self.current_symbol_table(); + return Err(self.error(CodegenErrorType::SyntaxError(format!( + "no annotation symbol table available in {} (type: {:?})", + current_table.name, current_table.typ + )))); + } + self.pop_annotation_symbol_table(); Ok(()) } @@ -9190,20 +10058,62 @@ impl Compiler { ) -> CompileResult<()> { use ast::visitor::Visitor; - struct SkippedScopeVisitor<'a> { - compiler: &'a mut Compiler, + struct SkippedScopeVisitor<'a, 'warnings> { + compiler: &'a mut Compiler<'warnings>, error: Option, } - impl SkippedScopeVisitor<'_> { + impl SkippedScopeVisitor<'_, '_> { fn consume_scope(&mut self) { if self.error.is_none() { self.error = self.compiler.consume_next_sub_table().err(); } } + + fn consume_inlined_comprehension_scope(&mut self) -> bool { + if self.error.is_some() { + return false; + } + let Some(current_table) = self.compiler.symbol_table_stack.last_mut() else { + return false; + }; + if current_table.next_inlined_comprehension_block + < current_table.inlined_comprehension_blocks.len() + { + current_table.next_inlined_comprehension_block += 1; + true + } else { + false + } + } + + fn visit_comprehension_tail( + &mut self, + elt1: &ast::Expr, + elt2: Option<&ast::Expr>, + generators: &[ast::Comprehension], + ) { + if let Some(outermost) = generators.first() { + self.visit_expr(&outermost.target); + for if_expr in &outermost.ifs { + self.visit_expr(if_expr); + } + } + for generator in generators.iter().skip(1) { + self.visit_expr(&generator.target); + self.visit_expr(&generator.iter); + for if_expr in &generator.ifs { + self.visit_expr(if_expr); + } + } + if let Some(elt2) = elt2 { + self.visit_expr(elt2); + } + self.visit_expr(elt1); + } } - impl ast::visitor::Visitor<'_> for SkippedScopeVisitor<'_> { + impl ast::visitor::Visitor<'_> for SkippedScopeVisitor<'_, '_> { fn visit_expr(&mut self, expr: &ast::Expr) { if self.error.is_some() { return; @@ -9227,19 +10137,41 @@ impl Compiler { } self.consume_scope(); } - ast::Expr::ListComp(ast::ExprListComp { generators, .. }) - | ast::Expr::SetComp(ast::ExprSetComp { generators, .. }) - | ast::Expr::Generator(ast::ExprGenerator { generators, .. }) => { + ast::Expr::Generator(ast::ExprGenerator { generators, .. }) => { if let Some(first) = generators.first() { self.visit_expr(&first.iter); } self.consume_scope(); } - ast::Expr::DictComp(ast::ExprDictComp { generators, .. }) => { + ast::Expr::ListComp(ast::ExprListComp { + elt, generators, .. + }) + | ast::Expr::SetComp(ast::ExprSetComp { + elt, generators, .. + }) => { + if let Some(first) = generators.first() { + self.visit_expr(&first.iter); + } + if self.consume_inlined_comprehension_scope() { + self.visit_comprehension_tail(elt, None, generators); + } else { + self.consume_scope(); + } + } + ast::Expr::DictComp(ast::ExprDictComp { + key, + value, + generators, + .. + }) => { if let Some(first) = generators.first() { self.visit_expr(&first.iter); } - self.consume_scope(); + if self.consume_inlined_comprehension_scope() { + self.visit_comprehension_tail(key, Some(value), generators); + } else { + self.consume_scope(); + } } _ => ast::visitor::walk_expr(self, expr), } @@ -9258,88 +10190,392 @@ impl Compiler { } } - fn peek_next_sub_table_after_skipped_nested_scopes_in_expr( - &mut self, - expression: &ast::Expr, - ) -> CompileResult { - let saved_cursor = self - .symbol_table_stack - .last() - .expect("no current symbol table") - .next_sub_table; - let result = (|| { - self.consume_skipped_nested_scopes_in_expr(expression)?; - let current_table = self - .symbol_table_stack - .last() - .expect("no current symbol table"); - if let Some(table) = current_table.sub_tables.get(current_table.next_sub_table) { - Ok(table.clone()) - } else { - let name = current_table.name.clone(); - let typ = current_table.typ; - Err(self.error(CodegenErrorType::SyntaxError(format!( - "no symbol table available in {name} (type: {typ:?})" - )))) - } - })(); - self.symbol_table_stack - .last_mut() - .expect("no current symbol table") - .next_sub_table = saved_cursor; - result - } - - fn push_output_with_symbol_table( + fn consume_skipped_nested_scopes_in_parameter_defaults( &mut self, - table: SymbolTable, - flags: bytecode::CodeFlags, - posonlyarg_count: u32, - arg_count: u32, - kwonlyarg_count: u32, - obj_name: &str, + parameters: &ast::Parameters, ) -> CompileResult<()> { - let scope_type = table.typ; - self.symbol_table_stack.push(table); - - let key = self.symbol_table_stack.len() - 1; - let lineno = self.get_source_line_number().get(); - self.enter_scope(obj_name, scope_type, key, lineno.to_u32())?; - - if let Some(info) = self.code_stack.last_mut() { - info.flags = flags - | (info.flags - & (bytecode::CodeFlags::NESTED - | bytecode::CodeFlags::METHOD - | bytecode::CodeFlags::FUTURE_ANNOTATIONS)); - info.metadata.argcount = arg_count; - info.metadata.posonlyargcount = posonlyarg_count; - info.metadata.kwonlyargcount = kwonlyarg_count; + for default in parameters + .posonlyargs + .iter() + .chain(¶meters.args) + .chain(¶meters.kwonlyargs) + .filter_map(|arg| arg.default.as_deref()) + { + self.consume_skipped_nested_scopes_in_expr(default)?; } Ok(()) } - #[expect(clippy::too_many_arguments, reason = "ignore warning for now")] - fn compile_comprehension( + fn consume_skipped_nested_scopes_in_statements( &mut self, - name: &str, - init_collection: Option, - generators: &[ast::Comprehension], - compile_element: &dyn Fn(&mut Self, usize) -> CompileResult<()>, - comprehension_type: ComprehensionType, - element_contains_await: bool, - comprehension_range: TextRange, - element_range: TextRange, - outer_backedge_range: TextRange, + statements: &[ast::Stmt], ) -> CompileResult<()> { - let prev_ctx = self.ctx; - let has_an_async_gen = generators.iter().any(|g| g.is_async); + use ast::visitor::Visitor; + + struct SkippedStatementScopeVisitor<'a, 'warnings> { + compiler: &'a mut Compiler<'warnings>, + error: Option, + } + + impl SkippedStatementScopeVisitor<'_, '_> { + fn consume_scope(&mut self) { + if self.error.is_none() { + self.error = self.compiler.consume_next_sub_table().err(); + } + } + + fn consume_function_annotation_scope_if_used(&mut self) { + if self.error.is_none() { + self.error = self + .compiler + .consume_function_annotation_symbol_table_if_used() + .err(); + } + } + + fn visit_parameter_defaults(&mut self, parameters: &ast::Parameters) { + for default in parameters + .posonlyargs + .iter() + .chain(¶meters.args) + .chain(¶meters.kwonlyargs) + .filter_map(|arg| arg.default.as_deref()) + { + self.visit_expr(default); + } + } + + fn visit_decorators(&mut self, decorators: &[ast::Decorator]) { + for decorator in decorators { + self.visit_expr(&decorator.expression); + } + } + + fn visit_arguments(&mut self, arguments: &ast::Arguments) { + for arg in &arguments.args { + self.visit_expr(arg); + } + for keyword in &arguments.keywords { + self.visit_expr(&keyword.value); + } + } + } + + impl ast::visitor::Visitor<'_> for SkippedStatementScopeVisitor<'_, '_> { + fn visit_stmt(&mut self, stmt: &ast::Stmt) { + if self.error.is_some() { + return; + } + + match stmt { + ast::Stmt::FunctionDef(ast::StmtFunctionDef { + parameters, + decorator_list, + type_params, + .. + }) => { + self.visit_parameter_defaults(parameters); + self.visit_decorators(decorator_list); + if type_params.is_some() { + self.consume_scope(); + } else { + self.consume_function_annotation_scope_if_used(); + self.consume_scope(); + } + } + ast::Stmt::ClassDef(ast::StmtClassDef { + arguments, + decorator_list, + type_params, + .. + }) => { + self.visit_decorators(decorator_list); + if type_params.is_some() { + self.consume_scope(); + } + if let Some(arguments) = arguments { + self.visit_arguments(arguments); + } + self.consume_scope(); + } + ast::Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { + if type_params.is_some() { + self.consume_scope(); + } + self.consume_scope(); + } + ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => { + self.visit_expr(target); + if let Some(value) = value { + self.visit_expr(value); + } + } + ast::Stmt::If(ast::StmtIf { + test, + body, + elif_else_clauses, + .. + }) => { + self.visit_expr(test); + for stmt in body { + self.visit_stmt(stmt); + } + for clause in elif_else_clauses { + if let Some(test) = &clause.test { + self.visit_expr(test); + } + for stmt in &clause.body { + self.visit_stmt(stmt); + } + } + } + ast::Stmt::Try(ast::StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) => { + for stmt in body { + self.visit_stmt(stmt); + } + for handler in handlers { + self.visit_except_handler(handler); + } + for stmt in orelse { + self.visit_stmt(stmt); + } + for stmt in finalbody { + self.visit_stmt(stmt); + } + } + _ => ast::visitor::walk_stmt(self, stmt), + } + } + + fn visit_expr(&mut self, expr: &ast::Expr) { + if self.error.is_some() { + return; + } + self.error = self + .compiler + .consume_skipped_nested_scopes_in_expr(expr) + .err(); + } + + fn visit_except_handler(&mut self, handler: &ast::ExceptHandler) { + if self.error.is_some() { + return; + } + let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + body, + .. + }) = handler; + if let Some(type_) = type_ { + self.visit_expr(type_); + } + for stmt in body { + self.visit_stmt(stmt); + } + } + } + + let mut visitor = SkippedStatementScopeVisitor { + compiler: self, + error: None, + }; + for statement in statements { + visitor.visit_stmt(statement); + } + if let Some(err) = visitor.error { + Err(err) + } else { + Ok(()) + } + } + + fn consume_skipped_nested_scopes_in_except_handlers( + &mut self, + handlers: &[ast::ExceptHandler], + ) -> CompileResult<()> { + use ast::visitor::Visitor; + + struct SkippedHandlerScopeVisitor<'a, 'warnings> { + compiler: &'a mut Compiler<'warnings>, + error: Option, + } + + impl ast::visitor::Visitor<'_> for SkippedHandlerScopeVisitor<'_, '_> { + fn visit_expr(&mut self, expr: &ast::Expr) { + if self.error.is_some() { + return; + } + self.error = self + .compiler + .consume_skipped_nested_scopes_in_expr(expr) + .err(); + } + + fn visit_stmt(&mut self, stmt: &ast::Stmt) { + if self.error.is_some() { + return; + } + self.error = self + .compiler + .consume_skipped_nested_scopes_in_statements(slice::from_ref(stmt)) + .err(); + } + } + + let mut visitor = SkippedHandlerScopeVisitor { + compiler: self, + error: None, + }; + for handler in handlers { + visitor.visit_except_handler(handler); + if visitor.error.is_some() { + break; + } + } + if let Some(err) = visitor.error { + Err(err) + } else { + Ok(()) + } + } + + fn current_symbol_table_cursors(&self) -> SymbolTableCursors { + let table = self + .symbol_table_stack + .last() + .expect("no current symbol table"); + SymbolTableCursors { + sub_table: table.next_sub_table, + hidden_annotation_block: table.next_hidden_annotation_block, + inlined_comprehension_block: table.next_inlined_comprehension_block, + } + } + + fn set_symbol_table_cursors(&mut self, cursors: SymbolTableCursors) { + let table = self + .symbol_table_stack + .last_mut() + .expect("no current symbol table"); + table.next_sub_table = cursors.sub_table; + table.next_hidden_annotation_block = cursors.hidden_annotation_block; + table.next_inlined_comprehension_block = cursors.inlined_comprehension_block; + } + + fn lookup_comprehension_symbol_table_after_skipped_nested_scopes_in_expr( + &mut self, + expression: &ast::Expr, + comprehension_type: ComprehensionType, + ) -> CompileResult<(SymbolTable, ComprehensionSymbolSource)> { + let saved_cursor = self + .symbol_table_stack + .last() + .expect("no current symbol table") + .next_sub_table; + let saved_inlined_cursor = self + .symbol_table_stack + .last() + .expect("no current symbol table") + .next_inlined_comprehension_block; + let result = (|| { + self.consume_skipped_nested_scopes_in_expr(expression)?; + let current_table = self + .symbol_table_stack + .last() + .expect("no current symbol table"); + if comprehension_type != ComprehensionType::Generator + && let Some(table) = current_table + .inlined_comprehension_blocks + .get(current_table.next_inlined_comprehension_block) + { + return Ok((table.clone(), ComprehensionSymbolSource::Inlined)); + } + if let Some(table) = current_table.sub_tables.get(current_table.next_sub_table) { + Ok((table.clone(), ComprehensionSymbolSource::Child)) + } else { + let name = current_table.name.clone(); + let typ = current_table.typ; + Err(self.error(CodegenErrorType::SyntaxError(format!( + "no symbol table available in {name} (type: {typ:?})" + )))) + } + })(); + self.symbol_table_stack + .last_mut() + .expect("no current symbol table") + .next_sub_table = saved_cursor; + self.symbol_table_stack + .last_mut() + .expect("no current symbol table") + .next_inlined_comprehension_block = saved_inlined_cursor; + result + } + + fn push_output_with_symbol_table( + &mut self, + table: SymbolTable, + flags: bytecode::CodeFlags, + posonlyarg_count: u32, + arg_count: u32, + kwonlyarg_count: u32, + obj_name: &str, + ) -> CompileResult<()> { + let scope_type = table.typ; + self.symbol_table_stack.push(table); + + let key = self.symbol_table_stack.len() - 1; + let lineno = self.get_source_line_number().get(); + self.enter_scope(obj_name, scope_type, key, lineno.to_u32())?; + + if let Some(info) = self.code_stack.last_mut() { + info.flags = flags + | (info.flags + & (bytecode::CodeFlags::NESTED + | bytecode::CodeFlags::METHOD + | 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)); + info.metadata.argcount = arg_count; + info.metadata.posonlyargcount = posonlyarg_count; + info.metadata.kwonlyargcount = kwonlyarg_count; + } + Ok(()) + } + + #[expect(clippy::too_many_arguments, reason = "ignore warning for now")] + fn compile_comprehension( + &mut self, + name: &str, + init_collection: Option, + generators: &[ast::Comprehension], + compile_element: &dyn Fn(&mut Self, usize) -> CompileResult<()>, + comprehension_type: ComprehensionType, + element_contains_await: bool, + comprehension_range: TextRange, + element_range: TextRange, + outer_backedge_range: TextRange, + ) -> CompileResult<()> { + let prev_ctx = self.ctx; + let has_an_async_gen = generators.iter().any(|g| g.is_async); + let is_top_level_await_context = self.opts.allow_top_level_await + && prev_ctx.func == FunctionContext::NoFunction + && !prev_ctx.in_class; // Check for async comprehension outside async function (list/set/dict only, not generator expressions) // Use in_async_scope to allow nested async comprehensions inside an async function if comprehension_type != ComprehensionType::Generator && (has_an_async_gen || element_contains_await) && !prev_ctx.in_async_scope + && !is_top_level_await_context { return Err(self.error(CodegenErrorType::InvalidAsyncComprehension)); } @@ -9350,7 +10586,7 @@ impl Compiler { let is_async_list_set_dict_comprehension = comprehension_type != ComprehensionType::Generator && (has_an_async_gen || element_contains_await) - && prev_ctx.in_async_scope; + && (prev_ctx.in_async_scope || is_top_level_await_context); let is_async_generator_comprehension = comprehension_type == ComprehensionType::Generator && (has_an_async_gen || element_contains_await); @@ -9362,8 +10598,11 @@ impl Compiler { // We must have at least one generator: assert!(!generators.is_empty()); let outermost = &generators[0]; - let comp_table = - self.peek_next_sub_table_after_skipped_nested_scopes_in_expr(&outermost.iter)?; + let (comp_table, comp_source) = self + .lookup_comprehension_symbol_table_after_skipped_nested_scopes_in_expr( + &outermost.iter, + comprehension_type, + )?; let is_inlined = self.is_inlined_comprehension_context(comprehension_type, &comp_table); @@ -9379,6 +10618,7 @@ impl Compiler { generators, compile_element, (comprehension_range, element_range, outer_backedge_range), + comp_source, ); } @@ -9539,9 +10779,6 @@ impl Compiler { is_async, end_async_for_target, } => { - self.set_source_range(backedge_range); - emit!(self, PseudoInstruction::Jump { delta: loop_block }); - self.use_cpython_label_block(if_cleanup_block); self.set_source_range(backedge_range); emit!(self, PseudoInstruction::Jump { delta: loop_block }); @@ -9570,6 +10807,7 @@ impl Compiler { if return_none { self.emit_return_const_no_location(ConstantData::None); } else { + self.set_source_range(comprehension_range); self.emit_return_value(); } @@ -9586,6 +10824,7 @@ impl Compiler { emit!(self, Instruction::Reraise { depth: 1u32 }); self.set_no_location(); } + self.emit_return_const_no_location(ConstantData::None); let code = self.exit_scope(); @@ -9623,37 +10862,32 @@ impl Compiler { generators: &[ast::Comprehension], compile_element: &dyn Fn(&mut Self, usize) -> CompileResult<()>, ranges: (TextRange, TextRange, TextRange), + comp_source: ComprehensionSymbolSource, ) -> CompileResult<()> { let (comprehension_range, element_range, outer_backedge_range) = ranges; - fn collect_bound_names(target: &ast::Expr, out: &mut Vec) { - match target { - ast::Expr::Name(ast::ExprName { id, .. }) => out.push(id.to_string()), - ast::Expr::Tuple(ast::ExprTuple { elts, .. }) - | ast::Expr::List(ast::ExprList { elts, .. }) => { - for elt in elts { - collect_bound_names(elt, out); - } - } - ast::Expr::Starred(ast::ExprStarred { value, .. }) => { - collect_bound_names(value, out); - } - _ => {} - } - } - // Compile the outermost iterator first. Its expression may reference // nested scopes (e.g. lambdas) whose sub_tables sit at the current // position in the parent's list. Those must be consumed before we // splice in the comprehension's own children. self.compile_comprehension_iter(&generators[0])?; - self.symbol_table_stack - .last_mut() - .expect("no current symbol table") - .next_sub_table += 1; - - let was_in_inlined_comp = self.current_code_info().in_inlined_comp; + match comp_source { + ComprehensionSymbolSource::Child => { + self.symbol_table_stack + .last_mut() + .expect("no current symbol table") + .next_sub_table += 1; + } + ComprehensionSymbolSource::Inlined => { + self.symbol_table_stack + .last_mut() + .expect("no current symbol table") + .next_inlined_comprehension_block += 1; + } + } + + let was_in_inlined_comp = self.current_code_info().in_inlined_comp; let saved_source_range = self.current_source_range; - let in_class_block = { + let tweak_in_class_block = { let ct = self.current_symbol_table(); ct.typ == CompilerScope::Class && !was_in_inlined_comp }; @@ -9663,9 +10897,13 @@ impl Compiler { let mut changed_fast_hidden = Vec::new(); let result = (|| { - // Splice the comprehension's children (e.g. nested inlined - // comprehensions) into the parent so the compiler can find them. - if !comp_table.sub_tables.is_empty() { + // If the symbol table still carries the inlined comprehension as + // a child, splice its children here. The symtable normally + // performs this splice before codegen, and the Inlined source path + // has already done so. + if matches!(comp_source, ComprehensionSymbolSource::Child) + && !comp_table.sub_tables.is_empty() + { let current_table = self .symbol_table_stack .last_mut() @@ -9675,30 +10913,19 @@ impl Compiler { current_table.sub_tables.insert(insert_pos + i, st.clone()); } } - let mut source_order_bound_names = Vec::new(); - for generator in generators { - collect_bound_names(&generator.target, &mut source_order_bound_names); - } - let mut pushed_locals: Vec = Vec::new(); - for name in source_order_bound_names - .into_iter() - .chain(comp_table.symbols.keys().cloned()) - { - if pushed_locals.iter().any(|existing| existing == &name) { - continue; + let mut fast_hidden_locals: Vec = Vec::new(); + for (name, sym) in &comp_table.symbols { + if sym.flags.contains(SymbolFlags::DEF_PARAM) { + continue; // skip .0 } - if let Some(sym) = comp_table.symbols.get(&name) { - if sym.flags.contains(SymbolFlags::PARAMETER) { - continue; // skip .0 - } - let is_local = sym - .flags - .intersects(SymbolFlags::ASSIGNED | SymbolFlags::ITER) - && !sym.flags.contains(SymbolFlags::NONLOCAL); - if is_local { - pushed_locals.push(name); - } + let is_local = sym.flags.contains(SymbolFlags::DEF_LOCAL) + && !sym.flags.contains(SymbolFlags::DEF_NONLOCAL); + if is_local { + pushed_locals.push(name.clone()); + } + if is_local || tweak_in_class_block { + fast_hidden_locals.push(name.clone()); } } @@ -9707,7 +10934,7 @@ impl Compiler { // module/class scopes, also enable temporary fast locals for // comprehension-bound names only. for (name, comp_sym) in &comp_table.symbols { - if comp_sym.flags.contains(SymbolFlags::PARAMETER) { + if comp_sym.flags.contains(SymbolFlags::DEF_PARAM) { continue; // skip .0 } let comp_scope = comp_sym.scope; @@ -9718,7 +10945,7 @@ impl Compiler { if (comp_scope != outer_scope && comp_scope != SymbolScope::Free && !(comp_scope == SymbolScope::Cell && outer_scope == SymbolScope::Free)) - || in_class_block + || tweak_in_class_block { temp_symbols.insert(name.clone(), outer_sym.clone()); let current_table = @@ -9728,7 +10955,7 @@ impl Compiler { } } if !self.ctx.in_func() { - for name in &pushed_locals { + for name in &fast_hidden_locals { if self .current_code_info() .metadata @@ -10027,18 +11254,41 @@ impl Compiler { if let DoneWithFuture::Yes = self.done_with_future_stmts { return Err(self.error(CodegenErrorType::InvalidFuturePlacement)); } + self.done_with_future_stmts = DoneWithFuture::DoneWithDoc; + for feature in features { - match feature.name.as_str() { - // Python 3 features; we've already implemented them by default - "nested_scopes" | "generators" | "division" | "absolute_import" - | "with_statement" | "print_function" | "unicode_literals" | "generator_stop" => {} - "annotations" => self.future_annotations = true, - other => { + let future_feature = feature.name.as_str().try_into().map_err(|name| { + self.error_ranged(CodegenErrorType::InvalidFutureFeature(name), feature.range) + })?; + + match future_feature { + FutureFeature::Braces => { return Err( - self.error(CodegenErrorType::InvalidFutureFeature(other.to_owned())) + self.error_ranged(CodegenErrorType::InvalidFutureBraces, feature.range) ); } + FutureFeature::Annotations => { + self.future_annotations = true; + self.future_features + .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + self.current_code_info() + .flags + .insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + } + FutureFeature::BarryAsFLUFL => { + // We do not support Barry-as-BDFL parser mode yet. This is a nop for now. + } + FutureFeature::AbsoluteImport + | FutureFeature::Division + | FutureFeature::GeneratorStop + | FutureFeature::Generators + | FutureFeature::NestedScopes + | FutureFeature::PrintFunction + | FutureFeature::UnicodeLiterals + | FutureFeature::WithStatement => { + // Python 3 features. They are already implemented by default. + } } } Ok(()) @@ -10359,31 +11609,6 @@ impl Compiler { && lhs.exceptiontable == rhs.exceptiontable } - /// Try to fold a collection of constant expressions into a single ConstantData::Tuple. - /// Returns None if any element cannot be folded. - fn try_fold_constant_collection( - &mut self, - elts: &[ast::Expr], - collection_type: CollectionType, - ) -> CompileResult> { - let mut constants = Vec::with_capacity(elts.len()); - for elt in elts { - let Some(constant) = self.try_fold_constant_expr(elt)? else { - return Ok(None); - }; - constants.push(constant); - } - let constant = match collection_type { - CollectionType::Tuple | CollectionType::List => ConstantData::Tuple { - elements: constants, - }, - CollectionType::Set => ConstantData::Frozenset { - elements: constants, - }, - }; - Ok(Some(constant)) - } - fn constant_as_fold_int(constant: &ConstantData) -> Option<(BigInt, bool)> { match constant { ConstantData::Boolean { value } => Some((BigInt::from(u8::from(*value)), true)), @@ -10419,6 +11644,9 @@ impl Compiler { } fn try_fold_constant_expr(&mut self, expr: &ast::Expr) -> CompileResult> { + if let Some(constant) = self.ast_constant_value(expr) { + return Ok(Some(constant)); + } Ok(Some(match expr { ast::Expr::NumberLiteral(num) => match &num.value { ast::Number::Int(int) => ConstantData::Integer { @@ -10607,6 +11835,9 @@ impl Compiler { &mut self, expr: &ast::Expr, ) -> CompileResult> { + if let Some(constant) = self.ast_constant_value(expr) { + return Ok(Some(constant)); + } Ok(Some(match expr { ast::Expr::NumberLiteral(num) => match &num.value { ast::Number::Int(int) => ConstantData::Integer { @@ -10630,24 +11861,76 @@ impl Compiler { })) } + fn try_compile_match_mapping_key_direct_constant( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.ast_constant_value(expr) { + return Ok(match constant { + ConstantData::Integer { .. } + | ConstantData::Float { .. } + | ConstantData::Bytes { .. } + | ConstantData::Complex { .. } + | ConstantData::Str { .. } + | ConstantData::Boolean { .. } + | ConstantData::None => Some(constant), + _ => None, + }); + } + if matches!( + expr, + ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) + ) { + return self.try_compile_ast_constant(expr); + } + self.try_compile_match_pattern_direct_literal(expr) + } + + fn is_unexpected_match_literal_constant(expr: &ast::Expr) -> bool { + if let Some(constant) = expr + .as_constant_expr() + .map(|expr| ast_constant_value_to_constant_data(expr.value.clone())) + { + return matches!( + constant, + ConstantData::Boolean { .. } | ConstantData::None | ConstantData::Ellipsis + ); + } + matches!( + expr, + ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_) + | ast::Expr::EllipsisLiteral(_) + ) + } + + fn try_compile_match_pattern_direct_literal( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.ast_constant_value(expr) { + return Ok(match constant { + ConstantData::Integer { .. } + | ConstantData::Float { .. } + | ConstantData::Bytes { .. } + | ConstantData::Complex { .. } + | ConstantData::Str { .. } => Some(constant), + _ => None, + }); + } + match expr { + ast::Expr::NumberLiteral(_) + | ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) => self.try_compile_ast_constant(expr), + _ => Ok(None), + } + } + fn try_negate_match_pattern_constant(constant: ConstantData) -> Option { match constant { ConstantData::Integer { value } => Some(ConstantData::Integer { value: -value }), ConstantData::Float { value } => Some(ConstantData::Float { value: -value }), ConstantData::Complex { value } => Some(ConstantData::Complex { value: -value }), - ConstantData::Boolean { value } => Some(ConstantData::Integer { - value: -BigInt::from(u8::from(value)), - }), - _ => None, - } - } - - fn constant_as_match_pattern_complex(constant: &ConstantData) -> Option> { - match constant { - ConstantData::Integer { value } => Some(Complex::new(value.to_f64()?, 0.0)), - ConstantData::Float { value } => Some(Complex::new(*value, 0.0)), - ConstantData::Complex { value } => Some(*value), - ConstantData::Boolean { value } => Some(Complex::new(f64::from(u8::from(*value)), 0.0)), _ => None, } } @@ -10657,51 +11940,20 @@ impl Compiler { left: &ConstantData, right: &ConstantData, ) -> Option { - if let (ConstantData::Integer { value: left }, ConstantData::Integer { value: right }) = - (left, right) - { - return match op { - ast::Operator::Add => Some(ConstantData::Integer { - value: left + right, - }), - ast::Operator::Sub => Some(ConstantData::Integer { - value: left - right, - }), - _ => None, - }; - } - - let left_is_complex = matches!(left, ConstantData::Complex { .. }); - let right_is_complex = matches!(right, ConstantData::Complex { .. }); - if left_is_complex || right_is_complex { - let left = Self::constant_as_match_pattern_complex(left)?; - let right = Self::constant_as_match_pattern_complex(right)?; - let value = match op { - ast::Operator::Add => Complex::new(left.re + right.re, left.im + right.im), - ast::Operator::Sub => { - let imag = if !left_is_complex && right_is_complex { - -right.im - } else { - left.im - right.im - }; - Complex::new(left.re - right.re, imag) - } - _ => return None, - }; - return Some(ConstantData::Complex { value }); - } - - let left = Self::constant_as_match_pattern_complex(left)?; - let right = Self::constant_as_match_pattern_complex(right)?; - match op { - ast::Operator::Add => Some(ConstantData::Float { - value: left.re + right.re, - }), - ast::Operator::Sub => Some(ConstantData::Float { - value: left.re - right.re, - }), - _ => None, - } + let left = match left { + ConstantData::Integer { value } => value.to_f64()?, + ConstantData::Float { value } => *value, + _ => return None, + }; + let ConstantData::Complex { value: right } = right else { + return None; + }; + let value = match op { + ast::Operator::Add => Complex::new(left + right.re, right.im), + ast::Operator::Sub => Complex::new(left - right.re, -right.im), + _ => return None, + }; + Some(ConstantData::Complex { value }) } fn try_fold_match_pattern_const_expr( @@ -10717,7 +11969,8 @@ impl Compiler { operand, .. }) => { - let Some(constant) = self.try_compile_ast_constant(operand)? else { + let Some(constant) = self.try_compile_match_pattern_number_constant(operand)? + else { return Ok(None); }; Self::try_negate_match_pattern_constant(constant) @@ -10725,13 +11978,10 @@ impl Compiler { ast::Expr::BinOp(ast::ExprBinOp { left, op, right, .. }) if matches!(op, ast::Operator::Add | ast::Operator::Sub) => { - let Some(left) = (match self.try_fold_match_pattern_const_expr(left)? { - Some(constant) => Some(constant), - None => self.try_compile_ast_constant(left)?, - }) else { + let Some(left) = self.try_compile_match_pattern_signed_real_constant(left)? else { return Ok(None); }; - let Some(right) = self.try_compile_ast_constant(right)? else { + let Some(right) = self.try_compile_match_pattern_imaginary_constant(right)? else { return Ok(None); }; Self::try_fold_match_pattern_binop(*op, &left, &right) @@ -10740,8 +11990,74 @@ impl Compiler { }) } + fn try_compile_match_pattern_signed_real_constant( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.try_compile_match_pattern_real_constant(expr)? { + return Ok(Some(constant)); + } + let ast::Expr::UnaryOp(ast::ExprUnaryOp { + op: ast::UnaryOp::USub, + operand, + .. + }) = expr + else { + return Ok(None); + }; + let Some(constant) = self.try_compile_match_pattern_real_constant(operand)? else { + return Ok(None); + }; + Ok(Self::try_negate_match_pattern_constant(constant)) + } + + fn try_compile_match_pattern_real_constant( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + let Some(constant) = self.try_compile_match_pattern_number_constant(expr)? else { + return Ok(None); + }; + Ok(match constant { + ConstantData::Integer { .. } | ConstantData::Float { .. } => Some(constant), + _ => None, + }) + } + + fn try_compile_match_pattern_imaginary_constant( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + let Some(constant) = self.try_compile_match_pattern_number_constant(expr)? else { + return Ok(None); + }; + Ok(match constant { + ConstantData::Complex { .. } => Some(constant), + _ => None, + }) + } + + fn try_compile_match_pattern_number_constant( + &mut self, + expr: &ast::Expr, + ) -> CompileResult> { + if let Some(constant) = self.ast_constant_value(expr) { + return Ok(match constant { + ConstantData::Integer { .. } + | ConstantData::Float { .. } + | ConstantData::Complex { .. } => Some(constant), + _ => None, + }); + } + match expr { + ast::Expr::NumberLiteral(_) => self.try_compile_ast_constant(expr), + _ => Ok(None), + } + } + fn compile_match_pattern_expr(&mut self, expr: &ast::Expr) -> CompileResult<()> { if let Some(constant) = self.try_fold_match_pattern_const_expr(expr)? { + self.set_source_range(expr.range()); self.emit_load_const(constant); } else { self.compile_expression(expr)?; @@ -10763,7 +12079,7 @@ impl Compiler { if [lower, upper, step] .into_iter() .flatten() - .any(|expr| !expr.is_constant()) + .any(|expr| !self.is_constant_expr(expr)) { return Ok(None); } @@ -10870,6 +12186,12 @@ impl Compiler { emit!(self, Instruction::ReturnValue) } + fn allows_top_level_await_in_current_context(&self) -> bool { + self.opts.allow_top_level_await + && self.ctx.func == FunctionContext::NoFunction + && !self.ctx.in_class + } + fn current_code_info(&mut self) -> &mut ir::CodeInfo { self.code_stack.last_mut().expect("no code on stack") } @@ -10951,10 +12273,11 @@ impl Compiler { debug_assert!(loop_fblock.fb_block.is_jump_target_label()); loop_fblock.fb_block }; - if let Some(loc) = unwind_loc { + let jump_is_artificial = if let Some(loc) = unwind_loc { self.set_source_range(loc); + false } else { - self.set_source_range(range); + true }; self.emit_jump_label( PseudoInstruction::Jump { @@ -10962,7 +12285,7 @@ impl Compiler { }, target_label, ); - if unwind_loc.is_none() { + if jump_is_artificial { self.set_no_location(); } self.set_source_range(prev_source_range); @@ -10978,7 +12301,7 @@ impl Compiler { let code = self.current_code_info(); let cur = code.current_block; if !code.blocks[cur.idx()] - .instructions + .used_instructions() .last() .is_some_and(|instr| instr.instr.is_terminator()) { @@ -11062,7 +12385,7 @@ impl Compiler { unwrap_internal(self, result); let code = self.current_code_info(); let idx = BlockIdx::new(code.blocks.len().to_u32()); - code.blocks.push(ir::Block::default()); + code.blocks.push(Block::default()); let result = code.push_unmapped_instr_sequence_label(); unwrap_internal(self, result); idx @@ -11077,7 +12400,7 @@ impl Compiler { unwrap_internal(self, result); let code = self.current_code_info(); let idx = BlockIdx::new(code.blocks.len().to_u32()); - code.blocks.push(ir::Block::default()); + code.blocks.push(Block::default()); let result = code.push_unlabeled_instr_sequence_block(); unwrap_internal(self, result); idx @@ -11159,7 +12482,7 @@ impl Compiler { if source.line_index(loc_range.start()) == source.line_index(attr_range.end()) { return loc_range; } - let Ok(attr_len) = u32::try_from(attr.len()) else { + let Ok(attr_len) = u32::try_from(attr.chars().count()) else { return TextRange::new(loc_range.start(), loc_range.end()); }; let attr_len = TextSize::new(attr_len); @@ -11262,7 +12585,7 @@ impl Compiler { let fstring_range = fstring.range; let fstring = fstring.value.as_slice(); if self.count_fstring_parts(fstring) > STACK_USE_GUIDELINE { - return self.compile_fstring_parts_joined(fstring); + return self.compile_fstring_parts_joined(fstring, fstring_range); } let mut element_count = 0; @@ -11276,7 +12599,7 @@ impl Compiler { &mut pending_literal_range, &mut pending_literal_no_location, &mut element_count, - false, + None, )?; } self.finish_fstring( @@ -11289,7 +12612,54 @@ impl Compiler { Ok(()) } - fn compile_fstring_parts_joined(&mut self, fstring: &[ast::FStringPart]) -> CompileResult<()> { + fn compile_runtime_joined_str( + &mut self, + fstring: &ast::ExprFString, + values: &[ast::Expr], + ) -> CompileResult<()> { + let range = fstring.range; + let value_count: u32 = values + .len() + .try_into() + .expect("JoinedStr value count overflowed"); + if value_count > STACK_USE_GUIDELINE { + self.set_source_range(range); + self.emit_load_const(ConstantData::Str { + value: Wtf8Buf::new(), + }); + let join_idx = self.get_global_name_index("join"); + self.emit_load_attr_method(join_idx); + emit!(self, Instruction::BuildList { count: 0 }); + for value in values { + self.compile_expression(value)?; + self.set_source_range(range); + emit!(self, Instruction::ListAppend { i: 1 }); + } + self.set_source_range(range); + emit!(self, Instruction::Call { argc: 1 }); + } else { + for value in values { + self.compile_expression(value)?; + } + if value_count > 1 { + self.set_source_range(range); + emit!(self, Instruction::BuildString { count: value_count }); + } else if value_count == 0 { + self.set_source_range(range); + self.emit_load_const(ConstantData::Str { + value: Wtf8Buf::new(), + }); + } + } + Ok(()) + } + + fn compile_fstring_parts_joined( + &mut self, + fstring: &[ast::FStringPart], + fstring_range: TextRange, + ) -> CompileResult<()> { + self.set_source_range(fstring_range); self.emit_load_const(ConstantData::Str { value: Wtf8Buf::new(), }); @@ -11308,7 +12678,7 @@ impl Compiler { &mut pending_literal_range, &mut pending_literal_no_location, &mut element_count, - true, + Some(fstring_range), )?; } self.finish_fstring_join( @@ -11316,6 +12686,7 @@ impl Compiler { pending_literal_range, pending_literal_no_location, element_count, + fstring_range, ); Ok(()) } @@ -11327,7 +12698,7 @@ impl Compiler { pending_literal_range: &mut Option, pending_literal_no_location: &mut bool, element_count: &mut u32, - append_to_join_list: bool, + join_append_range: Option, ) -> CompileResult<()> { match part { ast::FStringPart::Literal(string) => { @@ -11349,7 +12720,7 @@ impl Compiler { pending_literal, (pending_literal_range, pending_literal_no_location), element_count, - append_to_join_list, + join_append_range, ), } } @@ -11369,7 +12740,7 @@ impl Compiler { &mut pending_literal_no_location, &mut element_count, keep_empty, - false, + None, ); if element_count == 0 { @@ -11398,6 +12769,7 @@ impl Compiler { mut pending_literal_range: Option, mut pending_literal_no_location: bool, mut element_count: u32, + fstring_range: TextRange, ) { let keep_empty = element_count == 0; self.emit_pending_fstring_literal( @@ -11406,8 +12778,9 @@ impl Compiler { &mut pending_literal_no_location, &mut element_count, keep_empty, - true, + Some(fstring_range), ); + self.set_source_range(fstring_range); emit!(self, Instruction::Call { argc: 1 }); } @@ -11418,7 +12791,7 @@ impl Compiler { pending_literal_no_location: &mut bool, element_count: &mut u32, keep_empty: bool, - append_to_join_list: bool, + join_append_range: Option, ) { let Some(value) = pending_literal.take() else { return; @@ -11442,7 +12815,8 @@ impl Compiler { self.set_no_location(); } *element_count += 1; - if append_to_join_list { + if let Some(join_append_range) = join_append_range { + self.set_source_range(join_append_range); emit!(self, Instruction::ListAppend { i: 1 }); } } @@ -11517,7 +12891,8 @@ impl Compiler { fstring_range: Option, ) -> CompileResult<()> { if self.count_fstring_elements(flags, fstring_elements) > STACK_USE_GUIDELINE { - return self.compile_fstring_elements_joined(flags, fstring_elements); + let fstring_range = fstring_range.unwrap_or(self.current_source_range); + return self.compile_fstring_elements_joined(flags, fstring_elements, fstring_range); } let mut element_count = 0; @@ -11530,7 +12905,7 @@ impl Compiler { &mut pending_literal, (&mut pending_literal_range, &mut pending_literal_no_location), &mut element_count, - false, + None, )?; self.finish_fstring( pending_literal, @@ -11546,7 +12921,9 @@ impl Compiler { &mut self, flags: ast::FStringFlags, fstring_elements: &ast::InterpolatedStringElements, + fstring_range: TextRange, ) -> CompileResult<()> { + self.set_source_range(fstring_range); self.emit_load_const(ConstantData::Str { value: Wtf8Buf::new(), }); @@ -11564,13 +12941,14 @@ impl Compiler { &mut pending_literal, (&mut pending_literal_range, &mut pending_literal_no_location), &mut element_count, - true, + Some(fstring_range), )?; self.finish_fstring_join( pending_literal, pending_literal_range, pending_literal_no_location, element_count, + fstring_range, ); Ok(()) } @@ -11595,7 +12973,7 @@ impl Compiler { pending_literal: &mut Option, pending_literal_meta: (&mut Option, &mut bool), element_count: &mut u32, - append_to_join_list: bool, + join_append_range: Option, ) -> CompileResult<()> { let (pending_literal_range, pending_literal_no_location) = pending_literal_meta; for element in fstring_elements { @@ -11620,7 +12998,18 @@ impl Compiler { ast::ConversionFlag::Ascii => ConvertValueOparg::Ascii, }; - if let Some(ast::DebugText { leading, trailing }) = &fstring_expr.debug_text { + if let Some(debug_text) = &fstring_expr.debug_text { + let leading = debug_text.leading.as_str(); + let trailing = debug_text.trailing.as_str(); + self.emit_pending_fstring_literal( + pending_literal, + pending_literal_range, + pending_literal_no_location, + element_count, + false, + join_append_range, + ); + let range = fstring_expr.expression.range(); let leading = strip_fstring_debug_comments(leading); let trailing = strip_fstring_debug_comments(trailing); @@ -11640,16 +13029,9 @@ impl Compiler { ); let text: Wtf8Buf = text.into(); - if pending_literal.is_none() { - *pending_literal_range = Some(debug_text_range); - *pending_literal_no_location = false; - *pending_literal = Some(Wtf8Buf::new()); - } else { - Self::extend_pending_literal_range( - pending_literal_range, - debug_text_range, - ); - } + *pending_literal_range = Some(debug_text_range); + *pending_literal_no_location = false; + *pending_literal = Some(Wtf8Buf::new()); pending_literal.as_mut().unwrap().push_wtf8(text.as_ref()); // If debug text is present, apply repr conversion when no `format_spec` specified. @@ -11668,7 +13050,7 @@ impl Compiler { pending_literal_no_location, element_count, false, - append_to_join_list, + join_append_range, ); self.compile_expression(&fstring_expr.expression)?; @@ -11684,27 +13066,37 @@ impl Compiler { } } - match &fstring_expr.format_spec { - Some(format_spec) => { - let format_spec_range = - self.cpython_format_spec_range(format_spec.range); - self.compile_fstring_elements( - flags, - &format_spec.elements, - Some(format_spec_range), - )?; + if let Some(format_spec) = + fstring_expr.runtime_formatted_value_format_spec.as_deref() + { + self.compile_expression(format_spec)?; - self.set_source_range(formatted_value_range); - emit!(self, Instruction::FormatWithSpec); - } - None => { - self.set_source_range(formatted_value_range); - emit!(self, Instruction::FormatSimple); + self.set_source_range(formatted_value_range); + emit!(self, Instruction::FormatWithSpec); + } else { + match &fstring_expr.format_spec { + Some(format_spec) => { + let format_spec_range = + self.cpython_format_spec_range(format_spec.range); + self.compile_fstring_elements( + flags, + &format_spec.elements, + Some(format_spec_range), + )?; + + self.set_source_range(formatted_value_range); + emit!(self, Instruction::FormatWithSpec); + } + None => { + self.set_source_range(formatted_value_range); + emit!(self, Instruction::FormatSimple); + } } } *element_count += 1; - if append_to_join_list { + if let Some(join_append_range) = join_append_range { + self.set_source_range(join_append_range); emit!(self, Instruction::ListAppend { i: 1 }); } } @@ -11750,7 +13142,10 @@ impl Compiler { } } ast::InterpolatedStringElement::Interpolation(fstring_expr) => { - if let Some(ast::DebugText { leading, trailing }) = &fstring_expr.debug_text { + if let Some(debug_text) = &fstring_expr.debug_text { + let leading = debug_text.leading.as_str(); + let trailing = debug_text.trailing.as_str(); + Self::count_pending_fstring_literal(pending_literal, element_count, false); let range = fstring_expr.expression.range(); let source = self.source_file.slice(range); let text = [ @@ -11761,9 +13156,9 @@ impl Compiler { .concat(); let text: Wtf8Buf = text.into(); - pending_literal - .get_or_insert_with(Wtf8Buf::new) - .push_wtf8(text.as_ref()); + let mut debug_text = Wtf8Buf::new(); + debug_text.push_wtf8(text.as_ref()); + *pending_literal = Some(debug_text); } Self::count_pending_fstring_literal(pending_literal, element_count, false); @@ -11779,8 +13174,9 @@ impl Compiler { // strings tuple first, then evaluating interpolations left-to-right. let tstring_value = &expr_tstring.value; - let mut all_strings: Vec = Vec::new(); + let mut all_strings: Vec<(Wtf8Buf, TextRange)> = Vec::new(); let mut current_string = Wtf8Buf::new(); + let mut current_string_range = None; let mut interp_count: u32 = 0; for tstring in tstring_value { @@ -11788,19 +13184,26 @@ impl Compiler { tstring, &mut all_strings, &mut current_string, + &mut current_string_range, &mut interp_count, + expr_tstring.range, ); } - all_strings.push(core::mem::take(&mut current_string)); + all_strings.push(( + core::mem::take(&mut current_string), + current_string_range.unwrap_or(expr_tstring.range), + )); let string_count: u32 = all_strings .len() .try_into() .expect("t-string string count overflowed"); - for s in &all_strings { + for (s, range) in &all_strings { + self.set_source_range(*range); self.emit_load_const(ConstantData::Str { value: s.clone() }); } + self.set_source_range(expr_tstring.range); emit!( self, Instruction::BuildTuple { @@ -11812,74 +13215,249 @@ impl Compiler { self.compile_tstring_interpolations(tstring)?; } + self.set_source_range(expr_tstring.range); emit!( self, Instruction::BuildTuple { count: interp_count } ); + self.set_source_range(expr_tstring.range); emit!(self, Instruction::BuildTemplate); Ok(()) } - fn collect_tstring_strings( - &self, - tstring: &ast::TString, - strings: &mut Vec, - current_string: &mut Wtf8Buf, - interp_count: &mut u32, - ) { - for element in &tstring.elements { - match element { - ast::InterpolatedStringElement::Literal(lit) => { - current_string - .push_wtf8(&self.compile_tstring_literal_value(lit, tstring.flags)); - } - ast::InterpolatedStringElement::Interpolation(interp) => { - if let Some(ast::DebugText { leading, trailing }) = &interp.debug_text { - let range = interp.expression.range(); - let source = self.source_file.slice(range); - let text = [ - strip_fstring_debug_comments(leading).as_str(), - source, - strip_fstring_debug_comments(trailing).as_str(), - ] - .concat(); - current_string.push_str(&text); - } - strings.push(core::mem::take(current_string)); - *interp_count += 1; + fn compile_runtime_template_str( + &mut self, + expr_tstring: &ast::ExprTString, + values: &[ast::Expr], + ) -> CompileResult<()> { + let mut last_was_interpolation = true; + let mut strings_len = 0; + for value in values { + if self.runtime_template_value_interpolation(value).is_some() { + if last_was_interpolation { + self.set_source_range(expr_tstring.range); + self.emit_load_const(ConstantData::Str { + value: Wtf8Buf::new(), + }); + strings_len += 1; } + last_was_interpolation = true; + } else { + self.compile_expression(value)?; + strings_len += 1; + last_was_interpolation = false; } } - } - - fn compile_tstring_interpolations(&mut self, tstring: &ast::TString) -> CompileResult<()> { - for element in &tstring.elements { - let ast::InterpolatedStringElement::Interpolation(interp) = element else { - continue; - }; - - self.compile_expression(&interp.expression)?; - - let expr_range = interp.expression.range(); - let expr_source = if interp.range.start() < expr_range.start() - && interp.range.end() >= expr_range.end() - { - let after_brace = interp.range.start() + TextSize::new(1); - self.source_file - .slice(TextRange::new(after_brace, expr_range.end())) - } else { - self.source_file.slice(expr_range) - }; + if last_was_interpolation { + self.set_source_range(expr_tstring.range); self.emit_load_const(ConstantData::Str { - value: expr_source.to_string().into(), + value: Wtf8Buf::new(), }); + strings_len += 1; + } + self.set_source_range(expr_tstring.range); + emit!(self, Instruction::BuildTuple { count: strings_len }); - let mut conversion: u32 = match interp.conversion { - ast::ConversionFlag::None => 0, - ast::ConversionFlag::Str => 1, + let mut interpolations_len = 0; + for value in values { + if let Some((tstring, interpolation)) = self.runtime_template_value_interpolation(value) + { + self.compile_runtime_interpolation(tstring, interpolation)?; + interpolations_len += 1; + } + } + self.set_source_range(expr_tstring.range); + emit!( + self, + Instruction::BuildTuple { + count: interpolations_len + } + ); + self.set_source_range(expr_tstring.range); + emit!(self, Instruction::BuildTemplate); + Ok(()) + } + + fn runtime_template_value_interpolation<'a>( + &self, + value: &'a ast::Expr, + ) -> Option<( + &'a ast::ExprTString, + (&'a ast::ConstantValue, Option<&'a ast::Expr>), + )> { + let ast::Expr::TString(tstring) = value else { + return None; + }; + let interpolation = Self::single_runtime_interpolation(tstring)?; + Self::single_tstring_interpolation(tstring)?; + Some((tstring, interpolation)) + } + + fn compile_runtime_interpolation( + &mut self, + expr_tstring: &ast::ExprTString, + interpolation: (&ast::ConstantValue, Option<&ast::Expr>), + ) -> CompileResult { + let Some(interp) = Self::single_tstring_interpolation(expr_tstring) else { + return Ok(false); + }; + + self.compile_interpolation(interp, interpolation)?; + Ok(true) + } + + fn compile_interpolation( + &mut self, + interp: &ast::InterpolatedElement, + interpolation: (&ast::ConstantValue, Option<&ast::Expr>), + ) -> CompileResult<()> { + let (str, format_spec) = interpolation; + self.compile_expression(&interp.expression)?; + self.set_source_range(interp.range); + self.emit_load_const(ast_constant_value_to_constant_data(str.clone())); + + let conversion = match interp.conversion { + ast::ConversionFlag::None => 0, + ast::ConversionFlag::Str => 1, + ast::ConversionFlag::Repr => 2, + ast::ConversionFlag::Ascii => 3, + }; + + let has_format_spec = format_spec.is_some(); + if let Some(format_spec) = format_spec { + self.compile_expression(format_spec)?; + } + + let format = 2 | (conversion << 2) | u32::from(has_format_spec); + self.set_source_range(interp.range); + emit!(self, Instruction::BuildInterpolation { format }); + Ok(()) + } + + fn single_tstring_interpolation( + expr_tstring: &ast::ExprTString, + ) -> Option<&ast::InterpolatedElement> { + let [tstring] = expr_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; + } + Some(interp) + } + + fn collect_tstring_strings( + &self, + tstring: &ast::TString, + strings: &mut Vec<(Wtf8Buf, TextRange)>, + current_string: &mut Wtf8Buf, + current_string_range: &mut Option, + interp_count: &mut u32, + template_range: TextRange, + ) { + for element in &tstring.elements { + match element { + ast::InterpolatedStringElement::Literal(lit) => { + if current_string_range.is_none() { + *current_string_range = Some(lit.range); + } else { + Self::extend_pending_literal_range(current_string_range, lit.range); + } + current_string + .push_wtf8(&self.compile_tstring_literal_value(lit, tstring.flags)); + } + ast::InterpolatedStringElement::Interpolation(interp) => { + if let Some(debug_text) = &interp.debug_text { + let leading = debug_text.leading.as_str(); + let trailing = debug_text.trailing.as_str(); + let range = interp.expression.range(); + let source = self.source_file.slice(range); + let text = [ + strip_fstring_debug_comments(leading).as_str(), + source, + strip_fstring_debug_comments(trailing).as_str(), + ] + .concat(); + let debug_text_range = TextRange::new( + range.start() + - TextSize::new( + u32::try_from(leading.len()) + .expect("debug t-string leading text too long"), + ), + range.end() + + TextSize::new( + u32::try_from(trailing.len()) + .expect("debug t-string trailing text too long"), + ), + ); + if current_string_range.is_none() { + *current_string_range = Some(debug_text_range); + } else { + Self::extend_pending_literal_range( + current_string_range, + debug_text_range, + ); + } + current_string.push_str(&text); + strings.push(( + core::mem::take(current_string), + current_string_range.take().unwrap_or(template_range), + )); + } else { + strings.push(( + core::mem::take(current_string), + current_string_range.take().unwrap_or(template_range), + )); + } + *interp_count += 1; + } + } + } + } + + fn compile_tstring_interpolations(&mut self, tstring: &ast::TString) -> CompileResult<()> { + for element in &tstring.elements { + let ast::InterpolatedStringElement::Interpolation(interp) = element else { + continue; + }; + + if let Some(runtime_str) = interp.runtime_str.as_ref() { + let interpolation = ( + runtime_str, + interp.runtime_interpolation_format_spec.as_deref(), + ); + self.compile_interpolation(interp, interpolation)?; + continue; + } + + self.compile_expression(&interp.expression)?; + + let expr_range = interp.expression.range(); + let expr_source = if interp.range.start() < expr_range.start() + && interp.range.end() >= expr_range.end() + { + let after_brace = interp.range.start() + TextSize::new(1); + self.source_file + .slice(TextRange::new(after_brace, expr_range.end())) + } else { + self.source_file.slice(expr_range) + } + .to_string(); + self.set_source_range(interp.range); + self.emit_load_const(ConstantData::Str { + value: expr_source.into(), + }); + + let mut conversion: u32 = match interp.conversion { + ast::ConversionFlag::None => 0, + ast::ConversionFlag::Str => 1, ast::ConversionFlag::Repr => 2, ast::ConversionFlag::Ascii => 3, }; @@ -11890,16 +13468,18 @@ impl Compiler { let has_format_spec = interp.format_spec.is_some(); if let Some(format_spec) = &interp.format_spec { + let format_spec_range = self.cpython_format_spec_range(format_spec.range); self.compile_fstring_elements( ast::FStringFlags::empty(), &format_spec.elements, - Some(format_spec.range), + Some(format_spec_range), )?; } // CPython keeps bit 1 set in BUILD_INTERPOLATION's oparg and uses // bit 0 for the optional format spec. let format = 2 | (conversion << 2) | u32::from(has_format_spec); + self.set_source_range(interp.range); emit!(self, Instruction::BuildInterpolation { format }); } @@ -12005,15 +13585,19 @@ fn split_doc_with_range<'a>( opts: &CompileOpts, ) -> (Option<(String, TextRange)>, &'a [ast::Stmt]) { if let Some((ast::Stmt::Expr(expr), body_rest)) = body.split_first() { - let doc_comment = match &*expr.value { - ast::Expr::StringLiteral(value) => Some((&value.value, expr.value.range())), + let doc_comment: Option<(&str, TextRange)> = match &*expr.value { + ast::Expr::StringLiteral(value) => Some((value.value.to_str(), expr.value.range())), + ast::Expr::Constant(ast::ExprConstant { + value: ast::ConstantValue::Str(value), + .. + }) => Some((value.as_ref(), expr.value.range())), // f-strings are not allowed in Python doc comments. ast::Expr::FString(_) => None, _ => None, }; if let Some((doc, range)) = doc_comment { return if opts.optimize < 2 { - (Some((clean_doc(doc.to_str()), range)), body_rest) + (Some((clean_doc(doc), range)), body_rest) } else { (None, body_rest) }; @@ -12028,6 +13612,17 @@ fn split_doc<'a>(body: &'a [ast::Stmt], opts: &CompileOpts) -> (Option, (doc.map(|(doc, _)| doc), body) } +fn is_docstring_expr(expr: &ast::Expr) -> bool { + matches!( + expr, + ast::Expr::StringLiteral(_) + | ast::Expr::Constant(ast::ExprConstant { + value: ast::ConstantValue::Str(_), + .. + }) + ) +} + pub fn ruff_int_to_bigint(int: &ast::Int) -> Result { if let Some(small) = int.as_u64() { Ok(BigInt::from(small)) @@ -12131,11 +13726,16 @@ mod ruff_tests { debug_text: None, conversion: ast::ConversionFlag::None, format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, }, )] .into(), flags, }), + runtime_joined_str: None, + runtime_values: None, }); assert!(!Compiler::contains_await(not_present)); @@ -12164,11 +13764,16 @@ mod ruff_tests { debug_text: None, conversion: ast::ConversionFlag::None, format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, }, )] .into(), flags, }), + runtime_joined_str: None, + runtime_values: None, }); assert!(Compiler::contains_await(present)); @@ -12213,15 +13818,23 @@ mod ruff_tests { debug_text: None, conversion: ast::ConversionFlag::None, format_spec: None, + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, }, )] .into(), })), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec: None, }, )] .into(), flags, }), + runtime_joined_str: None, + runtime_values: None, }); assert!(Compiler::contains_await(present)); } @@ -12232,7 +13845,7 @@ mod tests { use super::*; use rustpython_compiler_core::{ SourceFileBuilder, - bytecode::{CodeUnit, OpArg}, + bytecode::{CO_FAST_ARG_KW, CO_FAST_ARG_POS, CodeUnit, OpArg}, }; fn assert_scope_exit_locations(code: &CodeObject) { @@ -12281,7 +13894,7 @@ mod tests { compile_exec_with_options(source, opts) } - fn compile_exec_with_options(source: &str, opts: CompileOpts) -> CodeObject { + fn compile_exec_with_options(source: &str, mut opts: CompileOpts) -> CodeObject { let source_file = SourceFileBuilder::new("source_path", source).finish(); let parsed = ruff_python_parser::parse( source_file.source_text(), @@ -12289,122 +13902,953 @@ mod tests { ) .unwrap(); let mut ast = parsed.into_syntax(); - preprocess::preprocess_mod(&mut ast); + opts.future_features |= checked_future_features(&ast, &source_file).unwrap(); + let future_annotations = opts + .future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false); let ast = match ast { ruff_python_ast::Mod::Module(stmts) => stmts, _ => unreachable!(), }; - let symbol_table = SymbolTable::scan_program(&ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) - .unwrap(); - let mut compiler = Compiler::new(opts, source_file, ""); + let symbol_table = SymbolTable::scan_program_with_options( + &ast, + source_file.clone(), + opts.allow_top_level_await, + opts.future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS), + opts.recursion_limit, + ) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) + .unwrap(); + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); compiler.compile_program(&ast, symbol_table).unwrap(); compiler.exit_scope() } - #[test] - fn empty_module_implicit_return_inherits_resume_location_like_cpython() { - let code = compile_exec(""); - // CPython 3.14 codegen emits the implicit LOAD_CONST/RETURN_VALUE with - // NO_LOCATION, then flowgraph.c::propagate_line_numbers() propagates - // the module RESUME location, whose line is 0. - assert_eq!(code.linetable.as_ref(), &[0xf2, 0x03, 0x01, 0x01, 0x01]); - } - - #[test] - fn redundant_nop_location_copies_full_location_like_cpython() { - let code = compile_exec( - "\ -def f(x, y, z): - while x: - if y: - pass - elif z: - if y < 0: - return y - if z: - y = y + 1 - elif y: - return 1 - return -1 -", - ); - let f = find_code(&code, "f").expect("missing function code"); - assert_eq!( - f.linetable.as_ref(), - &[ - 0x80, 0x00, 0xdf, 0x0a, 0x0b, 0xdf, 0x0b, 0x0c, 0xd9, 0x0c, 0x10, 0xdf, 0x0d, 0x0e, - 0xd8, 0x0f, 0x10, 0x90, 0x31, 0x8c, 0x75, 0xd8, 0x17, 0x18, 0x90, 0x08, 0xdf, 0x0f, - 0x10, 0xd8, 0x14, 0x15, 0x98, 0x01, 0x95, 0x45, 0x92, 0x01, 0xf1, 0x03, 0x00, 0x10, - 0x11, 0xe7, 0x0d, 0x0e, 0x89, 0x51, 0xd9, 0x13, 0x14, 0xd8, 0x0b, 0x0d, 0x80, 0x49, - ], - "CPython basicblock_remove_redundant_nops() copies the full NOP location into a following no-location jump" - ); - } - - fn scan_program_symbol_table(source: &str) -> SymbolTable { + fn compile_module_instruction_infos(source: &str, mode: Mode) -> Vec { + let mut opts = CompileOpts::default(); let source_file = SourceFileBuilder::new("source_path", source).finish(); let parsed = ruff_python_parser::parse( source_file.source_text(), ruff_python_parser::Mode::Module.into(), ) .unwrap(); - let ast = parsed.into_syntax(); + let mut ast = parsed.into_syntax(); + opts.future_features |= checked_future_features(&ast, &source_file).unwrap(); + let future_annotations = opts + .future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + if matches!(mode, Mode::Single) + && let ruff_python_ast::Mod::Module(module) = &mut ast + { + preprocess::preprocess_statements( + &mut module.body, + opts.optimize, + future_annotations, + false, + ); + } else { + preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false); + } let ast = match ast { ruff_python_ast::Mod::Module(stmts) => stmts, _ => unreachable!(), }; - SymbolTable::scan_program(&ast, source_file) - .map_err(|e| e.into_codegen_error("source_path".to_owned())) - .unwrap() - } - - fn find_symbol_table<'a>(table: &'a SymbolTable, name: &str) -> Option<&'a SymbolTable> { - if table.name == name { - return Some(table); + let symbol_table = SymbolTable::scan_program_with_options( + &ast, + source_file.clone(), + opts.allow_top_level_await, + opts.future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS), + opts.recursion_limit, + ) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) + .unwrap(); + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); + match mode { + Mode::Single => compiler.compile_program_single(&ast.body, symbol_table), + _ => compiler.compile_program(&ast, symbol_table), } - table - .sub_tables + .unwrap(); + + compiler + .current_code_info() + .blocks .iter() - .find_map(|sub_table| find_symbol_table(sub_table, name)) + .flat_map(|block| block.used_instructions().iter().copied()) + .collect() } - fn compile_exec_late_cfg_trace(source: &str) -> Vec<(String, String)> { + fn compile_eval_ast_with_options(expr: ast::Expr, opts: CompileOpts) -> CodeObject { + let source_file = SourceFileBuilder::new("source_path", "").finish(); + let parsed = ruff_python_ast::Mod::Expression(ast::ModExpression { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + body: Box::new(expr), + }); + compile_top(parsed, source_file, Mode::Eval, opts).unwrap() + } + + fn set_ast_constant(expr: &mut ast::Expr, constant: ConstantData) { + let constant = crate::constant_data_to_ast_constant_value(constant); + let range = expr.range(); + *expr = ast::Expr::Constant(ast::ExprConstant { + node_index: Default::default(), + range, + value: constant, + kind: None, + invalid_type: None, + }); + } + + fn compile_ast_constant_expr(mut expr: ast::Expr, constant: ConstantData) -> CodeObject { + set_ast_constant(&mut expr, constant); + compile_eval_ast_with_options(expr, CompileOpts::default()) + } + + fn first_ast_constant_warning(expr: ast::Expr) -> String { + let opts = CompileOpts::default(); + let source_file = SourceFileBuilder::new("source_path", "").finish(); + let parsed = ruff_python_ast::Mod::Expression(ast::ModExpression { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + body: Box::new(expr), + }); + let mut warning = None; + let mut handler = |location, message: String| { + warning = Some(message.clone()); + Err(CodegenError { + location: Some(location), + error: CodegenErrorType::SyntaxError(message), + source_path: "source_path".to_owned(), + }) + }; + compile_top_with_syntax_warning_handler( + parsed, + source_file, + Mode::Eval, + opts, + Some(&mut handler), + ) + .expect_err("expected SyntaxWarning handler to stop compilation"); + warning.expect("expected warning message") + } + + fn first_exec_warning(source: &str) -> String { let opts = CompileOpts::default(); let source_file = SourceFileBuilder::new("source_path", source).finish(); let parsed = ruff_python_parser::parse( source_file.source_text(), ruff_python_parser::Mode::Module.into(), ) - .unwrap(); - let ast = parsed.into_syntax(); + .unwrap() + .into_syntax(); + let mut warning = None; + let mut handler = |location, message: String| { + warning = Some(message.clone()); + Err(CodegenError { + location: Some(location), + error: CodegenErrorType::SyntaxError(message), + source_path: "source_path".to_owned(), + }) + }; + compile_top_with_syntax_warning_handler( + parsed, + source_file, + Mode::Exec, + opts, + Some(&mut handler), + ) + .expect_err("expected SyntaxWarning handler to stop compilation"); + warning.expect("expected warning message") + } + + fn frozenset_call_expr() -> ast::Expr { + ast::Expr::Call(ast::ExprCall { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + func: Box::new(ast::Expr::Name(ast::ExprName { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + id: ast::name::Name::new_static("frozenset"), + ctx: ast::ExprContext::Load, + })), + arguments: ast::Arguments { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + args: Box::default(), + keywords: Default::default(), + runtime_args: None, + runtime_bases: None, + }, + }) + } + + fn compile_exec_parsed_error( + source: &str, + parsed: ruff_python_parser::Parsed, + ) -> CodegenError { + let mut opts = CompileOpts::default(); + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let mut ast = parsed.into_syntax(); + opts.future_features |= match checked_future_features(&ast, &source_file) { + Ok(features) => features, + Err(err) => return err, + }; + let future_annotations = opts + .future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false); let ast = match ast { ruff_python_ast::Mod::Module(stmts) => stmts, _ => unreachable!(), }; - let symbol_table = SymbolTable::scan_program(&ast, source_file.clone()) - .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) - .unwrap(); - let mut compiler = Compiler::new(opts, source_file, ""); - compiler.compile_program(&ast, symbol_table).unwrap(); - let _table = compiler.pop_symbol_table(); - let stack_top = compiler.code_stack.pop().unwrap(); - stack_top.debug_late_cfg_trace().unwrap() + let symbol_table = match SymbolTable::scan_program(&ast, source_file.clone()) { + Ok(symbol_table) => symbol_table, + Err(err) => return err.into_codegen_error(source_file.name().to_owned()), + }; + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); + compiler.compile_program(&ast, symbol_table).unwrap_err() } - fn compile_single_function_late_cfg_trace( - source: &str, - function_name: &str, - ) -> Vec<(String, String)> { - let opts = CompileOpts::default(); + fn compile_exec_error(source: &str) -> CodegenError { let source_file = SourceFileBuilder::new("source_path", source).finish(); let parsed = ruff_python_parser::parse( source_file.source_text(), ruff_python_parser::Mode::Module.into(), ) .unwrap(); - let ast = parsed.into_syntax(); - let ast = match ast { + compile_exec_parsed_error(source, parsed) + } + + fn compile_exec_error_message(source: &str) -> String { + compile_exec_error(source).error.to_string() + } + + fn compile_exec_unchecked_error_message(source: &str) -> String { + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse_unchecked( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ); + compile_exec_parsed_error(source, parsed).error.to_string() + } + + #[test] + fn ast_constant_frozenset_compiles_as_load_const() { + let code = compile_ast_constant_expr( + frozenset_call_expr(), + ConstantData::Frozenset { + elements: vec![ConstantData::Integer { + value: BigInt::from(1u8), + }], + }, + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::LoadConst { .. })), + "public ast.Constant(frozenset(...)) must use CPython Constant_kind LOAD_CONST path, got {ops:?}" + ); + assert!( + !ops.iter().any(|op| matches!( + op, + Instruction::LoadName { .. } + | Instruction::Call { .. } + | Instruction::CallKw { .. } + )), + "public ast.Constant(frozenset(...)) must not compile as a frozenset() call, got {ops:?}" + ); + assert!( + code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Frozenset { elements } + if matches!( + elements.as_slice(), + [ConstantData::Integer { value }] if *value == BigInt::from(1u8) + ) + )), + "missing frozenset constant in code constants" + ); + } + + #[test] + fn ast_constant_is_not_scanned_as_lowered_expression() { + let mut expr = frozenset_call_expr(); + set_ast_constant( + &mut expr, + ConstantData::Frozenset { + elements: Vec::new(), + }, + ); + let module = ast::ModExpression { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + body: Box::new(expr), + }; + let table = SymbolTable::scan_expr_with_options( + &module, + SourceFileBuilder::new("source_path", "").finish(), + false, + false, + CompileOpts::default().recursion_limit, + ) + .unwrap(); + + assert!( + table.lookup("frozenset").is_none(), + "CPython symtable Constant_kind does not visit the lowered frozenset() expression" + ); + } + + #[test] + fn ast_constant_frozenset_call_warns_like_cpython_constant() { + let mut func = frozenset_call_expr(); + set_ast_constant( + &mut func, + ConstantData::Frozenset { + elements: Vec::new(), + }, + ); + let message = first_ast_constant_warning(ast::Expr::Call(ast::ExprCall { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + func: Box::new(func), + arguments: ast::Arguments { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + args: Box::default(), + keywords: Default::default(), + runtime_args: None, + runtime_bases: None, + }, + })); + assert!( + message.contains("'frozenset' object is not callable"), + "expected public ast.Constant(frozenset()) callable warning, got {message:?}" + ); + } + + #[test] + fn ast_constant_frozenset_subscript_warns_like_cpython_constant() { + let mut value = frozenset_call_expr(); + set_ast_constant( + &mut value, + ConstantData::Frozenset { + elements: Vec::new(), + }, + ); + let message = first_ast_constant_warning(ast::Expr::Subscript(ast::ExprSubscript { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: Box::new(value), + slice: Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: ast::Number::Int(ast::Int::ZERO), + })), + ctx: ast::ExprContext::Load, + })); + assert!( + message.contains("'frozenset' object is not subscriptable"), + "expected public ast.Constant(frozenset()) subscript warning, got {message:?}" + ); + } + + #[test] + fn ast_constant_str_bad_index_warns_like_cpython_constant() { + let mut value = frozenset_call_expr(); + set_ast_constant( + &mut value, + ConstantData::Str { + value: "abc".into(), + }, + ); + let message = first_ast_constant_warning(ast::Expr::Subscript(ast::ExprSubscript { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: Box::new(value), + slice: Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: ast::Number::Float(1.0), + })), + ctx: ast::ExprContext::Load, + })); + assert!( + message.contains("str indices must be integers or slices, not float"), + "expected public ast.Constant(str) bad-index warning, got {message:?}" + ); + } + + #[test] + fn ast_constant_frozenset_is_warns_like_cpython_constant() { + let mut left = frozenset_call_expr(); + set_ast_constant( + &mut left, + ConstantData::Frozenset { + elements: Vec::new(), + }, + ); + let message = first_ast_constant_warning(ast::Expr::Compare(ast::ExprCompare { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + left: Box::new(left), + ops: Box::new([ast::CmpOp::Is]), + comparators: Box::new([ast::Expr::NoneLiteral(ast::ExprNoneLiteral { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + })]), + runtime_comparators: None, + })); + assert!( + message.contains("\"is\" with 'frozenset' literal"), + "expected public ast.Constant(frozenset()) identity warning, got {message:?}" + ); + } + + #[test] + fn ast_constant_tuple_compiles_as_load_const() { + let expr = ast::Expr::Tuple(ast::ExprTuple { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + elts: Vec::new(), + ctx: ast::ExprContext::Load, + parenthesized: true, + runtime_elts: None, + }); + let code = compile_ast_constant_expr( + expr, + ConstantData::Tuple { + elements: vec![ConstantData::Integer { + value: BigInt::from(1u8), + }], + }, + ); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + assert!( + ops.iter() + .any(|op| matches!(op, Instruction::LoadConst { .. })), + "public ast.Constant(tuple(...)) must use CPython Constant_kind LOAD_CONST path, got {ops:?}" + ); + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::BuildTuple { .. })), + "public ast.Constant(tuple(...)) must not compile as a tuple display, got {ops:?}" + ); + assert!( + code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Tuple { elements } + if matches!( + elements.as_slice(), + [ConstantData::Integer { value }] if *value == BigInt::from(1u8) + ) + )), + "missing tuple constant in code constants" + ); + } + + #[test] + fn ast_constant_slice_bound_uses_cpython_constant_slice_path() { + let mut lower = frozenset_call_expr(); + set_ast_constant( + &mut lower, + ConstantData::Integer { + value: BigInt::from(1u8), + }, + ); + let expr = ast::Expr::Subscript(ast::ExprSubscript { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: Box::new(ast::Expr::Name(ast::ExprName { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + id: ast::name::Name::new_static("obj"), + ctx: ast::ExprContext::Load, + })), + slice: Box::new(ast::Expr::Slice(ast::ExprSlice { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + lower: Some(Box::new(lower)), + upper: Some(Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral { + node_index: ast::AtomicNodeIndex::NONE, + range: TextRange::default(), + value: ast::Number::Int(ast::Int::ZERO), + }))), + step: None, + })), + ctx: ast::ExprContext::Load, + }); + let code = compile_eval_ast_with_options(expr, CompileOpts::default()); + let ops: Vec<_> = code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + assert!( + !ops.iter().any(|op| matches!( + op, + Instruction::BinarySlice | Instruction::BuildSlice { .. } + )), + "public ast.Constant slice bound must follow CPython Constant_kind folded slice path, got {ops:?}" + ); + assert!( + code.constants.iter().any(|constant| matches!( + constant, + ConstantData::Slice { elements } + if matches!( + elements.as_ref(), + [ + ConstantData::Integer { value }, + ConstantData::Integer { .. }, + ConstantData::None, + ] if *value == BigInt::from(1u8) + ) + )), + "missing folded slice constant for public ast.Constant bound" + ); + } + + #[test] + fn match_pattern_errors_use_cpython_sequence_messages() { + let many_names = (0..256) + .map(|i| format!("a{i}")) + .collect::>() + .join(", "); + let too_many = format!( + "\ +match x: + case [{many_names}, *rest]: + pass +" + ); + assert_eq!( + compile_exec_error_message(&too_many), + "too many expressions in star-unpacking sequence pattern" + ); + + assert_eq!( + compile_exec_error_message( + "\ +match x: + case [*a, *b]: + pass +" + ), + "multiple starred names in sequence pattern" + ); + + assert_eq!( + compile_exec_unchecked_error_message( + "\ +match x: + case {**_}: + pass +" + ), + "invalid syntax" + ); + } + + #[test] + fn match_mapping_duplicate_literal_keys_use_cpython_equality() { + for (source, expected) in [ + ( + "\ +match x: + case {1: a, True: b}: + pass +", + "mapping pattern checks duplicate key (True)", + ), + ( + "\ +match x: + case {1: a, 1.0: b}: + pass +", + "mapping pattern checks duplicate key (1.0)", + ), + ( + "\ +match x: + case {0.0: a, -0.0: b}: + pass +", + "mapping pattern checks duplicate key (-0.0)", + ), + ( + "\ +match x: + case {9007199254740992: a, 9007199254740992.0: b}: + pass +", + "mapping pattern checks duplicate key (9007199254740992.0)", + ), + ( + "\ +match x: + case {-9007199254740992: a, -9007199254740992.0: b}: + pass +", + "mapping pattern checks duplicate key (-9007199254740992.0)", + ), + ( + "\ +match x: + case {1 + 0j: a, 1: b}: + pass +", + "mapping pattern checks duplicate key (1)", + ), + ( + "\ +match x: + case {1: a, 1 + 0j: b}: + pass +", + "mapping pattern checks duplicate key ((1+0j))", + ), + ( + "\ +match x: + case {0j: a, -0.0: b}: + pass +", + "mapping pattern checks duplicate key (-0.0)", + ), + ] { + assert_eq!(compile_exec_error_message(source), expected); + } + } + + #[test] + fn match_mapping_accepts_folded_literal_keys_like_cpython() { + compile_exec( + "\ +def f(x): + match x: + case {-1: a, 1 + 0j: b}: + return a, b + case {9007199254740993: a, 9007199254740992.0: b}: + return a, b + case {-9007199254740993: a, -9007199254740992.0: b}: + return a, b + case {1 + 1j: a, 1: b}: + return a, b + case _: + return None +", + ); + } + + #[test] + fn match_mapping_accepts_public_ast_constant_keys_like_cpython() { + let source = "\ +match x: + case {'a': _, b'b': _, 2: _, 1.5: _, 1j: _, True: _, None: _}: + pass +"; + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap(); + let mut ast = parsed.into_syntax(); + let ast::Mod::Module(module) = &mut ast else { + unreachable!(); + }; + let ast::Stmt::Match(match_stmt) = &mut module.body[0] else { + unreachable!(); + }; + let ast::Pattern::MatchMapping(mapping) = &mut match_stmt.cases[0].pattern else { + unreachable!(); + }; + + for (key, value) in mapping.keys.iter_mut().zip([ + ast::ConstantValue::Str("a".into()), + ast::ConstantValue::Bytes(vec![b'b'].into_boxed_slice()), + ast::ConstantValue::Integer("2".into()), + ast::ConstantValue::Float(1.5), + ast::ConstantValue::Complex { + real: 0.0, + imag: 1.0, + }, + ast::ConstantValue::Boolean(true), + ast::ConstantValue::None, + ]) { + let range = key.range(); + *key = ast::Expr::Constant(ast::ExprConstant { + node_index: ast::AtomicNodeIndex::NONE, + range, + value, + kind: None, + invalid_type: None, + }); + } + + compile_top(ast, source_file, Mode::Exec, CompileOpts::default()).unwrap(); + } + + #[test] + fn match_literal_binop_folding_uses_cpython_complex_shape() { + assert!( + Compiler::try_fold_match_pattern_binop( + ast::Operator::Add, + &ConstantData::Integer { + value: BigInt::from(1) + }, + &ConstantData::Integer { + value: BigInt::from(2) + }, + ) + .is_none() + ); + assert!( + Compiler::try_fold_match_pattern_binop( + ast::Operator::Add, + &ConstantData::Float { value: 1.0 }, + &ConstantData::Float { value: 2.0 }, + ) + .is_none() + ); + assert!(matches!( + Compiler::try_fold_match_pattern_binop( + ast::Operator::Add, + &ConstantData::Integer { + value: BigInt::from(1) + }, + &ConstantData::Complex { + value: Complex::new(0.0, 2.0) + }, + ), + Some(ConstantData::Complex { value }) if value == Complex::new(1.0, 2.0) + )); + assert!(matches!( + Compiler::try_fold_match_pattern_binop( + ast::Operator::Sub, + &ConstantData::Float { value: 1.5 }, + &ConstantData::Complex { + value: Complex::new(0.0, 2.0) + }, + ), + Some(ConstantData::Complex { value }) if value == Complex::new(1.5, -2.0) + )); + } + + #[test] + fn match_literal_patterns_reject_unexpected_constants_like_cpython() { + assert!(Compiler::is_unexpected_match_literal_constant( + &ast::ExprEllipsisLiteral { + range: TextRange::default(), + node_index: ast::AtomicNodeIndex::NONE, + } + .into() + )); + } + + #[test] + fn unpack_ex_allows_large_after_count_like_cpython() { + let suffix = (0..256) + .map(|i| format!("a{i}")) + .collect::>() + .join(", "); + let code = compile_exec(&format!( + "\ +def assignment(values): + *rest, {suffix} = values + return a255 + +def pattern(values): + match values: + case [*rest, {suffix}]: + return a255 + case _: + return None +" + )); + + let assignment = find_code(&code, "assignment").expect("missing assignment code"); + assert_eq!( + full_opargs_for(assignment, |op| matches!(op, Instruction::UnpackEx { .. })), + vec![256 << 8] + ); + + let pattern = find_code(&code, "pattern").expect("missing pattern code"); + assert_eq!( + full_opargs_for(pattern, |op| matches!(op, Instruction::UnpackEx { .. })), + vec![256 << 8] + ); + } + + #[test] + fn match_irrefutable_pattern_errors_use_cpython_messages() { + assert_eq!( + compile_exec_error_message( + "\ +match x: + case y | 1: + pass +" + ), + "name capture 'y' makes remaining patterns unreachable" + ); + + assert_eq!( + compile_exec_error_message( + "\ +match x: + case _ | 1: + pass +" + ), + "wildcard makes remaining patterns unreachable" + ); + } + + #[test] + fn empty_module_implicit_return_inherits_resume_location_like_cpython() { + let code = compile_exec(""); + // codegen emits the implicit LOAD_CONST/RETURN_VALUE with + // NO_LOCATION, then flowgraph.c::propagate_line_numbers() propagates + // the module RESUME location, whose line is 0. + assert_eq!(code.linetable.as_ref(), &[0xf2, 0x03, 0x01, 0x01, 0x01]); + } + + #[test] + fn module_docstring_load_uses_doc_location_like_cpython() { + let code = compile_exec( + "\ +\"doc\" +x = 1 +", + ); + + // codegen_body() emits the docstring LOAD_CONST at the + // string expression location, then emits STORE_NAME __doc__ with + // NO_LOCATION. + assert_eq!( + code.linetable.as_ref(), + &[ + 0xf0, 0x03, 0x01, 0x01, 0x01, 0xd9, 0x00, 0x05, 0xd8, 0x04, 0x05, 0x82, 0x01, + ], + ); + } + + #[test] + fn redundant_nop_location_copies_full_location_like_cpython() { + let code = compile_exec( + "\ +def f(x, y, z): + while x: + if y: + pass + elif z: + if y < 0: + return y + if z: + y = y + 1 + elif y: + return 1 + return -1 +", + ); + let f = find_code(&code, "f").expect("missing function code"); + assert_eq!( + f.linetable.as_ref(), + &[ + 0x80, 0x00, 0xdf, 0x0a, 0x0b, 0xdf, 0x0b, 0x0c, 0xd9, 0x0c, 0x10, 0xdf, 0x0d, 0x0e, + 0xd8, 0x0f, 0x10, 0x90, 0x31, 0x8c, 0x75, 0xd8, 0x17, 0x18, 0x90, 0x08, 0xdf, 0x0f, + 0x10, 0xd8, 0x14, 0x15, 0x98, 0x01, 0x95, 0x45, 0x92, 0x01, 0xf1, 0x03, 0x00, 0x10, + 0x11, 0xe7, 0x0d, 0x0e, 0x89, 0x51, 0xd9, 0x13, 0x14, 0xd8, 0x0b, 0x0d, 0x80, 0x49, + ], + "CPython basicblock_remove_redundant_nops() copies the full NOP location into a following no-location jump" + ); + } + + fn scan_program_symbol_table(source: &str) -> SymbolTable { + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap(); + let ast = parsed.into_syntax(); + let ast = match ast { + ruff_python_ast::Mod::Module(stmts) => stmts, + _ => unreachable!(), + }; + SymbolTable::scan_program(&ast, source_file) + .map_err(|e| e.into_codegen_error("source_path".to_owned())) + .unwrap() + } + + fn find_symbol_table<'a>(table: &'a SymbolTable, name: &str) -> Option<&'a SymbolTable> { + if table.name == name { + return Some(table); + } + table + .sub_tables + .iter() + .find_map(|sub_table| find_symbol_table(sub_table, name)) + } + + fn compile_exec_late_cfg_trace(source: &str) -> Vec<(String, String)> { + let opts = CompileOpts::default(); + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap(); + let ast = parsed.into_syntax(); + let ast = match ast { + ruff_python_ast::Mod::Module(stmts) => stmts, + _ => unreachable!(), + }; + let symbol_table = SymbolTable::scan_program(&ast, source_file.clone()) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) + .unwrap(); + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); + compiler.compile_program(&ast, symbol_table).unwrap(); + compiler.pop_symbol_table(); + let stack_top = compiler.code_stack.pop().unwrap(); + stack_top.debug_late_cfg_trace().unwrap() + } + + fn compile_single_function_late_cfg_trace( + source: &str, + function_name: &str, + ) -> Vec<(String, String)> { + let opts = CompileOpts::default(); + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap(); + let ast = parsed.into_syntax(); + let ast = match ast { ruff_python_ast::Mod::Module(stmts) => stmts, _ => unreachable!(), }; @@ -12431,7 +14875,8 @@ def f(x, y, z): let is_async = function.is_async; let range = function.range(); - let mut compiler = Compiler::new(opts, source_file, ""); + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); compiler.future_annotations = symbol_table.future_annotations; compiler.symbol_table_stack.push(symbol_table); compiler.set_source_range(range); @@ -12452,7 +14897,7 @@ def f(x, y, z): in_async_scope: is_async, }; compiler.set_qualname(); - let (_doc_str, body) = split_doc(body, &compiler.opts); + let (_, body) = split_doc(body, &compiler.opts); let start_label = compiler.use_cpython_function_start_label(); let is_gen = is_async || compiler.current_symbol_table().is_generator; let stop_iteration_block = if is_gen { @@ -12492,7 +14937,7 @@ def f(x, y, z): compiler.set_no_location(); } - let _table = compiler.pop_symbol_table(); + compiler.pop_symbol_table(); let stack_top = compiler.code_stack.pop().unwrap(); stack_top.debug_late_cfg_trace().unwrap() } @@ -12534,118 +14979,6 @@ def f(arch): ); } - #[test] - fn debug_trace_make_dataclass_borrow_tail() { - let trace = compile_single_function_late_cfg_trace( - r#" -def f(module, cls, decorator, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot): - if module is None: - try: - module = sys._getframemodulename(1) or '__main__' - except AttributeError: - try: - module = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass - if module is not None: - cls.__module__ = module - cls = decorator(cls, init=init, repr=repr, eq=eq, order=order, - unsafe_hash=unsafe_hash, frozen=frozen, - match_args=match_args, kw_only=kw_only, slots=slots, - weakref_slot=weakref_slot) - return cls -"#, - "f", - ); - for (label, dump) in trace { - if label.starts_with("after_") { - eprintln!("=== {label} ===\n{dump}"); - } - } - } - - #[test] - fn debug_trace_protected_attr_subscript_tail() { - let trace = compile_single_function_late_cfg_trace( - r#" -def f(f, oldcls, newcls): - try: - idx = f.__code__.co_freevars.index("__class__") - except ValueError: - return False - closure = f.__closure__[idx] - if closure.cell_contents is oldcls: - closure.cell_contents = newcls - return True - return False -"#, - "f", - ); - for (label, dump) in trace { - if label.starts_with("after_") { - eprintln!("=== {label} ===\n{dump}"); - } - } - } - - #[test] - fn debug_trace_dtrace_tail() { - let trace = compile_single_function_late_cfg_trace( - r#" -def f(proc, unittest): - try: - with proc: - version, stderr = proc.communicate() - if proc.returncode: - raise Exception(version, stderr) - except OSError: - raise unittest.SkipTest("x") - match = re.search("pat", version) - if match is None: - raise unittest.SkipTest(f"Unable to parse readelf version: {version}") - return int(match.group(1)), int(match.group(2)) -"#, - "f", - ); - for (label, dump) in trace { - if label == "after_optimize_load_fast" - || label.contains("deoptimize_borrow_in_protected_conditional_tail") - { - eprintln!("=== {label} ===\n{dump}"); - } - } - } - - #[test] - fn debug_trace_colorize_tail() { - let trace = compile_single_function_late_cfg_trace( - r#" -def f(sys, os, file): - if sys.platform == "win32": - try: - import nt - if not nt._supports_virtual_terminal(): - return False - except (ImportError, AttributeError): - return False - - try: - return os.isatty(file.fileno()) - except OSError: - return hasattr(file, "isatty") and file.isatty() -"#, - "f", - ); - for (label, dump) in trace { - if label == "after_optimize_load_fast" - || label == "after_deoptimize_borrow_after_protected_import" - || label == "after_borrow_deopts" - { - eprintln!("=== {label} ===\n{dump}"); - } - } - } - #[test] fn for_try_except_break_keeps_cpython_if_layout() { let code = compile_exec( @@ -14378,64 +16711,179 @@ def g(): } #[test] - fn module_deferred_annotations_use_start_location_like_cpython() { + fn starred_arg_annotation_unpack_uses_function_location_like_cpython() { + let code = compile_exec("def f(*args: *Ts): pass\n"); + let annotate = find_code(&code, "__annotate__").expect("missing annotation code"); + + // codegen_argannotation() visits `Ts` at the annotation + // expression location, then emits UNPACK_SEQUENCE at LOC(function). + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x80, 0x00, 0xd7, 0x00, 0x17, 0xd1, 0x00, 0x17, 0x8c, 0x62, 0xd3, 0x00, 0x17 + ], + ); + } + + #[test] + fn module_deferred_annotations_use_start_location_like_cpython() { + let code = compile_exec( + "\ +import os +X: int +Y: str +", + ); + let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + + // compile.c::start_location() passes the first module + // statement location into _PyCodegen_Module(), and + // codegen_process_deferred_annotations() uses that loc for annotation + // scope setup, BUILD_MAP, STORE_SUBSCR, and RETURN_VALUE. + assert_eq!( + annotate.linetable.as_ref(), + &[ + 0x80, 0x00, 0x87, 0x09, 0x81, 0x09, 0xdf, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, + 0x06, 0xf1, 0x03, 0x00, 0x01, 0x0a, 0xe7, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, + 0x06, 0xf2, 0x05, 0x00, 0x01, 0x0a, + ] + ); + } + + #[test] + fn super_method_call_kw_names_use_attribute_location_like_cpython() { + let code = compile_exec( + "\ +class C: + def f(self, x, y): + super().__init__( + x=x, + y=y) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let call_kw_index = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::CallKw { .. })) + .expect("missing CALL_KW"); + let (kw_names, (location, end_location)) = f + .instructions + .iter() + .zip(&f.locations) + .take(call_kw_index) + .rev() + .find(|(unit, _)| matches!(unit.op, Instruction::LoadConst { .. })) + .expect("missing CALL_KW names tuple"); + + assert!( + matches!(kw_names.op, Instruction::LoadConst { .. }), + "expected keyword names tuple before CALL_KW" + ); + assert_eq!( + (location.line.get(), end_location.line.get()), + (3, 3), + "CPython maybe_optimize_method_call() passes the updated method-attribute loc into codegen_call_simple_kw_helper()" + ); + } + + #[test] + fn multiline_super_method_load_uses_expression_start_location_like_cpython() { + let code = compile_exec( + "\ +class C: + def f(self): + return super( + ).m() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let load_super_index = f + .instructions + .iter() + .position(|unit| match unit.op { + Instruction::LoadSuperAttr { namei } => namei + .get(OpArg::new(u32::from(u8::from(unit.arg)))) + .is_load_method(), + _ => false, + }) + .expect("missing LOAD_SUPER_METHOD"); + let (load_location, _) = f.locations[load_super_index]; + + assert_eq!( + load_location.line.get(), + 3, + "CPython maybe_optimize_method_call() emits LOAD_SUPER_METHOD at LOC(meth), before updating to the attribute start" + ); + } + + #[test] + fn multiline_non_ascii_attribute_uses_cpython_unicode_length() { + let code = compile_exec( + "\ +def f(obj): + return (obj + .é) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let load_attr_position = f + .instructions + .iter() + .zip(&f.locations) + .find_map(|(unit, (location, end_location))| { + matches!(unit.op, Instruction::LoadAttr { .. }).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .expect("missing LOAD_ATTR"); + + assert_eq!( + load_attr_position, + (3, 11, 3, 12), + "CPython update_start_location_to_match_attr() subtracts PyUnicode_GET_LENGTH(attr), not the UTF-8 byte length; Rust SourceLocation exposes the resulting columns as one-based" + ); + } + + #[test] + fn two_arg_super_attr_in_class_body_is_optimized_like_cpython() { let code = compile_exec( "\ -import os -X: int -Y: str +class C: + x = super(C, self).attr ", ); - let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + let class_code = find_code(&code, "C").expect("missing class code"); - // CPython 3.14 compile.c::start_location() passes the first module - // statement location into _PyCodegen_Module(), and - // codegen_process_deferred_annotations() uses that loc for annotation - // scope setup, BUILD_MAP, STORE_SUBSCR, and RETURN_VALUE. - assert_eq!( - annotate.linetable.as_ref(), - &[ - 0x80, 0x00, 0x87, 0x09, 0x81, 0x09, 0xdf, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, - 0x06, 0xf1, 0x03, 0x00, 0x01, 0x0a, 0xe7, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81, - 0x06, 0xf2, 0x05, 0x00, 0x01, 0x0a, - ] + assert!( + class_code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadSuperAttr { .. })), + "CPython can_optimize_super_call() does not require function scope for two-argument super()" ); } #[test] - fn super_method_call_kw_names_use_attribute_location_like_cpython() { + fn module_super_symbol_blocks_zero_arg_super_optimization_like_cpython() { let code = compile_exec( "\ +super class C: - def f(self, x, y): - super().__init__( - x=x, - y=y) + def f(self): + return super().attr ", ); let f = find_code(&code, "f").expect("missing f code"); - let call_kw_index = f - .instructions - .iter() - .position(|unit| matches!(unit.op, Instruction::CallKw { .. })) - .expect("missing CALL_KW"); - let (kw_names, (location, end_location)) = f - .instructions - .iter() - .zip(&f.locations) - .take(call_kw_index) - .rev() - .find(|(unit, _)| matches!(unit.op, Instruction::LoadConst { .. })) - .expect("missing CALL_KW names tuple"); assert!( - matches!(kw_names.op, Instruction::LoadConst { .. }), - "expected keyword names tuple before CALL_KW" - ); - assert_eq!( - (location.line.get(), end_location.line.get()), - (3, 3), - "CPython maybe_optimize_method_call() passes the updated method-attribute loc into codegen_call_simple_kw_helper()" + !f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadSuperAttr { .. })), + "CPython can_optimize_super_call() rejects any top-level symbol-table entry for super" ); } @@ -14469,6 +16917,89 @@ def outer(): ); } + #[test] + fn explicit_return_value_locations_match_cpython_codegen_return() { + let code = compile_exec( + "\ +def dynamic(x): + return x + +def constant(): + return 1 + +def bare(): + return +", + ); + + let cases = [ + ("dynamic", vec![(2, 5, 2, 13)]), + ("constant", vec![(5, 12, 5, 13)]), + ("bare", vec![(8, 5, 8, 11)]), + ]; + for (name, expected) in cases { + let function = find_code(&code, name).expect("missing function code"); + let return_positions: Vec<_> = function + .instructions + .iter() + .zip(&function.locations) + .filter_map(|(unit, (location, end_location))| { + matches!(unit.op, Instruction::ReturnValue).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .collect(); + + assert_eq!( + return_positions, expected, + "CPython codegen_return() emits explicit return at loc for {name}" + ); + } + } + + #[test] + fn continue_jump_keeps_statement_location_like_cpython() { + let code = compile_exec( + "\ +def continues(xs): + for x in xs: + if x: + continue + use(x) +", + ); + + { + let (name, expected_position) = ("continues", (4, 13, 4, 21)); + let function = find_code(&code, name).expect("missing function code"); + let jump_positions: Vec<_> = function + .instructions + .iter() + .zip(&function.locations) + .filter_map(|(unit, (location, end_location))| { + matches!( + unit.op, + Instruction::JumpForward { .. } | Instruction::JumpBackward { .. } + ) + .then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .collect(); + + assert!( + jump_positions.contains(&expected_position), + "CPython codegen_continue() emits final jump at statement loc for {name}, got {jump_positions:?}" + ); + } + } + #[test] fn not_compare_uses_unary_location_like_cpython() { let code = compile_exec( @@ -14546,6 +17077,73 @@ def f(c): ); } + #[test] + fn typealias_value_scope_has_single_return_like_cpython() { + let code = compile_exec("type Alias = int\n"); + let alias = find_direct_child_code(&code, "Alias").expect("missing alias code"); + let return_count = alias + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) + .count(); + assert_eq!( + return_count, 1, + "CPython codegen_typealias_body() emits one RETURN_VALUE and assembles with addNone=0, got instructions={:?}", + alias.instructions + ); + } + + #[test] + fn generic_typealias_wrapper_return_uses_alias_location_like_cpython() { + let code = compile_exec("type A[T] = T\n"); + let type_params = + find_code(&code, "").expect("missing type params code"); + + // codegen_typealias() assembles the generic-parameters + // wrapper with addNone=0 after codegen_typealias_body() leaves the type + // alias object on the stack. The final RETURN_VALUE keeps LOC(type alias). + assert_eq!( + type_params.linetable.as_ref(), + &[ + 0xf8, 0x80, 0x00, 0x80, 0x0d, 0x84, 0x71, 0x87, 0x0d, 0x81, 0x0d + ], + ); + } + + #[test] + fn type_param_bound_scope_has_single_return_like_cpython() { + let code = compile_exec("type Alias[T: int] = T\n"); + let type_params = + find_code(&code, "").expect("missing type params code"); + let bound = find_direct_child_code(type_params, "T").expect("missing T bound code"); + let return_count = bound + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) + .count(); + assert_eq!( + return_count, 1, + "CPython codegen_type_param_bound_or_default() emits one explicit RETURN_VALUE before OptimizeAndAssemble(addNone=1), got instructions={:?}", + bound.instructions + ); + } + + #[test] + fn class_body_scope_has_single_return_like_cpython() { + let code = compile_exec("class C:\n pass\n"); + let class_code = find_code(&code, "C").expect("missing class code"); + let return_count = class_code + .instructions + .iter() + .filter(|unit| matches!(unit.op, Instruction::ReturnValue)) + .count(); + assert_eq!( + return_count, 1, + "CPython codegen_class_body() emits one explicit RETURN_VALUE before OptimizeAndAssemble(addNone=1), got instructions={:?}", + class_code.instructions + ); + } + #[test] fn generic_function_annotation_scope_uses_function_location_like_cpython() { let code = compile_exec("def f[T](x: int): ...\n"); @@ -14565,6 +17163,23 @@ def f(c): ); } + #[test] + fn decorated_generic_function_type_params_use_decorator_firstlineno_like_cpython() { + let code = compile_exec( + "\ +def deco(obj): return obj +@deco +def f[T](): pass +", + ); + let type_params = + find_code(&code, "").expect("missing type params code"); + + // codegen_function() passes firstlineno, not LOC(s).lineno, to + // the generic-parameters scope. + assert_eq!(type_params.first_line_number.unwrap().get(), 2); + } + #[test] fn generic_class_type_params_store_uses_class_location_like_cpython() { let code = compile_exec( @@ -14656,6 +17271,160 @@ def f(): ); } + #[test] + fn try_except_else_finally_child_scopes_follow_cpython_symbol_order() { + let code = compile_exec( + "\ +def f(x): + try: + pass + except Exception: + y = 1 + def h(): + return y + else: + def e(): + return x + finally: + def z(): + return x +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let h = find_code(f, "h").expect("missing handler function code"); + let e = find_code(f, "e").expect("missing else function code"); + let z = find_code(f, "z").expect("missing finally function code"); + + assert_eq!( + h.freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["y"], + "handler child scope should consume the handler symbol table" + ); + assert_eq!( + e.freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["x"], + "else child scope should be consumed before handler scopes, matching CPython codegen_try_except()" + ); + assert_eq!( + z.freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["x"], + "finally child scope should remain after body/else/handler scopes" + ); + } + + #[test] + fn try_star_child_scopes_follow_codegen_order_like_cpython() { + let code = compile_exec( + "\ +def f(x): + try: + pass + except* Exception: + y = 1 + def h(): + return y + else: + def e(): + return x +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let h = find_code(f, "h").expect("missing except* handler function code"); + let e = find_code(f, "e").expect("missing else function code"); + + assert_eq!( + h.freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["y"], + "except* handler child scope should consume handler symbol table before else" + ); + assert_eq!( + e.freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["x"], + "except* else child scope should be consumed after handler scopes, matching CPython codegen_try_star_except()" + ); + } + + #[test] + fn function_default_and_decorator_child_scopes_follow_cpython_symbol_order() { + fn direct_child_codes<'a>(code: &'a CodeObject, name: &str) -> Vec<&'a CodeObject> { + code.constants + .iter() + .filter_map(|constant| { + if let ConstantData::Code { code } = constant + && code.obj_name == name + { + Some(code.as_ref()) + } else { + None + } + }) + .collect() + } + + let code = compile_exec( + "\ +def outer(x, deco): + @(lambda f: deco(f)) + def inner(a=(lambda: x)()): + return a +", + ); + let outer = find_code(&code, "outer").expect("missing outer function code"); + let lambdas = direct_child_codes(outer, ""); + + assert_eq!(lambdas.len(), 2); + assert_eq!( + lambdas[0] + .freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["deco"], + "decorator lambda is emitted first by codegen_function()" + ); + assert_eq!( + lambdas[1] + .freevars + .iter() + .map(|name| name.as_str()) + .collect::>(), + vec!["x"], + "default lambda should still consume the default symbol table" + ); + } + + #[test] + fn decorated_generic_class_type_params_use_decorator_firstlineno_like_cpython() { + let code = compile_exec( + "\ +def deco(obj): return obj +@deco +class C[T]: pass +", + ); + let type_params = + find_code(&code, "").expect("missing type params code"); + + // codegen_class() also enters the generic-parameters scope with + // firstlineno, which is the first decorator line when decorators exist. + assert_eq!(type_params.first_line_number.unwrap().get(), 2); + } + #[test] fn class_deferred_annotations_use_class_body_location_like_cpython() { let code = compile_exec( @@ -14729,6 +17498,45 @@ g = lambda i: {**i} ); } + #[test] + fn dict_unpacking_large_regular_run_uses_subdict_chunks_like_cpython() { + let pairs = (0..17) + .map(|i| format!("{i}: {i}")) + .collect::>() + .join(", "); + let source = format!("def f(x):\n return {{{pairs}, **x}}\n"); + let code = compile_exec(&source); + let f = find_code(&code, "f").expect("missing f code"); + let first_dict_update = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::DictUpdate { .. })) + .expect("missing DICT_UPDATE"); + let prefix = &f.instructions[..first_dict_update]; + let build_map_args: Vec<_> = prefix + .iter() + .filter_map(|unit| { + matches!(unit.op, Instruction::BuildMap { .. }).then_some(u8::from(unit.arg)) + }) + .collect(); + let map_adds = prefix + .iter() + .filter(|unit| matches!(unit.op, Instruction::MapAdd { .. })) + .count(); + + assert_eq!( + build_map_args, + vec![0], + "CPython codegen_dict() routes a 17-pair run before ** through codegen_subdict(), got instructions={:?}", + f.instructions + ); + assert_eq!( + map_adds, 17, + "CPython codegen_subdict() uses MAP_ADD for all 17 pairs before **, got instructions={:?}", + f.instructions + ); + } + #[test] fn class_function_like_scopes_set_method_flag_like_cpython() { let code = compile_exec_with_options( @@ -14791,6 +17599,38 @@ class C: ); } + #[test] + fn class_inlined_comprehension_pushes_only_bound_locals_like_cpython() { + let code = compile_exec( + "\ +class C: + x = 1 + items = [x for i in range(3)] +", + ); + let class_code = find_code(&code, "C").expect("missing class code"); + let cleared_names = class_code + .instructions + .iter() + .filter_map(|unit| match unit.op { + Instruction::LoadFastAndClear { var_num } => { + let idx = var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))); + Some(class_code.varnames[usize::from(idx)].as_str()) + } + _ => None, + }) + .collect::>(); + + assert!( + cleared_names.contains(&"i"), + "the comprehension iteration variable should be isolated, got {cleared_names:?}" + ); + assert!( + !cleared_names.contains(&"x"), + "CPython applies the class-block special case while tweaking scopes, but codegen_push_inlined_comprehension_locals() runs after u_in_inlined_comp is set and only clears DEF_LOCAL names; got {cleared_names:?}" + ); + } + #[test] fn genexpr_implicit_iterator_is_not_posonly_like_cpython() { let code = compile_exec("x = (i for i in ())"); @@ -14803,6 +17643,29 @@ class C: ); } + #[test] + fn posonly_function_argcount_metadata_matches_cpython_assemble_split() { + let code = compile_exec("def f(a, /, b):\n pass\n"); + let func = find_code(&code, "f").expect("missing function code"); + + assert_eq!( + func.arg_count, 2, + "CPython assemble.c exposes co_argcount as u_posonlyargcount + u_argcount" + ); + assert_eq!(func.posonlyarg_count, 1); + assert_eq!(func.varnames.as_ref(), &["a".to_owned(), "b".to_owned()]); + assert_eq!( + func.localspluskinds[0] & (CO_FAST_ARG_POS | CO_FAST_ARG_KW), + CO_FAST_ARG_POS, + "CPython compute_localsplus_info marks only u_posonlyargcount slots as positional-only" + ); + assert_eq!( + func.localspluskinds[1] & (CO_FAST_ARG_POS | CO_FAST_ARG_KW), + CO_FAST_ARG_POS | CO_FAST_ARG_KW, + "CPython compute_localsplus_info marks u_argcount slots after posonly as positional-or-keyword" + ); + } + #[test] fn async_generator_uses_cpython_async_generator_flag() { let code = compile_exec_with_options( @@ -15189,6 +18052,27 @@ def f(a, b, c): .filter(|unit| !matches!(unit.op, Instruction::Cache)) } + fn full_opargs_for( + code: &CodeObject, + mut predicate: impl FnMut(Instruction) -> bool, + ) -> Vec { + let mut extended = 0u32; + let mut args = Vec::new(); + for unit in non_cache_instructions(code) { + let byte = u32::from(u8::from(unit.arg)); + if matches!(unit.op, Instruction::ExtendedArg) { + extended = (extended << 8) | byte; + continue; + } + let oparg = (extended << 8) | byte; + extended = 0; + if predicate(unit.op) { + args.push(oparg); + } + } + args + } + fn varname_index(code: &CodeObject, name: &str) -> usize { code.varnames .iter() @@ -15388,6 +18272,48 @@ def f(): ); } + #[test] + fn match_or_conflicting_bind_error_uses_or_pattern_location_like_cpython() { + let error = compile_exec_error( + "\ +def f(x): + match x: + case ( + a + | b + ): + pass +", + ); + let location = error.location.expect("missing error location"); + assert_eq!( + location.line.get(), + 4, + "CPython codegen_pattern_or() reports alternative binding mismatches at LOC(p), not LOC(alt)" + ); + } + + #[test] + fn match_or_duplicate_store_error_uses_or_pattern_location_like_cpython() { + let error = compile_exec_error( + "\ +def f(value): + match value: + case [ + x, + (x | x), + ]: + pass +", + ); + let location = error.location.expect("missing error location"); + assert_eq!( + location.line.get(), + 5, + "CPython codegen_pattern_or() reports merge-time duplicate stores at LOC(p)" + ); + } + #[test] fn match_success_jump_uses_no_location_like_cpython() { let code = compile_exec( @@ -15413,6 +18339,55 @@ def f(self): ); } + #[test] + fn match_default_simple_guard_jump_uses_guard_location_like_cpython() { + let code = compile_exec( + "\ +def f(x, y): + match x: + case 0: + return 1 + case _ if y: + return 2 + return 3 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let guard_jump_location = f + .instructions + .iter() + .enumerate() + .find_map(|(idx, unit)| { + let (Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }) = + unit.op + else { + return None; + }; + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + if f.varnames[usize::from(var_num.get(arg))] != "y" { + return None; + } + f.instructions + .iter() + .zip(&f.locations) + .skip(idx + 1) + .take(8) + .find_map(|(unit, (location, _))| { + matches!(unit.op, Instruction::PopJumpIfFalse { .. }).then_some(*location) + }) + }) + .expect("missing default guard jump"); + + assert_eq!( + ( + guard_jump_location.line.get(), + guard_jump_location.character_offset.get() + ), + (5, 19), + "CPython codegen_jump_if() receives LOC(pattern), but the simple guard fallback emits TO_BOOL/jump at LOC(guard)" + ); + } + #[test] fn match_mapping_keys_scaffolding_uses_mapping_location_like_cpython() { let code = compile_exec( @@ -15439,6 +18414,57 @@ def f(self): ); } + #[test] + fn match_mapping_rest_cleanup_uses_mapping_location_like_cpython() { + let code = compile_exec( + "\ +def f(x): + match x: + case { + 0: _, + **rest, + }: + return rest +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let rest_cleanup_start = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::BuildMap { .. })) + .expect("missing BUILD_MAP"); + for expected in [ + "BUILD_MAP", + "DICT_UPDATE", + "DELETE_SUBSCR", + "rest cleanup COPY", + "rest cleanup SWAP", + ] { + let location = f + .instructions + .iter() + .zip(&f.locations) + .skip(rest_cleanup_start) + .find_map(|(unit, (location, _))| { + let found = matches!( + (expected, unit.op), + ("BUILD_MAP", Instruction::BuildMap { .. }) + | ("DICT_UPDATE", Instruction::DictUpdate { .. }) + | ("DELETE_SUBSCR", Instruction::DeleteSubscr) + | ("rest cleanup COPY", Instruction::Copy { .. }) + | ("rest cleanup SWAP", Instruction::Swap { .. }) + ); + found.then_some(*location) + }) + .unwrap_or_else(|| panic!("missing {expected}")); + assert_eq!( + location.line.get(), + 3, + "CPython codegen_pattern_mapping() emits {expected} with LOC(p)" + ); + } + } + #[test] fn match_class_scaffolding_uses_class_pattern_location_like_cpython() { let code = compile_exec( @@ -15462,6 +18488,40 @@ def f(x): ); } + #[test] + fn match_class_wildcard_pop_uses_class_pattern_location_like_cpython() { + let code = compile_exec( + "\ +def f(x): + match x: + case bool( + _ + ): + return True +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let unpack_index = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })) + .expect("missing class pattern UNPACK_SEQUENCE"); + let wildcard_pop_location = f + .instructions + .iter() + .zip(&f.locations) + .skip(unpack_index + 1) + .find_map(|(unit, (location, _))| { + matches!(unit.op, Instruction::PopTop).then_some(*location) + }) + .expect("missing wildcard POP_TOP"); + assert_eq!( + wildcard_pop_location.line.get(), + 3, + "CPython codegen_pattern_class() emits wildcard POP_TOP with LOC(p)" + ); + } + #[test] fn while_try_body_layout_keeps_false_jump_to_anchor() { let code = compile_exec( @@ -16197,67 +19257,193 @@ def explicit_gen(xs): } #[test] - fn genexpr_filter_cleanup_jumps_use_element_location_like_cpython() { - let code = compile_exec( - "\ -def simple(names): - return (x for x in names if not _ishidden(x)) - -def boolop(fields): - return (f for f in fields if f.init and not f.kw_only) -", - ); - let simple = find_code(&code, "simple").expect("missing simple code"); - let simple_gen = find_code(simple, "").expect("missing simple genexpr code"); - let boolop = find_code(&code, "boolop").expect("missing boolop code"); - let boolop_gen = find_code(boolop, "").expect("missing boolop genexpr code"); + fn genexpr_filter_cleanup_jumps_use_element_location_like_cpython() { + let code = compile_exec( + "\ +def simple(names): + return (x for x in names if not _ishidden(x)) + +def boolop(fields): + return (f for f in fields if f.init and not f.kw_only) +", + ); + let simple = find_code(&code, "simple").expect("missing simple code"); + let simple_gen = find_code(simple, "").expect("missing simple genexpr code"); + let boolop = find_code(&code, "boolop").expect("missing boolop code"); + let boolop_gen = find_code(boolop, "").expect("missing boolop genexpr code"); + + // codegen_sync_comprehension_generator() emits the + // comprehension guard jump to if_cleanup, then emits the if_cleanup + // backedge with elt_loc. flowgraph.c::jump_thread() copies that target + // jump location to the threaded POP_JUMP/NOT_TAKEN cleanup path. + assert_eq!( + simple_gen.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x31, 0x91, 0x75, 0x90, 0x21, 0xa4, 0x49, 0xa8, + 0x61, 0xa7, 0x4c, 0x8f, 0x41, 0x8a, 0x41, 0x93, 0x75, 0xf9, + ] + ); + assert_eq!( + boolop_gen.linetable.as_ref(), + &[ + 0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x3a, 0x91, 0x76, 0x90, 0x21, 0xa7, 0x16, 0xa5, + 0x16, 0x8c, 0x41, 0xb0, 0x01, 0xb7, 0x09, 0xb5, 0x09, 0x8f, 0x41, 0x8a, 0x41, 0x93, + 0x76, 0xf9, + ] + ); + } + + #[test] + fn try_finally_exception_scaffolding_uses_no_location_like_cpython() { + let code = compile_exec( + "\ +def f(self, node): + self.flag = True + try: + self.body(node) + finally: + self.flag = False +", + ); + let f = find_code(&code, "f").expect("missing f code"); + + // codegen_try_finally() emits the exception path + // SETUP_CLEANUP/PUSH_EXC_INFO and POP_EXCEPT_AND_RERAISE with + // NO_LOCATION; flowgraph line propagation then gives only the + // finalbody's direct RERAISE the finalbody location. + assert_eq!( + f.linetable.as_ref(), + &[ + 0x80, 0x00, 0xd8, 0x10, 0x14, 0x80, 0x44, 0x84, 0x49, 0xf0, 0x02, 0x03, 0x05, 0x1a, + 0xd8, 0x08, 0x0c, 0x8f, 0x09, 0x89, 0x09, 0x90, 0x24, 0x8c, 0x0f, 0xe0, 0x14, 0x19, + 0x88, 0x04, 0x8e, 0x09, 0xf8, 0x90, 0x45, 0x88, 0x04, 0x8d, 0x09, 0xfa, + ] + ); + } + + #[test] + fn return_debug_in_finally_uses_cpython_preprocessed_constant_order() { + let code = compile_exec( + "\ +def f(close): + try: + return __debug__ + finally: + close() +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let call_pos = f + .instructions + .iter() + .position(|unit| matches!(unit.op, Instruction::Call { .. })) + .expect("missing finally-body call"); + let debug_load_pos = f + .instructions + .iter() + .position(|unit| { + let Instruction::LoadConst { consti } = unit.op else { + return false; + }; + let constant = &f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))]; + matches!(constant, ConstantData::Boolean { value: true }) + }) + .expect("missing __debug__ constant load"); + + assert!( + call_pos < debug_load_pos, + "CPython ast_preprocess.c folds __debug__ to Constant before codegen_return(), so the return constant is loaded after finally cleanup; ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() + ); + } + + #[test] + fn debug_statement_is_preprocessed_constant_like_cpython() { + for code in [ + compile_exec("__debug__\n"), + compile_exec_optimized("__debug__\n"), + ] { + let ops = non_cache_instructions(&code) + .map(|unit| unit.op) + .collect::>(); + assert!( + !ops.iter().any(|op| matches!(op, Instruction::PopTop)), + "CPython ast_preprocess.c folds __debug__ to Constant before codegen_stmt_expr(), so it must not compile as LOAD_CONST/POP_TOP; ops={ops:?}" + ); + } + } + + #[test] + fn statement_expr_pop_top_uses_no_location_like_cpython() { + let infos = compile_module_instruction_infos("x + 1\n", Mode::Exec); + let pop = infos + .iter() + .find(|info| matches!(info.instr.real(), Some(Instruction::PopTop))) + .expect("missing expression-statement POP_TOP"); + + assert_eq!( + pop.lineno_override, + Some(ir::NO_LOCATION_OVERRIDE), + "CPython codegen_stmt_expr() emits artificial expression-statement POP_TOP at NO_LOCATION" + ); + } + + #[test] + fn interactive_statement_expr_pop_top_uses_no_location_like_cpython() { + let infos = compile_module_instruction_infos("x + 1\n", Mode::Single); + let print = infos + .iter() + .position(|info| { + matches!( + info.instr.real(), + Some(Instruction::CallIntrinsic1 { func }) + if func.get(info.arg) == bytecode::IntrinsicFunction1::Print + ) + }) + .expect("missing interactive PRINT intrinsic"); + let pop = infos + .get(print + 1) + .expect("missing POP_TOP after interactive PRINT"); - // CPython 3.14 codegen_sync_comprehension_generator() emits the - // comprehension guard jump to if_cleanup, then emits the if_cleanup - // backedge with elt_loc. flowgraph.c::jump_thread() copies that target - // jump location to the threaded POP_JUMP/NOT_TAKEN cleanup path. - assert_eq!( - simple_gen.linetable.as_ref(), - &[ - 0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x31, 0x91, 0x75, 0x90, 0x21, 0xa4, 0x49, 0xa8, - 0x61, 0xa7, 0x4c, 0x8f, 0x41, 0x8a, 0x41, 0x93, 0x75, 0xf9, - ] + assert!( + matches!(pop.instr.real(), Some(Instruction::PopTop)), + "CPython codegen_stmt_expr() emits POP_TOP immediately after INTRINSIC_PRINT; got {pop:?}" ); assert_eq!( - boolop_gen.linetable.as_ref(), - &[ - 0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x3a, 0x91, 0x76, 0x90, 0x21, 0xa7, 0x16, 0xa5, - 0x16, 0x8c, 0x41, 0xb0, 0x01, 0xb7, 0x09, 0xb5, 0x09, 0x8f, 0x41, 0x8a, 0x41, 0x93, - 0x76, 0xf9, - ] + pop.lineno_override, + Some(ir::NO_LOCATION_OVERRIDE), + "CPython codegen_stmt_expr() emits interactive PRINT cleanup POP_TOP at NO_LOCATION" ); } #[test] - fn try_finally_exception_scaffolding_uses_no_location_like_cpython() { - let code = compile_exec( - "\ -def f(self, node): - self.flag = True - try: - self.body(node) - finally: - self.flag = False -", - ); - let f = find_code(&code, "f").expect("missing f code"); + fn import_star_pop_top_uses_no_location_like_cpython() { + let infos = compile_module_instruction_infos("from m import *\n", Mode::Exec); + let import_star = infos + .iter() + .position(|info| { + matches!( + info.instr.real(), + Some(Instruction::CallIntrinsic1 { func }) + if func.get(info.arg) == bytecode::IntrinsicFunction1::ImportStar + ) + }) + .expect("missing IMPORT_STAR intrinsic"); + let pop = infos + .get(import_star + 1) + .expect("missing POP_TOP after IMPORT_STAR"); - // CPython 3.14 codegen_try_finally() emits the exception path - // SETUP_CLEANUP/PUSH_EXC_INFO and POP_EXCEPT_AND_RERAISE with - // NO_LOCATION; flowgraph line propagation then gives only the - // finalbody's direct RERAISE the finalbody location. + assert!( + matches!(pop.instr.real(), Some(Instruction::PopTop)), + "CPython codegen_from_import() emits POP_TOP immediately after INTRINSIC_IMPORT_STAR; got {pop:?}" + ); assert_eq!( - f.linetable.as_ref(), - &[ - 0x80, 0x00, 0xd8, 0x10, 0x14, 0x80, 0x44, 0x84, 0x49, 0xf0, 0x02, 0x03, 0x05, 0x1a, - 0xd8, 0x08, 0x0c, 0x8f, 0x09, 0x89, 0x09, 0x90, 0x24, 0x8c, 0x0f, 0xe0, 0x14, 0x19, - 0x88, 0x04, 0x8e, 0x09, 0xf8, 0x90, 0x45, 0x88, 0x04, 0x8d, 0x09, 0xfa, - ] + pop.lineno_override, + Some(ir::NO_LOCATION_OVERRIDE), + "CPython codegen_from_import() emits import-star cleanup POP_TOP at NO_LOCATION" ); } @@ -16372,9 +19558,9 @@ def prefixed(x): "CPython represents f'{{x=}}' debug text as a literal at the expression/debug-text location" ); assert_eq!( - string_load_position(prefixed, "a x="), - (5, 14, 5, 19), - "CPython extends a pending f-string literal through the debug text range" + string_load_position(prefixed, "x="), + (5, 17, 5, 19), + "CPython keeps debug text as a separate JoinedStr Constant instead of merging it with the preceding literal" ); } @@ -17115,6 +20301,98 @@ def f(cls, args, kwargs): } } + #[test] + fn method_call_at_stack_guideline_uses_plain_load_attr_like_cpython() { + let params = (0..STACK_USE_GUIDELINE) + .map(|i| format!("a{i}")) + .collect::>() + .join(", "); + let code = compile_exec(&format!( + "def f(obj, {params}):\n return obj.m({params})\n" + )); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let plain_load_attr = f.instructions.iter().any(|unit| { + if let Instruction::LoadAttr { namei } = unit.op { + !namei + .get(OpArg::new(u32::from(u8::from(unit.arg)))) + .is_method() + } else { + false + } + }); + let direct_call_30 = f.instructions.iter().any(|unit| match unit.op { + Instruction::Call { argc } => { + argc.get(OpArg::new(u32::from(u8::from(unit.arg)))) == STACK_USE_GUIDELINE + } + _ => false, + }); + + assert!( + plain_load_attr && direct_call_30, + "CPython maybe_optimize_method_call rejects arg count at the guideline, got ops={ops:?}" + ); + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::CallFunctionEx)), + "exactly guideline-sized method call should stay direct after LOAD_ATTR fallback, got ops={ops:?}" + ); + } + + #[test] + fn method_call_many_keywords_stays_load_method_call_kw_like_cpython() { + let params = (0..16) + .map(|i| format!("a{i}")) + .collect::>() + .join(", "); + let keywords = (0..16) + .map(|i| format!("k{i}=a{i}")) + .collect::>() + .join(", "); + let code = compile_exec(&format!( + "def f(obj, {params}):\n return obj.m({keywords})\n" + )); + let f = find_code(&code, "f").expect("missing function code"); + let ops: Vec<_> = f + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + + let method_load_attr = f.instructions.iter().any(|unit| { + if let Instruction::LoadAttr { namei } = unit.op { + namei + .get(OpArg::new(u32::from(u8::from(unit.arg)))) + .is_method() + } else { + false + } + }); + let call_kw_16 = f.instructions.iter().any(|unit| match unit.op { + Instruction::CallKw { argc } => { + argc.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 16 + } + _ => false, + }); + + assert!( + method_load_attr && call_kw_16, + "CPython maybe_optimize_method_call emits LOAD_METHOD/CALL_KW under its own stack threshold, got ops={ops:?}" + ); + assert!( + !ops.iter() + .any(|op| matches!(op, Instruction::CallFunctionEx)), + "method-call keyword path should not reuse codegen_call_helper_impl's lower kw threshold, got ops={ops:?}" + ); + } + #[test] fn large_plain_call_uses_direct_call_until_stack_guideline() { let code = compile_exec( @@ -17388,7 +20666,7 @@ def set_f(xs): ); assert!( !has_common_constant(list_f, bytecode::CommonConstant::BuiltinList), - "CPython 3.14.2 does not optimize list(genexpr)" + "CPython 3.14.5 does not optimize list(genexpr)" ); let set_f = find_code(&code, "set_f").expect("missing set_f code"); @@ -17401,7 +20679,7 @@ def set_f(xs): ); assert!( !has_common_constant(set_f, bytecode::CommonConstant::BuiltinSet), - "CPython 3.14.2 does not optimize set(genexpr)" + "CPython 3.14.5 does not optimize set(genexpr)" ); } @@ -17816,6 +21094,40 @@ def aug_const(x, y): ); } + #[test] + fn augassign_attribute_copy_uses_target_location_like_cpython() { + let code = compile_exec( + "\ +def f(obj, value): + obj.attr += value +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let copy_position = f + .instructions + .iter() + .zip(&f.locations) + .find_map(|(unit, (location, end_location))| { + let Instruction::Copy { i } = unit.op else { + return None; + }; + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + (i.get(arg) == 1).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .expect("missing augmented attribute COPY"); + + assert_eq!( + copy_position, + (2, 5, 2, 13), + "CPython codegen_augassign() emits COPY 1 at LOC(target) before updating to attr location" + ); + } + #[test] fn loop_return_reorders_backedge_before_exit_cleanup() { let code = compile_exec( @@ -18710,6 +22022,82 @@ t = t\"Value: {value=}\" ); } + #[test] + fn tstring_ops_restore_template_and_interpolation_locations_like_cpython() { + let code = compile_exec( + "\ +def f(x): + return t\"{x}\" +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let mut build_interpolation = None; + let mut build_tuple = None; + let mut build_template = None; + for (unit, (location, end_location)) in f.instructions.iter().zip(&f.locations) { + let range = ( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + ); + match unit.op { + Instruction::BuildInterpolation { .. } => build_interpolation = Some(range), + Instruction::BuildTuple { .. } => build_tuple = Some(range), + Instruction::BuildTemplate => build_template = Some(range), + _ => {} + } + } + + assert_eq!( + build_interpolation, + Some((2, 14, 2, 17)), + "CPython codegen_interpolation() restores LOC(Interpolation) after visiting the value; this direct codegen path uses the parser's Interpolation range" + ); + assert_eq!( + build_tuple, + Some((2, 12, 2, 18)), + "CPython codegen_template_str() emits the interpolations tuple at LOC(TemplateStr); this direct codegen path uses the parser's TemplateStr range" + ); + assert_eq!( + build_template, + Some((2, 12, 2, 18)), + "CPython codegen_template_str() emits BUILD_TEMPLATE at LOC(TemplateStr); this direct codegen path uses the parser's TemplateStr range" + ); + } + + #[test] + fn regular_call_push_null_uses_callee_location_like_cpython() { + let code = compile_exec( + "\ +def f(g, x): + return ( + g + )(x) +", + ); + let f = find_code(&code, "f").expect("missing f code"); + let push_null = f + .instructions + .iter() + .zip(&f.locations) + .find_map(|(unit, (location, end_location))| { + matches!(unit.op, Instruction::PushNull).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .expect("missing PUSH_NULL"); + + assert_eq!( + push_null, + (3, 9, 3, 10), + "CPython codegen_call() resets loc to LOC(func) before emitting PUSH_NULL; this direct codegen path uses the parser's callee range" + ); + } + #[test] fn tstring_literal_preserves_surrogate_wtf8() { let code = compile_exec("t = t\"\\ud800\""); @@ -19777,78 +23165,21 @@ def f(self): .iter() .position(|unit| match unit.op { Instruction::LoadAttr { namei } => { - let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); - f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "KEY" - } - _ => false, - }) - .expect("missing Keys.KEY attribute load"); - let prev = f.instructions[key_load_idx - 1].op; - assert!( - matches!(prev, Instruction::LoadFast { .. }), - "CPython optimize_load_fast() records MATCH_KEYS' no-input pseudo-ref with the produced-value loop index, so this consumed Keys load stays strong; got ops={:?}", - f.instructions - .iter() - .map(|unit| unit.op) - .collect::>() - ); - } - - #[test] - fn debug_trace_match_sequence_star_wildcard_layout() { - let trace = compile_single_function_late_cfg_trace( - "\ -def f(w): - match w: - case [x, *_, y]: - z = 0 - return x, y, z -", - "f", - ); - for (stage, dump) in trace { - eprintln!("=== {stage} ===\n{dump}"); - } - } - - #[test] - fn debug_trace_loop_break_bool_chain_layout() { - let trace = compile_single_function_late_cfg_trace( - "\ -def f(filters, text, category, module, lineno, defaultaction): - for item in filters: - action, msg, cat, mod, ln = item - if ((msg is None or msg.match(text)) and - issubclass(category, cat) and - (mod is None or mod.match(module)) and - (ln == 0 or lineno == ln)): - break - else: - action = defaultaction - return action -", - "f", - ); - for (stage, dump) in trace { - eprintln!("=== {stage} ===\n{dump}"); - } - } - - #[test] - fn debug_trace_loop_conditional_body_layout() { - let trace = compile_single_function_late_cfg_trace( - "\ -def f(new, old): - for replace in ['__module__', '__name__', '__qualname__', '__doc__']: - if hasattr(old, replace): - setattr(new, replace, getattr(old, replace)) - return new -", - "f", + let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "KEY" + } + _ => false, + }) + .expect("missing Keys.KEY attribute load"); + let prev = f.instructions[key_load_idx - 1].op; + assert!( + matches!(prev, Instruction::LoadFast { .. }), + "CPython optimize_load_fast() records MATCH_KEYS' no-input pseudo-ref with the produced-value loop index, so this consumed Keys load stays strong; got ops={:?}", + f.instructions + .iter() + .map(|unit| unit.op) + .collect::>() ); - for (stage, dump) in trace { - eprintln!("=== {stage} ===\n{dump}"); - } } #[test] @@ -19984,58 +23315,6 @@ def f(self): ); } - #[test] - fn debug_trace_utf7_min_encode_layout() { - let trace = compile_single_function_late_cfg_trace( - "\ -def f(s, size, encodeSetO, encodeWhiteSpace): - inShift = True - base64bits = 0 - out = [] - for i, ch in enumerate(s): - if base64bits == 0: - if i + 1 < size: - ch2 = s[i + 1] - if E(ch2, encodeSetO, encodeWhiteSpace): - if B(ch2) or ch2 == '-': - out.append(b'-') - inShift = False - else: - out.append(b'-') - inShift = False - return out -", - "f", - ); - for (stage, dump) in trace { - eprintln!("=== {stage} ===\n{dump}"); - } - } - - #[test] - fn debug_trace_with_loop_break_bool_chain_layout() { - let trace = compile_single_function_late_cfg_trace( - "\ -def f(filters, text, category, module, lineno, defaultaction, _wm): - with _wm._lock: - for item in filters: - action, msg, cat, mod, ln = item - if ((msg is None or msg.match(text)) and - issubclass(category, cat) and - (mod is None or mod.match(module)) and - (ln == 0 or lineno == ln)): - break - else: - action = defaultaction - return action -", - "f", - ); - for (stage, dump) in trace { - eprintln!("=== {stage} ===\n{dump}"); - } - } - #[test] fn try_except_else_with_finally_keeps_with_handler_before_outer_except() { let code = compile_exec( @@ -20998,6 +24277,63 @@ def f(lines, close): ); } + #[test] + fn try_finally_return_inside_with_pops_unwound_fblocks_for_finalbody() { + let code = compile_exec( + "\ +def f(cm): + try: + with cm: + return 1 + finally: + return 2 +", + ); + let f = find_code(&code, "f").expect("missing f code"); + assert!( + f.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::ReturnValue)), + "return inside with/try-finally should compile without leaving an invalid CFG" + ); + } + + #[test] + fn except_star_return_after_with_unwind_uses_no_location_like_cpython() { + let err = compile_exec_error( + "\ +def f(cm): + try: + pass + except* Exception: + with cm: + return 1 +", + ); + assert!(matches!( + err.error, + CodegenErrorType::BreakContinueReturnInExceptStar + )); + assert!( + err.location.is_none(), + "CPython codegen_unwind_fblock(WITH) sets *ploc = NO_LOCATION before the except* error" + ); + } + + #[test] + fn async_generator_return_value_error_message_matches_cpython() { + assert_eq!( + compile_exec_error_message( + "\ +async def f(): + yield 1 + return 2 +" + ), + "'return' with value in async generator" + ); + } + #[test] fn try_except_finally_handler_normal_exit_keeps_nointerrupt_jump() { let code = compile_exec( @@ -24081,6 +27417,66 @@ class C: ); } + #[test] + fn optimize_two_strips_docstrings_during_preprocess() { + let code = compile_exec_with_options( + "\ +\"module doc\" + +def f(): + \"function doc\" + return 1 + +class C: + \"class doc\" + x = 1 +", + CompileOpts { + optimize: 2, + ..CompileOpts::default() + }, + ); + + assert!( + !code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__doc__" + ) + }), + "module docstring should be stripped before codegen, got instructions={:?}", + code.instructions + ); + + let function_code = find_code(&code, "f").expect("missing function code"); + assert!( + !function_code + .flags + .contains(bytecode::CodeFlags::HAS_DOCSTRING), + "function docstring should not set HAS_DOCSTRING when optimize=2" + ); + + let class_code = find_code(&code, "C").expect("missing class code"); + assert!( + !class_code.instructions.iter().any(|unit| { + matches!( + unit.op, + Instruction::StoreName { namei } + if class_code.names + [namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize] + .as_str() + == "__doc__" + ) + }), + "class docstring should be stripped before codegen, got instructions={:?}", + class_code.instructions + ); + } + #[test] fn future_annotations_flag_is_inherited_like_cpython() { let code = compile_exec( @@ -24104,6 +27500,108 @@ def f(): ); } + #[test] + fn future_flags_from_compile_options_are_merged_like_cpython() { + let opts = CompileOpts { + future_features: bytecode::CodeFlags::FUTURE_ANNOTATIONS + | bytecode::CodeFlags::FUTURE_DIVISION, + ..CompileOpts::default() + }; + let code = compile_exec_with_options( + "\ +x: int +def f(): + pass +", + opts, + ); + assert!(code.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS)); + assert!(code.flags.contains(bytecode::CodeFlags::FUTURE_DIVISION)); + assert!( + code.instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::SetupAnnotations)) + ); + let f = find_code(&code, "f").expect("missing f code"); + assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS)); + assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_DIVISION)); + } + + #[test] + fn future_barry_as_flufl_is_accepted_but_ignored() { + let code = compile_exec( + "\ +from __future__ import barry_as_FLUFL + +def f(): + pass +", + ); + let future_flags = 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; + assert!((code.flags & future_flags).is_empty()); + let f = find_code(&code, "f").expect("missing f code"); + assert!((f.flags & future_flags).is_empty()); + } + + #[test] + fn relative_future_import_does_not_enable_annotations_like_cpython() { + let code = compile_exec( + "\ +from .__future__ import annotations +x: int +", + ); + assert!(!code.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS)); + } + + #[test] + fn future_braces_uses_cpython_special_error() { + assert_eq!( + compile_exec_error_message("from __future__ import braces\n"), + "not a chance" + ); + } + + #[test] + fn invalid_future_feature_is_checked_before_ast_preprocess_like_cpython() { + assert_eq!( + compile_exec_error_message("from __future__ import spam, annotations\nx: (y := int)\n"), + "future feature spam is not defined" + ); + } + + #[test] + fn allow_top_level_await_marks_module_coroutine_like_cpython() { + let opts = CompileOpts { + allow_top_level_await: true, + ..CompileOpts::default() + }; + let code = compile_exec_with_options("await f()\n", opts); + assert!(code.flags.contains(bytecode::CodeFlags::COROUTINE)); + } + + #[test] + fn allow_top_level_await_accepts_module_async_for_like_cpython() { + let opts = CompileOpts { + allow_top_level_await: true, + ..CompileOpts::default() + }; + let code = compile_exec_with_options( + "\ +async for x in y: + pass +", + opts, + ); + assert!(code.flags.contains(bytecode::CodeFlags::COROUTINE)); + } + #[test] fn annotation_scope_nested_flag_matches_cpython() { let code = compile_exec( @@ -26803,21 +30301,90 @@ def f(cm, func, args, kwds): let return_positions: Vec<_> = f .instructions .iter() - .zip(&f.locations) - .filter_map(|(unit, (location, end_location))| { - matches!(unit.op, Instruction::ReturnValue).then_some(( - location.line.get(), - location.character_offset.get(), - end_location.line.get(), - end_location.character_offset.get(), + .zip(&f.locations) + .filter_map(|(unit, (location, end_location))| { + matches!(unit.op, Instruction::ReturnValue).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .collect(); + + assert_eq!( + return_positions, + vec![(2, 10, 2, 12), (2, 10, 2, 12)], + "CPython codegen_unwind_fblock(WITH) leaves RETURN_VALUE inheriting the context expression location" + ); + } + + #[test] + fn with_normal_cleanup_jump_uses_context_expr_location_like_cpython() { + let source = "\ +with cm: + pass +x = 1 +"; + let mut opts = CompileOpts::default(); + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap(); + let mut ast = parsed.into_syntax(); + opts.future_features |= preprocess::future_features(&ast); + let future_annotations = opts + .future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false); + let ast = match ast { + ruff_python_ast::Mod::Module(stmts) => stmts, + _ => unreachable!(), + }; + let symbol_table = SymbolTable::scan_program_with_options( + &ast, + source_file.clone(), + opts.allow_top_level_await, + opts.future_features + .contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS), + opts.recursion_limit, + ) + .map_err(|e| e.into_codegen_error(source_file.name().to_owned())) + .unwrap(); + let mut compiler = + Compiler::new_with_syntax_warning_handler(opts, source_file, "", None); + compiler.compile_program(&ast, symbol_table).unwrap(); + + let jump_positions = compiler + .current_code_info() + .blocks + .iter() + .flat_map(|block| block.used_instructions()) + .filter_map(|info| { + matches!( + info.instr, + AnyInstruction::Pseudo(PseudoInstruction::Jump { .. }) + ) + .then_some(( + ( + info.location.line.get(), + info.location.character_offset.get(), + info.end_location.line.get(), + info.end_location.character_offset.get(), + ), + info.lineno_override, )) }) - .collect(); + .collect::>(); - assert_eq!( - return_positions, - vec![(2, 10, 2, 12), (2, 10, 2, 12)], - "CPython codegen_unwind_fblock(WITH) leaves RETURN_VALUE inheriting the context expression location" + assert!( + jump_positions + .iter() + .any(|(position, lineno_override)| *position == (1, 6, 1, 8) + && *lineno_override != Some(ir::NO_LOCATION_OVERRIDE)), + "CPython codegen_with_inner() emits the normal-exit JUMP at LOC(context_expr), not NO_LOCATION; got {jump_positions:?}" ); } @@ -27133,6 +30700,44 @@ def f(x): assert_eq!(join_attr_count, 1); } + #[test] + fn large_fstring_join_scaffolding_uses_joinedstr_location_like_cpython() { + let mut source = String::from("def f(x):\n return f\""); + for _ in 0..=STACK_USE_GUIDELINE { + source.push_str("{x}"); + } + source.push_str("\"\n"); + + let code = compile_exec(&source); + let f = find_code(&code, "f").expect("missing function code"); + let fstring_end = " return ".len() + + 3 + + 3 * usize::try_from(STACK_USE_GUIDELINE + 1).expect("guideline overflowed") + + 1; + let expected = (2, 12, 2, fstring_end); + + for (unit, (location, end_location)) in f.instructions.iter().zip(&f.locations) { + if matches!( + unit.op, + Instruction::BuildList { .. } + | Instruction::ListAppend { .. } + | Instruction::Call { .. } + ) { + assert_eq!( + ( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + ), + expected, + "CPython codegen_joined_str() emits join scaffolding at LOC(JoinedStr); this direct codegen path uses the parser's FString range, op={:?}", + unit.op + ); + } + } + } + #[test] fn large_power_is_not_constant_folded() { let code = compile_exec("x = 2**100\n"); @@ -27633,6 +31238,84 @@ class C: assert_eq!(varnames, vec!["format"]); } + #[test] + fn future_function_signature_annotation_uses_hidden_block_like_cpython() { + let code = compile_exec( + "\ +from __future__ import annotations +def f(x: T): pass +", + ); + let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + let varnames = annotate + .varnames + .iter() + .map(|name| name.as_str()) + .collect::>(); + assert_eq!(varnames, vec!["format"]); + assert!( + find_code(&code, "f").is_some(), + "function body symbol-table cursor must skip the hidden AnnotationBlock" + ); + } + + #[test] + fn future_unannotated_function_does_not_hide_next_annotation_block() { + let code = compile_exec( + "\ +from __future__ import annotations +def plain(x): pass +def annotated(x: int): pass +", + ); + let annotate = find_direct_child_code(&code, "__annotate__") + .expect("second function must retain its annotation closure"); + assert!( + annotate.constants.iter().any( + |constant| matches!(constant, ConstantData::Str { value } if value.as_str() == Ok("int")) + ), + "annotation closure must belong to the annotated function" + ); + } + + #[test] + fn deferred_annotation_format_name_does_not_capture_helper_parameter() { + let code = compile_exec( + "\ +format = object() +x: format +", + ); + let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code"); + let varnames = annotate + .varnames + .iter() + .map(|name| name.as_str()) + .collect::>(); + assert_eq!(varnames, vec!["format"]); + assert!( + annotate.names.iter().any(|name| name.as_str() == "format"), + "CPython keeps the helper parameter as internal .format during symbol analysis, so annotation expression `format` must remain a separate name; got names={:?}", + annotate.names + ); + + let helper_param_loads = annotate + .instructions + .iter() + .filter(|unit| match unit.op { + Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => { + let arg = OpArg::new(u32::from(u8::from(unit.arg))); + annotate.varnames[usize::from(var_num.get(arg))].as_str() == "format" + } + _ => false, + }) + .count(); + assert_eq!( + helper_param_loads, 1, + "only the CPython format-validation prologue should load the helper parameter; annotation expression `format` must not compile as LOAD_FAST" + ); + } + #[test] fn non_simple_class_annotation_is_not_deferred_like_cpython() { let code = compile_exec( @@ -27680,6 +31363,106 @@ class C: ); } + #[test] + fn class_deferred_annotations_guard_only_conditional_entries_like_cpython() { + let code = compile_exec( + "\ +class C: + x: int + if flag: + y: str + z: float +", + ); + let class_code = find_code(&code, "C").expect("missing class code"); + let class_ops: Vec<_> = class_code + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let class_set_adds = class_ops + .iter() + .filter(|op| matches!(op, Instruction::SetAdd { .. })) + .count(); + assert_eq!( + class_set_adds, 1, + "CPython _PyCompile_AddDeferredAnnotation() adds class annotations to __conditional_annotations__ only inside conditional blocks, got ops={class_ops:?}" + ); + assert!( + class_code.instructions.iter().any(|unit| match unit.op { + Instruction::LoadDeref { i } => { + let idx = i.get(OpArg::new(u32::from(u8::from(unit.arg)))).as_usize(); + localsplus_name(class_code, idx) == Some("__conditional_annotations__") + } + _ => false, + }), + "CPython codegen_annassign() emits LOAD_DEREF for class __conditional_annotations__, got ops={class_ops:?}" + ); + assert!( + !class_code + .instructions + .iter() + .any(|unit| matches!(unit.op, Instruction::LoadFromDictOrDeref { .. })), + "CPython codegen_annassign() bypasses codegen_nameop for class __conditional_annotations__, got ops={class_ops:?}" + ); + + let annotate = find_code(class_code, "__annotate__").expect("missing __annotate__ code"); + let annotate_ops: Vec<_> = annotate + .instructions + .iter() + .map(|unit| unit.op) + .filter(|op| !matches!(op, Instruction::Cache)) + .collect(); + let annotation_body = annotate_ops + .iter() + .position(|op| matches!(op, Instruction::BuildMap { .. })) + .map(|idx| &annotate_ops[idx..]) + .expect("missing annotation map build"); + let guarded_entries = annotation_body + .iter() + .filter(|op| matches!(op, Instruction::PopJumpIfFalse { .. })) + .count(); + assert_eq!( + guarded_entries, 1, + "CPython codegen_deferred_annotations_body() guards only conditional class annotations, got ops={annotate_ops:?}" + ); + } + + #[test] + fn future_annotations_non_simple_target_checks_target_but_not_annotation_like_cpython() { + let code = compile_exec( + "\ +from __future__ import annotations +class C: + target[item]: missing +", + ); + let class_code = find_code(&code, "C").expect("missing class code"); + let loaded_names: Vec<_> = class_code + .instructions + .iter() + .filter_map(|unit| match unit.op { + Instruction::LoadName { namei } => { + let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))); + Some(class_code.names[usize::try_from(idx).unwrap()].as_str()) + } + _ => None, + }) + .collect(); + + assert!( + ["target", "item"] + .iter() + .all(|name| loaded_names.contains(name)), + "CPython codegen_annassign() still checks bare complex annotation targets under future annotations, got loaded_names={loaded_names:?}" + ); + assert!( + !loaded_names.contains(&"missing"), + "CPython codegen_check_annotation() skips the annotation expression under future annotations, got loaded_names={loaded_names:?}" + ); + } + #[test] fn type_param_evaluator_uses_dot_format_varname() { let code = compile_exec( @@ -27784,7 +31567,7 @@ def func[T](a: T = 'a', *, b: T = 'b'): } #[test] - fn generic_function_type_params_varnames_include_defaults_like_cpython() { + fn generic_function_type_params_omit_defaults_without_defaults_like_cpython() { let code = compile_exec( "\ def func[T](): @@ -27800,8 +31583,29 @@ def func[T](): .iter() .map(String::as_str) .collect::>(), - vec![".defaults", "T"] + vec!["T"] + ); + } + + #[test] + fn generic_function_type_params_split_defaults_like_cpython() { + let code = compile_exec( + "\ +def with_pos[T](a: T = 1): + pass +def with_kw[U](*, a: U = 1): + pass +", ); + let with_pos = + find_code(&code, "").expect("missing type params code"); + let with_kw = + find_code(&code, "").expect("missing type params code"); + + assert!(with_pos.varnames.iter().any(|name| name == ".defaults")); + assert!(!with_pos.varnames.iter().any(|name| name == ".kwdefaults")); + assert!(!with_kw.varnames.iter().any(|name| name == ".defaults")); + assert!(with_kw.varnames.iter().any(|name| name == ".kwdefaults")); } #[test] @@ -28029,6 +31833,39 @@ class C[T]: } } + #[test] + fn non_inlined_listcomp_return_uses_comprehension_location_like_cpython() { + let code = compile_exec( + "\ +class C[T]: + class Inner[U]( + make_base([T for _ in (1,)]) + ): + pass +", + ); + let listcomp = find_code(&code, "").expect("missing listcomp code"); + let return_positions: Vec<_> = listcomp + .instructions + .iter() + .zip(&listcomp.locations) + .filter_map(|(unit, (location, end_location))| { + matches!(unit.op, Instruction::ReturnValue).then_some(( + location.line.get(), + location.character_offset.get(), + end_location.line.get(), + end_location.character_offset.get(), + )) + }) + .collect(); + + assert_eq!( + return_positions, + vec![(3, 19, 3, 36)], + "CPython codegen_comprehension() emits non-gen RETURN_VALUE at LOC(e)" + ); + } + #[test] fn class_annotation_global_resolution_matches_cpython() { let class_global = compile_exec( @@ -28384,6 +32221,21 @@ def tuple_or_tuple(): ); } + #[test] + fn chained_compare_jump_if_runs_cpython_check_compare_warning() { + let message = first_exec_warning( + "\ +def f(x): + if 1 is 1 < x: + return x +", + ); + assert!( + message.contains("\"is\" with 'int' literal"), + "CPython codegen_jump_if() checks chained comparisons before conditional lowering, got {message:?}" + ); + } + #[test] fn lambda_without_body_constants_keeps_none_like_cpython() { let code = compile_exec("f = lambda x: x"); @@ -28397,6 +32249,17 @@ def tuple_or_tuple(): ); } + #[test] + fn generator_lambda_without_body_constants_omits_none_like_cpython() { + let code = compile_exec("f = lambda x: (yield x)"); + let lambda = find_code(&code, "").expect("missing lambda code"); + + assert!( + lambda.constants.is_empty(), + "CPython codegen_lambda() assembles generator lambdas with addNone=0" + ); + } + #[test] fn call_function_ex_empty_args_tuple_is_folded_late_like_cpython() { let code = compile_exec( @@ -28529,6 +32392,39 @@ f = lambda x: x in {0} ))); } + #[test] + fn frozenset_membership_consts_deduplicate_like_cpython_constant_key() { + let code = compile_exec( + "\ +def f(x): + return x in {1, 2}, x in {2, 1}, x in {1, 1} +", + ); + let f = find_code(&code, "f").expect("missing function code"); + let frozensets: Vec<_> = f + .constants + .iter() + .filter_map(|constant| match constant { + ConstantData::Frozenset { elements } => Some(elements.as_slice()), + _ => None, + }) + .collect(); + + assert_eq!( + frozensets.len(), + 2, + "CPython folds equal frozensets to the same const key and removes duplicate set items" + ); + assert!( + frozensets.iter().any(|elements| elements.len() == 2), + "missing shared frozenset constant for {{1, 2}} and {{2, 1}}" + ); + assert!( + frozensets.iter().any(|elements| elements.len() == 1), + "missing duplicate-collapsed frozenset constant for {{1, 1}}" + ); + } + #[test] fn nonconstant_list_membership_uses_tuple() { let code = compile_exec( @@ -29314,7 +33210,7 @@ deoptmap = { } let comp = symbol_table - .sub_tables + .inlined_comprehension_blocks .first() .expect("missing comprehension symbol table"); assert!(comp.comp_inlined, "expected comprehension to be inlined"); @@ -29713,6 +33609,34 @@ values = ( ); } + #[test] + fn single_mode_returns_none_after_print_like_cpython() { + let code = compile_single("1\n"); + let ops = code + .instructions + .iter() + .filter(|unit| !matches!(unit.op, Instruction::Resume { .. })) + .collect::>(); + + assert!( + !ops.iter() + .any(|unit| matches!(unit.op, Instruction::Copy { .. })), + "CPython codegen_stmt_expr() prints and pops interactive expressions; it does not preserve the final expression as the code object's return value, got ops={ops:?}" + ); + let Some(load_none) = ops.iter().rev().nth(1) else { + panic!("missing final LOAD_CONST None before RETURN_VALUE, got ops={ops:?}"); + }; + let Instruction::LoadConst { consti } = load_none.op else { + panic!("missing final LOAD_CONST None before RETURN_VALUE, got ops={ops:?}"); + }; + let constant = &code.constants[consti.get(OpArg::new(u32::from(u8::from(load_none.arg))))]; + assert!(matches!(constant, ConstantData::None)); + assert!(matches!( + ops.last().map(|unit| unit.op), + Some(Instruction::ReturnValue) + )); + } + #[test] fn folded_multiline_bytes_binop_does_not_leave_operand_nops() { let code = compile_exec( diff --git a/crates/codegen/src/error.rs b/crates/codegen/src/error.rs index fb848354e86..668ceb605dc 100644 --- a/crates/codegen/src/error.rs +++ b/crates/codegen/src/error.rs @@ -3,21 +3,6 @@ use core::fmt::Display; use rustpython_compiler_core::SourceLocation; use thiserror::Error; -#[derive(Clone, Copy, Debug)] -pub enum PatternUnreachableReason { - NameCapture, - Wildcard, -} - -impl Display for PatternUnreachableReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NameCapture => write!(f, "name capture"), - Self::Wildcard => write!(f, "wildcard"), - } - } -} - // pub type CodegenError = rustpython_parser_core::source_code::LocatedError; #[derive(Error, Debug)] @@ -70,6 +55,8 @@ pub enum CodegenErrorType { SyntaxError(String), /// Multiple `*` detected MultipleStarArgs, + MultipleStarredExpressionsInSequencePattern, + MultipleStarredNamesInSequencePattern, /// Misplaced `*` expression InvalidStarExpr, /// Break statement outside of loop. @@ -87,18 +74,20 @@ pub enum CodegenErrorType { AsyncReturnValue, InvalidFuturePlacement, InvalidFutureFeature(String), - FunctionImportStar, + InvalidFutureBraces, + RecursionError, TooManyStarUnpack, + TooManyExpressionsInStarUnpackingSequencePattern, EmptyWithItems, EmptyWithBody, ForbiddenName, DuplicateStore(String), - UnreachablePattern(PatternUnreachableReason), - RepeatedAttributePattern, + UnreachableWildcardPattern, + UnreachableNameCapturePattern(String), + RepeatedAttributePattern(String), ConflictingNameBindPattern, /// break/continue/return inside except* block BreakContinueReturnInExceptStar, - NotImplementedYet, // RustPython marker for unimplemented features } impl core::error::Error for CodegenErrorType {} @@ -112,6 +101,12 @@ impl fmt::Display for CodegenErrorType { Self::MultipleStarArgs => { write!(f, "multiple starred expressions in assignment") } + Self::MultipleStarredExpressionsInSequencePattern => { + write!(f, "multiple starred expressions in sequence pattern") + } + Self::MultipleStarredNamesInSequencePattern => { + write!(f, "multiple starred names in sequence pattern") + } Self::InvalidStarExpr => write!(f, "can't use starred expression here"), Self::InvalidBreak => write!(f, "'break' outside loop"), Self::InvalidContinue => write!(f, "'continue' not properly in loop"), @@ -128,9 +123,7 @@ impl fmt::Display for CodegenErrorType { ) } Self::AsyncYieldFrom => write!(f, "'yield from' inside async function"), - Self::AsyncReturnValue => { - write!(f, "'return' with value inside async generator") - } + Self::AsyncReturnValue => write!(f, "'return' with value in async generator"), Self::InvalidFuturePlacement => write!( f, "from __future__ imports must occur at the beginning of the file" @@ -138,12 +131,16 @@ impl fmt::Display for CodegenErrorType { Self::InvalidFutureFeature(feat) => { write!(f, "future feature {feat} is not defined") } - Self::FunctionImportStar => { - write!(f, "import * only allowed at module level") + Self::InvalidFutureBraces => write!(f, "not a chance"), + Self::RecursionError => { + write!(f, "maximum recursion depth exceeded during compilation") } Self::TooManyStarUnpack => { write!(f, "too many expressions in star-unpacking assignment") } + Self::TooManyExpressionsInStarUnpackingSequencePattern => { + write!(f, "too many expressions in star-unpacking sequence pattern") + } Self::EmptyWithItems => { write!(f, "empty items on With") } @@ -153,14 +150,18 @@ impl fmt::Display for CodegenErrorType { Self::ForbiddenName => { write!(f, "forbidden attribute name") } - Self::DuplicateStore(s) => { - write!(f, "duplicate store {s}") + Self::DuplicateStore(s) => write!(f, "multiple assignments to name '{s}' in pattern"), + Self::UnreachableWildcardPattern => { + write!(f, "wildcard makes remaining patterns unreachable") } - Self::UnreachablePattern(reason) => { - write!(f, "{reason} makes remaining patterns unreachable") + Self::UnreachableNameCapturePattern(name) => { + write!( + f, + "name capture '{name}' makes remaining patterns unreachable" + ) } - Self::RepeatedAttributePattern => { - write!(f, "attribute name repeated in class pattern") + Self::RepeatedAttributePattern(name) => { + write!(f, "attribute name repeated in class pattern: {name}") } Self::ConflictingNameBindPattern => { write!(f, "alternative patterns bind different names") @@ -171,9 +172,6 @@ impl fmt::Display for CodegenErrorType { "'break', 'continue' and 'return' cannot appear in an except* block" ) } - Self::NotImplementedYet => { - write!(f, "RustPython does not implement this feature yet") - } } } } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index f22efe8e52d..12fba37f6c3 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -1,4 +1,4 @@ -use core::ops; +use core::ops::{Deref, DerefMut, Index, IndexMut}; use crate::{IndexMap, IndexSet, error::InternalError}; use malachite_bigint::BigInt; @@ -9,10 +9,10 @@ use rustpython_wtf8::Wtf8Buf; use rustpython_compiler_core::{ OneIndexed, SourceLocation, bytecode::{ - AnyInstruction, AnyOpcode, Arg, CO_FAST_ARG_KW, CO_FAST_ARG_POS, CO_FAST_ARG_VAR, - CO_FAST_CELL, CO_FAST_FREE, CO_FAST_HIDDEN, CO_FAST_LOCAL, CodeFlags, CodeObject, CodeUnit, - CodeUnits, ConstantData, InstrDisplayContext, Instruction, IntrinsicFunction1, OpArg, - OpArgByte, Opcode, PseudoInstruction, PseudoOpcode, PyCodeLocationInfoKind, oparg, + AnyInstruction, AnyOpcode, CO_FAST_ARG_KW, CO_FAST_ARG_POS, CO_FAST_ARG_VAR, CO_FAST_CELL, + CO_FAST_FREE, CO_FAST_HIDDEN, CO_FAST_LOCAL, CodeFlags, CodeObject, CodeUnit, CodeUnits, + ConstantData, InstrDisplayContext, Instruction, IntrinsicFunction1, OpArg, OpArgByte, + Opcode, PseudoInstruction, PseudoOpcode, PyCodeLocationInfoKind, oparg, }, varint::{write_signed_varint, write_varint}, }; @@ -78,15 +78,108 @@ impl ConstantPool { } } - pub fn insert_full(&mut self, constant: ConstantData) -> (usize, bool) { - // CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct - // because Python-level NaN keys do not compare equal. - if !Self::constant_contains_nan(&constant) - && let Some(idx) = self - .constants + fn frozenset_key_contains(elements: &[ConstantData], needle: &ConstantData) -> bool { + if Self::constant_contains_nan(needle) { + return false; + } + elements.iter().any(|element| { + !Self::constant_contains_nan(element) && Self::constant_key_eq(element, needle) + }) + } + + fn frozenset_key_eq(left: &[ConstantData], right: &[ConstantData]) -> bool { + left.iter() + .all(|element| Self::frozenset_key_contains(right, element)) + && right .iter() - .position(|existing| existing == &constant) - { + .all(|element| Self::frozenset_key_contains(left, element)) + } + + fn constant_key_eq(left: &ConstantData, right: &ConstantData) -> bool { + match (left, right) { + (ConstantData::Tuple { elements: left }, ConstantData::Tuple { elements: right }) => { + left.len() == right.len() + && left + .iter() + .zip(right.iter()) + .all(|(left, right)| Self::constant_key_eq(left, right)) + } + ( + ConstantData::Frozenset { elements: left }, + ConstantData::Frozenset { elements: right }, + ) => Self::frozenset_key_eq(left, right), + (ConstantData::Slice { elements: left }, ConstantData::Slice { elements: right }) => { + left.iter() + .zip(right.iter()) + .all(|(left, right)| Self::constant_key_eq(left, right)) + } + _ => left == right, + } + } + + fn canonicalize_constant_key(constant: ConstantData) -> crate::InternalResult { + 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 { + if Self::constant_contains_nan(constant) { + return None; + } + self.constants + .iter() + .position(|existing| Self::constant_key_eq(existing, constant)) + } + + pub fn insert_full(&mut self, constant: ConstantData) -> (usize, bool) { + let constant = Self::canonicalize_constant_key_infallible(constant); + if let Some(idx) = self.find_existing(&constant) { return (idx, false); } let idx = self.constants.len(); @@ -95,14 +188,8 @@ impl ConstantPool { } fn try_insert_full(&mut self, constant: ConstantData) -> crate::InternalResult<(usize, bool)> { - // CPython's _PyCode_ConstantKey() keeps NaN-bearing constants distinct - // because Python-level NaN keys do not compare equal. - if !Self::constant_contains_nan(&constant) - && let Some(idx) = self - .constants - .iter() - .position(|existing| existing == &constant) - { + let constant = Self::canonicalize_constant_key(constant)?; + if let Some(idx) = self.find_existing(&constant) { return Ok((idx, false)); } self.constants @@ -141,7 +228,7 @@ impl ConstantPool { } } -impl ops::Index for ConstantPool { +impl Index for ConstantPool { type Output = ConstantData; fn index(&self, idx: usize) -> &Self::Output { @@ -190,44 +277,34 @@ impl BlockIdx { Self(value) } - /// Returns the inner value as a [`usize`]. + /// Returns the inner [`u32`] value. #[must_use] - pub const fn idx(self) -> usize { - self.0 as usize - } -} - -impl From for u32 { - fn from(block_idx: BlockIdx) -> Self { - block_idx.0 + pub const fn as_u32(self) -> u32 { + self.0 } -} - -impl ops::Index for [Block] { - type Output = Block; - fn index(&self, idx: BlockIdx) -> &Block { - &self[idx.idx()] + /// Returns the inner value as a [`usize`]. + #[must_use] + pub const fn as_usize(self) -> usize { + self.0 as usize } -} -impl ops::IndexMut for [Block] { - fn index_mut(&mut self, idx: BlockIdx) -> &mut Block { - &mut self[idx.idx()] + /// Returns the inner value as a [`usize`]. + #[must_use] + pub const fn idx(self) -> usize { + self.as_usize() } } -impl ops::Index for Vec { - type Output = Block; - - fn index(&self, idx: BlockIdx) -> &Block { - &self[idx.idx()] +impl From for u32 { + fn from(block_idx: BlockIdx) -> Self { + block_idx.as_u32() } } -impl ops::IndexMut for Vec { - fn index_mut(&mut self, idx: BlockIdx) -> &mut Block { - &mut self[idx.idx()] +impl From for usize { + fn from(block_idx: BlockIdx) -> Self { + block_idx.as_usize() } } @@ -243,84 +320,206 @@ pub struct InstructionInfo { pub lineno_override: Option, } -/// Exception handler information for an instruction. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ExceptHandlerInfo { - /// Block to jump to when exception occurs - pub handler_block: BlockIdx, - /// Whether to push lasti before exception - pub preserve_lasti: bool, -} +impl InstructionInfo { + /// flowgraph.c INSTR_SET_OP0 + fn instr_set_op0(&mut self, instr: AnyInstruction) { + debug_assert!(!AnyOpcode::from(instr).has_arg()); + self.instr = instr; + self.arg = OpArg::new(0); + } -/// flowgraph.c INSTR_SET_OP0 -fn instr_set_op0(info: &mut InstructionInfo, instr: AnyInstruction) { - debug_assert!(!AnyOpcode::from(instr).has_arg()); - info.instr = instr; - info.arg = OpArg::new(0); -} + /// flowgraph.c INSTR_SET_OP1 + fn instr_set_op1(&mut self, instr: AnyInstruction, arg: OpArg) { + debug_assert!(AnyOpcode::from(instr).has_arg()); + self.instr = instr; + self.arg = arg; + } -/// flowgraph.c INSTR_SET_OP1 -fn instr_set_op1(info: &mut InstructionInfo, instr: AnyInstruction, arg: OpArg) { - debug_assert!(AnyOpcode::from(instr).has_arg()); - info.instr = instr; - info.arg = arg; -} + /// flowgraph.c INSTR_SET_LOC + fn instr_set_loc( + &mut self, + location: SourceLocation, + end_location: SourceLocation, + lineno_override: Option, + ) { + self.location = location; + self.end_location = end_location; + self.lineno_override = lineno_override; + } -/// flowgraph.c INSTR_SET_LOC -fn instr_set_loc( - info: &mut InstructionInfo, - location: SourceLocation, - end_location: SourceLocation, - lineno_override: Option, -) { - info.location = location; - info.end_location = end_location; - info.lineno_override = lineno_override; -} + fn instr_location(&self) -> InstructionLocation { + InstructionLocation { + location: self.location, + end_location: self.end_location, + lineno_override: self.lineno_override, + } + } -fn instr_location(info: &InstructionInfo) -> InstructionLocation { - InstructionLocation { - location: info.location, - end_location: info.end_location, - lineno_override: info.lineno_override, + fn instr_set_location(&mut self, loc: InstructionLocation) { + self.instr_set_loc(loc.location, loc.end_location, loc.lineno_override); } -} -fn instr_set_location(info: &mut InstructionInfo, loc: InstructionLocation) { - instr_set_loc(info, loc.location, loc.end_location, loc.lineno_override); -} + fn set_to_nop(&mut self) { + self.instr_set_op0(Instruction::Nop.into()); + } -fn no_instruction_location() -> InstructionLocation { - InstructionLocation { - location: SourceLocation::default(), - end_location: SourceLocation::default(), - lineno_override: Some(NO_LOCATION_OVERRIDE), + fn nop_out_no_location(&mut self) { + self.set_to_nop(); + self.instr_set_loc( + SourceLocation::default(), + SourceLocation::default(), + Some(NO_LOCATION_OVERRIDE), + ); + } + + #[must_use] + fn empty() -> Self { + Self { + instr: Instruction::Nop.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: None, + } + } + + /// instruction_sequence.c _PyInstructionSequence_Addop asserts. + fn instruction_sequence_debug_check_addop(&self) { + let opcode = AnyOpcode::from(self.instr); + debug_assert!(is_within_opcode_range(opcode)); + debug_assert!( + opcode.has_arg() || self.instr.has_target() || u32::from(self.arg) == 0, + "CPython _PyInstructionSequence_Addop requires either OPCODE_HAS_ARG, HAS_TARGET, or oparg == 0" + ); + debug_assert!( + u32::from(self.arg) < (1 << 30), + "CPython _PyInstructionSequence_Addop requires 0 <= oparg < (1 << 30)" + ); + } + + /// assemble.c instr_size + fn instr_size(&self) -> usize { + let opcode = self.instr.expect_real(); + let oparg = u32::from(self.arg) as i32; + debug_assert!( + self.instr.has_arg() || oparg == 0, + "CPython assemble.c instr_size requires OPCODE_HAS_ARG or oparg == 0" + ); + let extended_args = + (0xFF_FFFF < oparg) as usize + (0xFF_FF < oparg) as usize + (0xFF < oparg) as usize; + let caches = opcode.cache_entries(); + extended_args + 1 + caches + } + + fn instruction_linetable_location(&self) -> LineTableLocation { + match self.lineno_override { + Some(NO_LOCATION_OVERRIDE) => LineTableLocation { + line: NO_LOCATION_OVERRIDE, + end_line: NO_LOCATION_OVERRIDE, + col: NO_LOCATION_OVERRIDE, + end_col: NO_LOCATION_OVERRIDE, + }, + Some(LINE_ONLY_LOCATION_OVERRIDE) => LineTableLocation { + line: self.location.line.get() as i32, + end_line: self.end_location.line.get() as i32, + col: -1, + end_col: -1, + }, + Some(NEXT_LOCATION_OVERRIDE) => next_linetable_location(), + Some(lineno) => LineTableLocation { + line: lineno, + end_line: self.end_location.line.get() as i32, + col: self.location.character_offset.to_zero_indexed() as i32, + end_col: self.end_location.character_offset.to_zero_indexed() as i32, + }, + None => LineTableLocation { + line: self.location.line.get() as i32, + end_line: self.end_location.line.get() as i32, + col: self.location.character_offset.to_zero_indexed() as i32, + end_col: self.end_location.character_offset.to_zero_indexed() as i32, + }, + } + } + + /// flowgraph.c loads_const + const fn loads_const(&self) -> bool { + self.instr.has_const() || matches!(self.instr.real_opcode(), Some(Opcode::LoadSmallInt)) + } + + /// flowgraph.c STORES_TO + fn stores_to(&self) -> i32 { + match self.instr.into() { + AnyOpcode::Real(Opcode::StoreFast) + | AnyOpcode::Pseudo(PseudoOpcode::StoreFastMaybeNull) => u32::from(self.arg) as i32, + _ => -1, + } + } + + /// flowgraph.c maybe_instr_make_load_smallint + fn maybe_instr_make_load_smallint(&mut self, constant: &ConstantData) -> bool { + if let ConstantData::Integer { value } = constant + && let Some(small) = value.to_i32().filter(|v| (0..=255).contains(v)) + { + self.instr_set_op1(Opcode::LoadSmallInt.into(), OpArg::new(small as u32)); + return true; + } + false + } + + /// flowgraph.c make_super_instruction + fn make_super_instruction(inst1: &mut Self, inst2: &mut Self, super_op: AnyInstruction) { + let line1 = inst1.instruction_lineno(); + let line2 = inst2.instruction_lineno(); + if line1 >= 0 && line2 >= 0 && line1 != line2 { + return; + } + let arg1 = u32::from(inst1.arg); + let arg2 = u32::from(inst2.arg); + if arg1 >= 16 || arg2 >= 16 { + return; + } + inst1.instr_set_op1(super_op, OpArg::new((arg1 << 4) | arg2)); + inst2.set_to_nop(); + } + + fn instruction_lineno(&self) -> i32 { + match self.lineno_override { + Some(LINE_ONLY_LOCATION_OVERRIDE) | None => self.location.line.get() as i32, + Some(lineno) => lineno, + } + } + + fn instruction_is_no_location(&self) -> bool { + self.instruction_lineno() == NO_LOCATION_OVERRIDE + } + + /// flowgraph.c is_jump + fn is_jump(&self) -> bool { + self.instr.has_jump() } -} -fn set_to_nop(info: &mut InstructionInfo) { - instr_set_op0(info, Instruction::Nop.into()); + /// flowgraph.c is_block_push + fn is_block_push(&self) -> bool { + self.instr.is_block_push() + } } -fn nop_out_no_location(info: &mut InstructionInfo) { - set_to_nop(info); - instr_set_loc( - info, - SourceLocation::default(), - SourceLocation::default(), - Some(NO_LOCATION_OVERRIDE), - ); +/// Exception handler information for an instruction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ExceptHandlerInfo { + /// Block to jump to when exception occurs + pub handler_block: BlockIdx, + /// Whether to push lasti before exception + pub preserve_lasti: bool, } -fn empty_instruction_info() -> InstructionInfo { - InstructionInfo { - instr: Instruction::Nop.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, +fn no_instruction_location() -> InstructionLocation { + InstructionLocation { location: SourceLocation::default(), end_location: SourceLocation::default(), - except_handler: None, - lineno_override: None, + lineno_override: Some(NO_LOCATION_OVERRIDE), } } @@ -363,119 +562,6 @@ fn c_array_ensure_capacity( } } -/// flowgraph.c basicblock_next_instr -fn basicblock_next_instr(block: &mut Block) -> crate::InternalResult { - let off = block.instruction_used; - let new_allocation = c_array_ensure_capacity::( - block.instruction_allocation, - off + 1, - DEFAULT_BLOCK_SIZE, - )?; - if new_allocation > block.instruction_allocation { - if new_allocation > block.instructions.len() { - block - .instructions - .try_reserve_exact(new_allocation - block.instructions.len()) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - block - .instructions - .resize_with(new_allocation, empty_instruction_info); - } - block.instruction_allocation = new_allocation; - } - debug_assert!(block.instruction_allocation > off); - block.instruction_used += 1; - Ok(off) -} - -/// flowgraph.c basicblock_last_instr -fn basicblock_last_instr(block: &Block) -> Option<&InstructionInfo> { - debug_assert!(block.instruction_allocation >= block.instruction_used); - if block.instruction_used > 0 { - debug_assert!(!block.instructions.is_empty()); - Some(&block.instructions[block.instruction_used - 1]) - } else { - None - } -} - -/// flowgraph.c basicblock_last_instr -fn basicblock_last_instr_mut(block: &mut Block) -> Option<&mut InstructionInfo> { - debug_assert!(block.instruction_allocation >= block.instruction_used); - if block.instruction_used > 0 { - debug_assert!(!block.instructions.is_empty()); - Some(&mut block.instructions[block.instruction_used - 1]) - } else { - None - } -} - -/// flowgraph.c basicblock_addop -fn basicblock_addop(block: &mut Block, mut info: InstructionInfo) -> crate::InternalResult<()> { - let opcode = AnyOpcode::from(info.instr); - debug_assert!(is_within_opcode_range(opcode)); - debug_assert!(!info.instr.is_assembler()); - debug_assert!( - info.instr.has_arg() || info.instr.has_target() || u32::from(info.arg) == 0, - "CPython basicblock_addop requires OPCODE_HAS_ARG, HAS_TARGET, or oparg == 0" - ); - debug_assert!( - u32::from(info.arg) < (1 << 30), - "CPython basicblock_addop requires 0 <= oparg < (1 << 30)" - ); - let off = basicblock_next_instr(block)?; - let except_handler = block.instructions[off].except_handler; - info.target = BlockIdx::NULL; - info.except_handler = except_handler; - block.instructions[off] = info; - Ok(()) -} - -/// flowgraph.c basicblock_insert_instruction -fn basicblock_insert_instruction( - block: &mut Block, - pos: usize, - info: InstructionInfo, -) -> crate::InternalResult<()> { - let old_len = block.instruction_used; - debug_assert!(pos <= old_len); - basicblock_next_instr(block)?; - for i in (pos + 1..=old_len).rev() { - block.instructions[i] = block.instructions[i - 1]; - } - block.instructions[pos] = info; - Ok(()) -} - -/// flowgraph.c basicblock_append_instructions -fn basicblock_append_block_instructions( - blocks: &mut [Block], - to: BlockIdx, - from: BlockIdx, -) -> crate::InternalResult<()> { - debug_assert_ne!(to, from); - let from_len = blocks[from.idx()].instruction_used; - for i in 0..from_len { - let info = blocks[from.idx()].instructions[i]; - let off = basicblock_next_instr(&mut blocks[to.idx()])?; - blocks[to.idx()].instructions[off] = info; - } - Ok(()) -} - -/// flowgraph.c direct `b_iused = 0` -fn basicblock_clear(block: &mut Block) { - block.instruction_used = 0; -} - -/// CPython direct `b_instr[0]` access. Some passes set `b_iused = 0` -/// without clearing the backing array, so an empty basic block can still have -/// a first raw instruction slot. -fn basicblock_raw_first_instr_mut(block: &mut Block) -> &mut InstructionInfo { - debug_assert!(block.instruction_allocation > 0); - &mut block.instructions[0] -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct InstructionSequenceLabel(i32); @@ -621,20 +707,6 @@ fn instruction_sequence_new_label(seq: &mut InstructionSequence) -> InstructionS InstructionSequenceLabel(seq.next_free_label) } -/// instruction_sequence.c _PyInstructionSequence_Addop asserts. -fn instruction_sequence_debug_check_addop(info: &InstructionInfo) { - let opcode = AnyOpcode::from(info.instr); - debug_assert!(is_within_opcode_range(opcode)); - debug_assert!( - opcode.has_arg() || info.instr.has_target() || u32::from(info.arg) == 0, - "CPython _PyInstructionSequence_Addop requires either OPCODE_HAS_ARG, HAS_TARGET, or oparg == 0" - ); - debug_assert!( - u32::from(info.arg) < (1 << 30), - "CPython _PyInstructionSequence_Addop requires 0 <= oparg < (1 << 30)" - ); -} - /// instruction_sequence.c _PyInstructionSequence_SetAnnotationsCode fn instruction_sequence_set_annotations_code( seq: &mut InstructionSequence, @@ -645,7 +717,6 @@ fn instruction_sequence_set_annotations_code( } /// instruction_sequence.c _PyInstructionSequence_UseLabel -#[allow(clippy::needless_range_loop)] fn instruction_sequence_use_label( seq: &mut InstructionSequence, label: InstructionSequenceLabel, @@ -679,9 +750,8 @@ fn instruction_sequence_use_label( if label_map.len() < seq.label_map_allocation { label_map.resize(seq.label_map_allocation, INSTRUCTION_SEQUENCE_UNSET_LABEL); } - for i in old_size..seq.label_map_allocation { - label_map[i] = INSTRUCTION_SEQUENCE_UNSET_LABEL; - } + + label_map[old_size..seq.label_map_allocation].fill(INSTRUCTION_SEQUENCE_UNSET_LABEL); label_map[label.idx()] = seq.instr_used as i32; Ok(()) } @@ -691,7 +761,7 @@ fn instruction_sequence_addop( seq: &mut InstructionSequence, info: InstructionInfo, ) -> crate::InternalResult<&mut InstructionSequenceEntry> { - instruction_sequence_debug_check_addop(&info); + info.instruction_sequence_debug_check_addop(); let idx = instruction_sequence_next_inst(seq)?; let entry = &mut seq.instrs[idx]; entry.info = info; @@ -709,7 +779,6 @@ fn instruction_sequence_last_info_mut( } /// instruction_sequence.c _PyInstructionSequence_InsertInstruction -#[allow(clippy::needless_range_loop)] fn instruction_sequence_insert_instruction( seq: &mut InstructionSequence, pos: usize, @@ -720,27 +789,28 @@ fn instruction_sequence_insert_instruction( for i in (pos..last_idx).rev() { seq.instrs[i + 1] = seq.instrs[i]; } + seq.instrs[pos].info = info; if let Some(label_map) = &mut seq.label_map { let pos = pos as i32; - for lbl in 0..seq.label_map_allocation { - if label_map[lbl] >= pos { - label_map[lbl] += 1; + + for lbl in label_map.iter_mut().take(seq.label_map_allocation) { + if *lbl >= pos { + *lbl += 1; } } } + Ok(()) } /// instruction_sequence.c _PyInstructionSequence_ApplyLabelMap -#[allow(clippy::needless_range_loop, clippy::unnecessary_wraps)] -fn instruction_sequence_apply_label_map( - instrs: &mut InstructionSequence, -) -> crate::InternalResult<()> { +fn instruction_sequence_apply_label_map(instrs: &mut InstructionSequence) { { let Some(label_map) = instrs.label_map.as_ref() else { - return Ok(()); + return; }; + for i in 0..instrs.instr_used { let entry = &mut instrs.instrs[i]; if entry.info.instr.has_target() { @@ -758,81 +828,13 @@ fn instruction_sequence_apply_label_map( } } } + instrs.label_map = None; instrs.label_map_allocation = 0; - Ok(()) -} - -/// flowgraph.c _PyCfg_ToInstructionSequence -fn cfg_to_instruction_sequence( - blocks: &mut [Block], - instr_sequence: &mut InstructionSequence, -) -> crate::InternalResult<()> { - let mut label_id = 0; - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - blocks[block_idx.idx()].cpython_label = InstructionSequenceLabel::from_index(label_id); - label_id += 1; - block_idx = blocks[block_idx.idx()].next; - } - - block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let block_label = blocks[block_idx.idx()].cpython_label; - debug_assert!(is_label(block_label)); - instruction_sequence_use_label(instr_sequence, block_label)?; - - let instr_count = blocks[block_idx.idx()].instruction_used; - for i in 0..instr_count { - if blocks[block_idx.idx()].instructions[i].instr.has_target() { - let target_block = blocks[block_idx.idx()].instructions[i].target; - debug_assert!(target_block != BlockIdx::NULL); - let lbl = blocks[target_block.idx()].cpython_label; - debug_assert!(is_label(lbl)); - blocks[block_idx.idx()].instructions[i].arg = OpArg::new(lbl.0 as u32); - } - - let mut info = blocks[block_idx.idx()].instructions[i]; - info.target = BlockIdx::NULL; - let except_handler = info.except_handler.take(); - let entry = instruction_sequence_addop(instr_sequence, info)?; - let hi = &mut entry.except_handler; - if let Some(handler) = except_handler { - debug_assert!(handler.handler_block != BlockIdx::NULL); - let lbl = blocks[handler.handler_block.idx()].cpython_label; - debug_assert!(is_label(lbl)); - let start_depth = blocks[handler.handler_block.idx()].start_depth; - debug_assert!(start_depth >= 0); - hi.h_label = lbl.0; - hi.start_depth = start_depth; - hi.preserve_lasti = i32::from(handler.preserve_lasti); - } else { - hi.h_label = NO_EXCEPTION_HANDLER_LABEL; - } - } - block_idx = blocks[block_idx.idx()].next; - } - - instruction_sequence_apply_label_map(instr_sequence)?; - Ok(()) -} - -/// assemble.c instr_size -fn instr_size(instr: &InstructionInfo) -> usize { - let opcode = instr.instr.expect_real(); - let oparg = u32::from(instr.arg) as i32; - debug_assert!( - instr.instr.has_arg() || oparg == 0, - "CPython assemble.c instr_size requires OPCODE_HAS_ARG or oparg == 0" - ); - let extended_args = - (0xFF_FFFF < oparg) as usize + (0xFF_FF < oparg) as usize + (0xFF < oparg) as usize; - let caches = opcode.cache_entries(); - extended_args + 1 + caches } /// pycore_opcode_metadata.h is_pseudo_target -fn is_pseudo_target(pseudo: PseudoOpcode, target: Opcode) -> bool { +const fn is_pseudo_target(pseudo: PseudoOpcode, target: Opcode) -> bool { match pseudo { PseudoOpcode::LoadClosure => matches!(target, Opcode::LoadFast), PseudoOpcode::StoreFastMaybeNull => matches!(target, Opcode::StoreFast), @@ -863,10 +865,7 @@ fn is_pseudo_target(pseudo: PseudoOpcode, target: Opcode) -> bool { } } /// assemble.c resolve_unconditional_jumps -#[allow(clippy::unnecessary_wraps)] -fn resolve_unconditional_jumps( - instr_sequence: &mut InstructionSequence, -) -> crate::InternalResult<()> { +fn resolve_unconditional_jumps(instr_sequence: &mut InstructionSequence) { for i in 0..instr_sequence.instr_used { let instr = &mut instr_sequence.instrs[i].info; let is_forward = (u32::from(instr.arg) as i32) > i as i32; @@ -874,16 +873,11 @@ fn resolve_unconditional_jumps( AnyInstruction::Pseudo(PseudoInstruction::Jump { .. }) => { debug_assert!(is_pseudo_target(PseudoOpcode::Jump, Opcode::JumpForward)); debug_assert!(is_pseudo_target(PseudoOpcode::Jump, Opcode::JumpBackward)); + if is_forward { - instr.instr = Instruction::JumpForward { - delta: Arg::marker(), - } - .into(); + instr.instr = Opcode::JumpForward.into(); } else { - instr.instr = Instruction::JumpBackward { - delta: Arg::marker(), - } - .into(); + instr.instr = Opcode::JumpBackward.into(); } } AnyInstruction::Pseudo(PseudoInstruction::JumpNoInterrupt { .. }) => { @@ -896,15 +890,9 @@ fn resolve_unconditional_jumps( Opcode::JumpBackwardNoInterrupt )); if is_forward { - instr.instr = Instruction::JumpForward { - delta: Arg::marker(), - } - .into(); + instr.instr = Opcode::JumpForward.into(); } else { - instr.instr = Instruction::JumpBackwardNoInterrupt { - delta: Arg::marker(), - } - .into(); + instr.instr = Opcode::JumpBackwardNoInterrupt.into(); } } _ => { @@ -914,12 +902,10 @@ fn resolve_unconditional_jumps( } } } - Ok(()) } /// assemble.c resolve_jump_offsets -#[allow(clippy::needless_range_loop, clippy::unnecessary_wraps)] -fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) -> crate::InternalResult<()> { +fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) { // The offset (in code units) of END_SEND from SEND in the yield-from sequence. const END_SEND_OFFSET: i32 = 5; for i in 0..instr_sequence.instr_used { @@ -929,22 +915,24 @@ fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) -> crate::Inte instr.i_target = u32::from(instr.info.arg) as i32; } } + let mut extended_arg_recompile; loop { let mut totsize = 0i32; for i in 0..instr_sequence.instr_used { let instr = &mut instr_sequence.instrs[i]; instr.i_offset = totsize; - let isize = instr_size(&instr.info); - totsize += isize as i32; + let instr_size = instr.info.instr_size(); + totsize += instr_size as i32; } + extended_arg_recompile = false; let mut offset = 0i32; for i in 0..instr_sequence.instr_used { - let isize = instr_size(&instr_sequence.instrs[i].info); + let i_size = instr_sequence.instrs[i].info.instr_size(); // Jump offsets are computed relative to the instruction pointer // after fetching the jump instruction. - offset += isize as i32; + offset += i_size as i32; let opcode = instr_sequence.instrs[i].info.instr.expect_real(); if opcode.has_jump() { @@ -970,7 +958,7 @@ fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) -> crate::Inte oparg -= offset; } info.arg = OpArg::new(oparg as u32); - if instr_size(info) != isize { + if info.instr_size() != i_size { extended_arg_recompile = true; } } @@ -980,8 +968,6 @@ fn resolve_jump_offsets(instr_sequence: &mut InstructionSequence) -> crate::Inte break; } } - - Ok(()) } struct AssembledCode { @@ -1000,36 +986,6 @@ fn same_location(a: LineTableLocation, b: LineTableLocation) -> bool { a.line == b.line && a.end_line == b.end_line && a.col == b.col && a.end_col == b.end_col } -fn instruction_linetable_location(info: &InstructionInfo) -> LineTableLocation { - match info.lineno_override { - Some(NO_LOCATION_OVERRIDE) => LineTableLocation { - line: NO_LOCATION_OVERRIDE, - end_line: NO_LOCATION_OVERRIDE, - col: NO_LOCATION_OVERRIDE, - end_col: NO_LOCATION_OVERRIDE, - }, - Some(LINE_ONLY_LOCATION_OVERRIDE) => LineTableLocation { - line: info.location.line.get() as i32, - end_line: info.end_location.line.get() as i32, - col: -1, - end_col: -1, - }, - Some(NEXT_LOCATION_OVERRIDE) => next_linetable_location(), - Some(lineno) => LineTableLocation { - line: lineno, - end_line: info.end_location.line.get() as i32, - col: info.location.character_offset.to_zero_indexed() as i32, - end_col: info.end_location.character_offset.to_zero_indexed() as i32, - }, - None => LineTableLocation { - line: info.location.line.get() as i32, - end_line: info.end_location.line.get() as i32, - col: info.location.character_offset.to_zero_indexed() as i32, - end_col: info.end_location.character_offset.to_zero_indexed() as i32, - }, - } -} - /// assemble.c write_instr fn write_instr(instructions: &mut Vec, info: &InstructionInfo, ilen: usize) { let opcode = info.instr.expect_real(); @@ -1073,7 +1029,7 @@ fn assemble_emit_instr( instructions: &mut Vec, info: &mut InstructionInfo, ) -> crate::InternalResult<()> { - let size = instr_size(info); + let size = info.instr_size(); let required = instructions .len() .checked_add(size) @@ -1086,14 +1042,15 @@ fn assemble_emit_instr( } /// assemble.c assemble_location_info -#[allow(clippy::needless_range_loop)] fn assemble_location_info( instr_sequence: &mut InstructionSequence, first_line: i32, debug_ranges: bool, ) -> crate::InternalResult> { for i in (0..instr_sequence.instr_used).rev() { - let loc = instruction_linetable_location(&instr_sequence.instrs[i].info); + let loc = instr_sequence.instrs[i] + .info + .instruction_linetable_location(); if same_location(loc, next_linetable_location()) { if instr_sequence.instrs[i] .info @@ -1105,8 +1062,7 @@ fn assemble_location_info( } else { debug_assert!(i < instr_sequence.instr_used - 1); let next = instr_sequence.instrs[i + 1].info; - instr_set_loc( - &mut instr_sequence.instrs[i].info, + instr_sequence.instrs[i].info.instr_set_loc( next.location, next.end_location, next.lineno_override, @@ -1122,13 +1078,13 @@ fn assemble_location_info( let mut size = 0; for i in 0..instr_sequence.instr_used { let entry = &instr_sequence.instrs[i]; - let instr_loc = instruction_linetable_location(&entry.info); + let instr_loc = entry.info.instruction_linetable_location(); if !same_location(loc, instr_loc) { assemble_emit_location(&mut linetable, loc, size, &mut prev_line, debug_ranges)?; loc = instr_loc; size = 0; } - size += instr_size(&entry.info); + size += entry.info.instr_size(); } assemble_emit_location(&mut linetable, loc, size, &mut prev_line, debug_ranges)?; Ok(linetable.into_boxed_slice()) @@ -1319,4567 +1275,4835 @@ impl Block { &self.instructions[..self.instruction_used] } - pub(crate) fn is_empty(&self) -> bool { + #[must_use] + pub(crate) const fn is_empty(&self) -> bool { self.instruction_used == 0 } -} - -pub(crate) const START_DEPTH_UNSET: i32 = i32::MIN; -const CO_MAXBLOCKS: usize = 20; - -/// flowgraph.c struct _PyCfgExceptStack -#[derive(Clone, Debug)] -struct CfgExceptStack { - handlers: [BlockIdx; CO_MAXBLOCKS + 2], - depth: usize, -} - -/// flowgraph.c `basicblock **stack` -#[derive(Clone, Debug)] -struct CfgTraversalStack { - stack: Vec, - sp: usize, -} -impl CfgTraversalStack { - fn push(&mut self, block: BlockIdx) { - debug_assert!(self.sp < self.stack.len()); - self.stack[self.sp] = block; - self.sp += 1; + /// flowgraph.c basicblock_next_instr + fn basicblock_next_instr(&mut self) -> crate::InternalResult { + let off = self.instruction_used; + let new_allocation = c_array_ensure_capacity::( + self.instruction_allocation, + off + 1, + DEFAULT_BLOCK_SIZE, + )?; + if new_allocation > self.instruction_allocation { + if new_allocation > self.instructions.len() { + self.instructions + .try_reserve_exact(new_allocation - self.instructions.len()) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + self.instructions + .resize_with(new_allocation, InstructionInfo::empty); + } + self.instruction_allocation = new_allocation; + } + debug_assert!(self.instruction_allocation > off); + self.instruction_used += 1; + Ok(off) } - fn pop(&mut self) -> Option { - if self.sp == 0 { - return None; + /// flowgraph.c basicblock_last_instr + fn basicblock_last_instr(&self) -> Option<&InstructionInfo> { + debug_assert!(self.instruction_allocation >= self.instruction_used); + if self.instruction_used > 0 { + debug_assert!(!self.instructions.is_empty()); + Some(&self.instructions[self.instruction_used - 1]) + } else { + None } - self.sp -= 1; - Some(self.stack[self.sp]) } - fn capacity(&self) -> usize { - self.stack.len() + /// flowgraph.c basicblock_last_instr + fn basicblock_last_instr_mut(&mut self) -> Option<&mut InstructionInfo> { + debug_assert!(self.instruction_allocation >= self.instruction_used); + if self.instruction_used > 0 { + debug_assert!(!self.instructions.is_empty()); + Some(&mut self.instructions[self.instruction_used - 1]) + } else { + None + } } -} -#[derive(Clone, Debug)] -pub(crate) struct InstructionSequenceLabelMap { - block_labels: Vec, - /// Codegen-side shadow of CPython's instruction-sequence label map. - /// - /// `_PyInstructionSequence_UseLabel()` can map multiple labels to the same - /// instruction offset before `_PyCfg_FromInstructionSequence()` materializes - /// CFG blocks. The codegen CFG path keeps the same aliasing by resolving - /// those labels to the block that owns the shared offset. - cpython_block_by_label: Vec, -} + /// flowgraph.c basicblock_addop + fn basicblock_addop(&mut self, mut info: InstructionInfo) -> crate::InternalResult<()> { + let opcode = AnyOpcode::from(info.instr); + debug_assert!(is_within_opcode_range(opcode)); + debug_assert!(!info.instr.is_assembler()); + debug_assert!( + info.instr.has_arg() || info.instr.has_target() || u32::from(info.arg) == 0, + "CPython basicblock_addop requires OPCODE_HAS_ARG, HAS_TARGET, or oparg == 0" + ); + debug_assert!( + u32::from(info.arg) < (1 << 30), + "CPython basicblock_addop requires 0 <= oparg < (1 << 30)" + ); + let off = self.basicblock_next_instr()?; + let except_handler = self.instructions[off].except_handler; + info.target = BlockIdx::NULL; + info.except_handler = except_handler; + self.instructions[off] = info; + Ok(()) + } -fn instruction_sequence_label_map_register_label( - map: &mut InstructionSequenceLabelMap, - label: InstructionSequenceLabel, -) -> crate::InternalResult<()> { - debug_assert!(is_label(label)); - let old_size = map.cpython_block_by_label.len(); - let new_allocation = c_array_ensure_capacity::( - old_size, - label.idx(), - INITIAL_INSTR_SEQUENCE_LABELS_MAP_SIZE, - )?; - if new_allocation > old_size { - if new_allocation > map.cpython_block_by_label.capacity() { - map.cpython_block_by_label - .try_reserve_exact(new_allocation - map.cpython_block_by_label.capacity()) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - } - map.cpython_block_by_label - .resize(new_allocation, BlockIdx::NULL); - for i in old_size..map.cpython_block_by_label.len() { - map.cpython_block_by_label[i] = BlockIdx::NULL; + /// flowgraph.c basicblock_insert_instruction + fn basicblock_insert_instruction( + &mut self, + pos: usize, + info: InstructionInfo, + ) -> crate::InternalResult<()> { + let old_len = self.instruction_used; + debug_assert!(pos <= old_len); + self.basicblock_next_instr()?; + for i in (pos + 1..=old_len).rev() { + self.instructions[i] = self.instructions[i - 1]; } + self.instructions[pos] = info; + Ok(()) } - debug_assert!(map.cpython_block_by_label.len() > label.idx()); - Ok(()) -} -fn instruction_sequence_label_map_ensure_label_for_block( - map: &mut InstructionSequenceLabelMap, - seq: &mut InstructionSequence, - block: BlockIdx, -) -> crate::InternalResult { - debug_assert_ne!(block, BlockIdx::NULL); - let block_label = map.block_labels[block.idx()]; - if is_label(block_label) { - return Ok(block_label); + /// flowgraph.c direct `b_iused = 0` + fn basicblock_clear(&mut self) { + self.instruction_used = 0; } - let label = instruction_sequence_new_label(seq); - debug_assert_eq!(label.0, seq.next_free_label); - instruction_sequence_label_map_register_label(map, label)?; - map.cpython_block_by_label[label.idx()] = block; - map.block_labels[block.idx()] = label; - Ok(label) -} - -fn instruction_sequence_label_map_label_for_block( - map: &InstructionSequenceLabelMap, - block: BlockIdx, -) -> InstructionSequenceLabel { - debug_assert_ne!(block, BlockIdx::NULL); - map.block_labels - .get(block.idx()) - .copied() - .unwrap_or(InstructionSequenceLabel::NO_LABEL) -} -fn instruction_sequence_label_map_block_for_label( - map: &InstructionSequenceLabelMap, - label: InstructionSequenceLabel, -) -> Option { - if !is_label(label) { - return None; + /// CPython direct `b_instr[0]` access. Some passes set `b_iused = 0` + /// without clearing the backing array, so an empty basic block can still have + /// a first raw instruction slot. + fn basicblock_raw_first_instr_mut(&mut self) -> &mut InstructionInfo { + debug_assert!(self.instruction_allocation > 0); + &mut self.instructions[0] } - map.cpython_block_by_label - .get(label.idx()) - .copied() - .filter(|&block| block != BlockIdx::NULL) -} -fn instruction_sequence_label_map_resolve_label( - map: &InstructionSequenceLabelMap, - block: BlockIdx, -) -> BlockIdx { - if block == BlockIdx::NULL { - return BlockIdx::NULL; + /// flowgraph.c BB_NO_FALLTHROUGH + fn bb_no_fallthrough(&self) -> bool { + self.basicblock_nofallthrough() } - let label = instruction_sequence_label_map_label_for_block(map, block); - if !is_label(label) { - return block; + + /// flowgraph.c BB_HAS_FALLTHROUGH + fn bb_has_fallthrough(&self) -> bool { + !self.bb_no_fallthrough() } - instruction_sequence_label_map_block_for_label(map, label).unwrap_or_else(|| { - debug_assert!( - false, - "CPython instruction-sequence label must map to a codegen CFG block" - ); - BlockIdx::NULL - }) -} -fn instruction_sequence_label_map_resolve_label_to_block( - map: &InstructionSequenceLabelMap, - label: InstructionSequenceLabel, -) -> BlockIdx { - if !is_label(label) { - return BlockIdx::NULL; + /// flowgraph.c basicblock_returns + #[cfg(test)] + fn basicblock_returns(&self) -> bool { + let last = self.basicblock_last_instr(); + if let Some(last) = last { + matches!(last.instr.real(), Some(Instruction::ReturnValue)) + } else { + false + } } - instruction_sequence_label_map_block_for_label(map, label).unwrap_or_else(|| { - debug_assert!( - false, - "CPython instruction-sequence label must map to a codegen CFG block" - ); - BlockIdx::NULL - }) -} -fn instruction_sequence_label_oparg(label: InstructionSequenceLabel) -> OpArg { - debug_assert!(is_label(label)); - OpArg::new(label.idx() as u32) -} + /// flowgraph.c basicblock_exits_scope + fn basicblock_exits_scope(&self) -> bool { + let last = self.basicblock_last_instr(); + last.is_some_and(|last| last.instr.is_scope_exit()) + } -fn instruction_sequence_label_map_use_label_at_block( - map: &mut InstructionSequenceLabelMap, - seq: &mut InstructionSequence, - from: BlockIdx, - to: BlockIdx, -) -> crate::InternalResult<()> { - if from == BlockIdx::NULL || from == to { - return Ok(()); + /// flowgraph.c is_exit_or_eval_check_without_lineno + fn is_exit_or_eval_check_without_lineno(&self) -> bool { + if self.basicblock_exits_scope() || self.basicblock_has_eval_break() { + self.basicblock_has_no_lineno() + } else { + false + } } - let from_label = instruction_sequence_label_map_ensure_label_for_block(map, seq, from)?; - debug_assert!(map.cpython_block_by_label.len() > from_label.idx()); - let to_block = instruction_sequence_label_map_resolve_label(map, to); - if to_block == BlockIdx::NULL { - debug_assert!( - false, - "CPython label target must map to a codegen CFG block" - ); - return Ok(()); + + /// flowgraph.c basicblock_has_eval_break + fn basicblock_has_eval_break(&self) -> bool { + let mut i = 0; + while i < self.instruction_used { + if self.instructions[i].instr.has_eval_break() { + return true; + } + i += 1; + } + false } - map.cpython_block_by_label[from_label.idx()] = to_block; - Ok(()) -} -fn instruction_sequence_label_map_push_unlabeled_block( - map: &mut InstructionSequenceLabelMap, -) -> crate::InternalResult<()> { - map.block_labels - .try_reserve(1) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - map.block_labels.push(InstructionSequenceLabel::NO_LABEL); - Ok(()) -} + /// flowgraph.c basicblock_has_no_lineno + fn basicblock_has_no_lineno(&self) -> bool { + let mut i = 0; + while i < self.instruction_used { + if self.instructions[i].instruction_lineno() >= 0 { + return false; + } + i += 1; + } + true + } -fn instruction_sequence_label_map_push_unmapped_label( - map: &mut InstructionSequenceLabelMap, - seq: &mut InstructionSequence, -) -> crate::InternalResult<()> { - let label = instruction_sequence_new_label(seq); - debug_assert_eq!(label.0, seq.next_free_label); - instruction_sequence_label_map_register_label(map, label)?; - let block = BlockIdx( - map.block_labels - .len() - .to_u32() - .ok_or(InternalError::MalformedControlFlowGraph)?, - ); - map.cpython_block_by_label[label.idx()] = block; - map.block_labels - .try_reserve(1) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - map.block_labels.push(label); - Ok(()) -} + /// flowgraph.c basicblock_nofallthrough + fn basicblock_nofallthrough(&self) -> bool { + let last = self.basicblock_last_instr(); + last.is_some_and(|last| last.instr.is_scope_exit() || last.instr.is_unconditional_jump()) + } -impl InstructionSequenceLabelMap { - pub(crate) fn new() -> Self { - Self { - block_labels: vec![InstructionSequenceLabel::NO_LABEL], - cpython_block_by_label: Vec::new(), + /// flowgraph.c nop_out + fn nop_out(&mut self, instrs: &[usize]) { + for &i in instrs { + self.instructions[i].nop_out_no_location(); } } -} -pub struct CodeInfo { - pub flags: CodeFlags, - pub source_path: String, - pub private: Option, // For private name mangling, mostly for class + /// flowgraph.c get_const_loading_instrs + fn get_const_loading_instrs( + &self, + mut start: usize, + size: usize, + ) -> crate::InternalResult>> { + let mut indices = Vec::new(); + indices + .try_reserve_exact(size) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + loop { + if start >= self.instruction_used { + return Ok(None); + } - pub blocks: Vec, - pub current_block: BlockIdx, - pub(crate) instr_sequence: InstructionSequence, - pub(crate) instr_sequence_label_map: InstructionSequenceLabelMap, - pub(crate) annotations_instr_sequence: Option, + let instr = &self.instructions[start]; + if !matches!(instr.instr.real(), Some(Instruction::Nop)) { + if !instr.loads_const() { + return Ok(None); + } - pub metadata: CodeUnitMetadata, + indices.push(start); + if indices.len() == size { + break; + } + } - // For class scopes: attributes accessed via self.X - pub static_attributes: Option>, + let Some(prev) = start.checked_sub(1) else { + return Ok(None); + }; - // True if compiling an inlined comprehension - pub in_inlined_comp: bool, + start = prev; + } - // Block stack for tracking nested control structures - pub fblock: Vec, + indices.reverse(); + Ok(Some(indices)) + } - // Reference to the symbol table for this scope - pub symbol_table_index: usize, - // CPython compile.c uses PyList_GET_SIZE(u->u_ste->ste_varnames) - // when calling flowgraph.c _PyCfg_OptimizeCodeUnit(). - pub nparams: usize, + /// flowgraph.c next_swappable_instruction + fn next_swappable_instruction(&self, mut i: usize, lineno: i32) -> Option { + loop { + i += 1; + if i >= self.instruction_used { + return None; + } - // PEP 649: Track nesting depth inside conditional blocks (if/for/while/etc.) - // u_in_conditional_block - pub in_conditional_block: u32, + let info = &self.instructions[i]; + let info_lineno = info.instruction_lineno(); - // PEP 649: Next index for conditional annotation tracking - // u_next_conditional_annotation_index - pub next_conditional_annotation_index: u32, -} + if lineno >= 0 && info_lineno != lineno { + return None; + } -impl CodeInfo { - pub(crate) fn addop_to_instr_sequence( - &mut self, - mut info: InstructionInfo, - ) -> crate::InternalResult<()> { - if info.instr.has_target() && info.target != BlockIdx::NULL { - let label = instruction_sequence_label_map_ensure_label_for_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - info.target, - )?; - info.arg = instruction_sequence_label_oparg(label); - info.target = BlockIdx::NULL; + if matches!(info.instr, AnyInstruction::Real(Instruction::Nop)) { + continue; + } + + if is_swappable(info.instr) { + return Some(i); + } + + return None; } - instruction_sequence_addop(&mut self.instr_sequence, info)?; - Ok(()) } - pub(crate) fn addop_to_instr_sequence_with_target_label( - &mut self, - mut info: InstructionInfo, - target_label: InstructionSequenceLabel, - ) -> crate::InternalResult<()> { - if !info.instr.has_target() { - return Err(InternalError::MalformedControlFlowGraph); + /// flowgraph.c swaptimize + fn swaptimize(&mut self, ix: &mut usize) -> crate::InternalResult<()> { + debug_assert!(matches!( + self.instructions[*ix].instr.real_opcode(), + Some(Opcode::Swap) + )); + let mut depth = u32::from(self.instructions[*ix].arg) as usize; + let mut len = 1usize; + let mut more = false; + let limit = self.instruction_used - *ix; + while len < limit { + match self.instructions[*ix + len].instr.real_opcode() { + Some(Opcode::Swap) => { + depth = depth.max(u32::from(self.instructions[*ix + len].arg) as usize); + more = true; + len += 1; + } + Some(Opcode::Nop) => { + len += 1; + } + _ => break, + } } - info.arg = instruction_sequence_label_oparg(target_label); - info.target = BlockIdx::NULL; - instruction_sequence_addop(&mut self.instr_sequence, info)?; + + if !more { + return Ok(()); + } + + let mut stack = Vec::new(); + stack + .try_reserve_exact(depth) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + stack.resize(depth, 0); + let mut i = 0; + while i < depth { + stack[i] = i as i32; + i += 1; + } + + i = 0; + while i < len { + let info = &self.instructions[*ix + i]; + if matches!(info.instr.real_opcode(), Some(Opcode::Swap)) { + let oparg = u32::from(info.arg) as usize; + stack.swap(0, oparg - 1); + } + i += 1; + } + + let mut current = len as isize - 1; + for i in 0..depth { + if stack[i] == VISITED || stack[i] == i as i32 { + continue; + } + let mut j = i; + loop { + if j != 0 { + debug_assert!(current >= 0); + let out = &mut self.instructions[*ix + current as usize]; + out.instr = Opcode::Swap.into(); + out.arg = OpArg::new((j + 1) as u32); + current -= 1; + } + if stack[j] == VISITED { + debug_assert_eq!(j, i); + break; + } + let next_j = stack[j] as usize; + stack[j] = VISITED; + j = next_j; + } + } + + while current >= 0 { + self.instructions[*ix + current as usize].set_to_nop(); + current -= 1; + } + *ix += len - 1; Ok(()) } - pub(crate) fn addop_to_current_block( - &mut self, - info: InstructionInfo, - ) -> crate::InternalResult<()> { - basicblock_addop(&mut self.blocks[self.current_block.idx()], info) - } + /// flowgraph.c apply_static_swaps + fn apply_static_swaps(&mut self, mut i: isize) { + while i >= 0 { + let idx = i as usize; + debug_assert!(idx < self.instruction_used); + let swap_arg = match self.instructions[idx].instr.real_opcode() { + Some(Opcode::Swap) => u32::from(self.instructions[idx].arg), + Some(Opcode::Nop | Opcode::PopTop | Opcode::StoreFast) => { + i -= 1; + continue; + } + _ if matches!( + self.instructions[idx].instr.pseudo_opcode(), + Some(PseudoOpcode::StoreFastMaybeNull) + ) => + { + i -= 1; + continue; + } + _ => return, + }; - pub(crate) fn last_current_block_instr_mut(&mut self) -> Option<&mut InstructionInfo> { - basicblock_last_instr_mut(&mut self.blocks[self.current_block.idx()]) - } + let Some(j) = self.next_swappable_instruction(idx, -1) else { + return; + }; + let lineno = self.instructions[j].instruction_lineno(); + let mut k = j; + for _ in 1..swap_arg { + let Some(next) = self.next_swappable_instruction(k, lineno) else { + return; + }; + k = next; + } - pub(crate) fn set_last_instr_sequence_lineno_override(&mut self, lineno_override: i32) { - if let Some(last) = instruction_sequence_last_info_mut(&mut self.instr_sequence) { - last.lineno_override = Some(lineno_override); + let store_j = self.instructions[j].stores_to(); + let store_k = self.instructions[k].stores_to(); + if store_j >= 0 || store_k >= 0 { + if store_j == store_k { + return; + } + let mut idx = j + 1; + while idx < k { + let store_idx = self.instructions[idx].stores_to(); + if store_idx >= 0 && (store_idx == store_j || store_idx == store_k) { + return; + } + idx += 1; + } + } + + self.instructions[idx].set_to_nop(); + self.instructions.swap(j, k); + i -= 1; } } - pub(crate) fn use_instr_sequence_label( - &mut self, - block: BlockIdx, - ) -> crate::InternalResult<()> { - let label = instruction_sequence_label_map_ensure_label_for_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - block, - )?; - instruction_sequence_use_label(&mut self.instr_sequence, label) + /// flowgraph.c optimize_basic_block swap pass + fn apply_static_swaps_block(&mut self) -> crate::InternalResult<()> { + let mut i = 0; + while i < self.instruction_used { + if matches!(self.instructions[i].instr.real_opcode(), Some(Opcode::Swap)) { + self.swaptimize(&mut i)?; + self.apply_static_swaps(i as isize); + } + i += 1; + } + Ok(()) } +} - pub(crate) fn new_instr_sequence_label(&mut self) -> InstructionSequenceLabel { - instruction_sequence_new_label(&mut self.instr_sequence) - } +#[derive(Clone, Debug, Default)] +pub struct Blocks(Vec); - pub(crate) fn use_raw_instr_sequence_label( +// Vec like methods +impl Blocks { + pub fn try_reserve( &mut self, - label: InstructionSequenceLabel, - ) -> crate::InternalResult<()> { - instruction_sequence_use_label(&mut self.instr_sequence, label) + additional: usize, + ) -> Result<(), alloc::collections::TryReserveError> { + self.0.try_reserve(additional) } - pub(crate) fn mark_cpython_cfg_label(&mut self, block: BlockIdx) -> crate::InternalResult<()> { - let label = instruction_sequence_label_map_ensure_label_for_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - block, - )?; - self.blocks[block.idx()].cpython_label = label; - Ok(()) + pub fn push(&mut self, value: Block) { + self.0.push(value) } +} - pub(crate) fn resolve_instr_sequence_label(&self, block: BlockIdx) -> BlockIdx { - instruction_sequence_label_map_resolve_label(&self.instr_sequence_label_map, block) - } +// CPython functions - pub(crate) fn block_for_instr_sequence_label( - &self, - label: InstructionSequenceLabel, - ) -> BlockIdx { - instruction_sequence_label_map_resolve_label_to_block(&self.instr_sequence_label_map, label) +impl Blocks { + /// # See also + /// [CPython's remove_unreachable](https://github.com/python/cpython/blob/v3.14.6/Python/flowgraph.c#L995-L1041) + pub fn remove_unreachable(&mut self) -> crate::InternalResult<()> { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + self[block_idx].predecessors = 0; + block_idx = self[block_idx].next; + } + + let mut stack = self.make_cfg_traversal_stack()?; + self[0].predecessors = 1; + stack.push(BlockIdx(0)); + self[0].visited = true; + while let Some(current) = stack.pop() { + let idx = current.idx(); + let next = self[idx].next; + if next != BlockIdx::NULL && self[idx].bb_has_fallthrough() { + if !self[next].visited { + debug_assert_eq!(self[next].predecessors, 0); + stack.push(next); + self[next].visited = true; + } + self[next].predecessors += 1; + } + + let instr_count = self[idx].instruction_used; + for i in 0..instr_count { + let instr = self[idx].instructions[i]; + if instr.is_jump() || instr.is_block_push() { + let target = instr.target; + debug_assert!(target != BlockIdx::NULL); + let target_idx = target.idx(); + if !self[target_idx].visited { + stack.push(target); + self[target_idx].visited = true; + } + self[target_idx].predecessors += 1; + } + } + } + + block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next = self[block_idx].next; + if self[block_idx].predecessors == 0 { + let block = &mut self[block_idx]; + block.basicblock_clear(); + block.except_handler = false; + } + block_idx = next; + } + Ok(()) } - pub(crate) fn use_instr_sequence_label_at_block( + /// flowgraph.c basicblock_append_instructions + fn basicblock_append_block_instructions( &mut self, - from: BlockIdx, to: BlockIdx, + from: BlockIdx, ) -> crate::InternalResult<()> { - instruction_sequence_label_map_use_label_at_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - from, - to, - ) - } + debug_assert_ne!(to, from); - pub(crate) fn instr_sequence_label_for_block( - &mut self, - block: BlockIdx, - ) -> crate::InternalResult { - if block == BlockIdx::NULL { - Ok(InstructionSequenceLabel::NO_LABEL) - } else { - instruction_sequence_label_map_ensure_label_for_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - block, - ) + let from_len = self[from].instruction_used; + for i in 0..from_len { + let info = self[from].instructions[i]; + let off = self[to].basicblock_next_instr()?; + self[to].instructions[off] = info; } - } - pub(crate) fn insert_start_setup_cleanup( - &mut self, - handler_block: BlockIdx, - ) -> crate::InternalResult<()> { - let handler_label = instruction_sequence_label_map_ensure_label_for_block( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - handler_block, - )?; - instruction_sequence_insert_instruction( - &mut self.instr_sequence, - 0, - InstructionInfo { - instr: PseudoInstruction::SetupCleanup { - delta: Arg::marker(), - } - .into(), - arg: instruction_sequence_label_oparg(handler_label), - target: BlockIdx::NULL, - location: SourceLocation::default(), - end_location: SourceLocation::default(), - except_handler: None, - lineno_override: Some(NO_LOCATION_OVERRIDE), - }, - ) + Ok(()) } - pub(crate) fn push_unmapped_instr_sequence_label(&mut self) -> crate::InternalResult<()> { - instruction_sequence_label_map_push_unmapped_label( - &mut self.instr_sequence_label_map, - &mut self.instr_sequence, - ) - } + /// flowgraph.c copy_basicblock + fn copy_basicblock(&mut self, block_idx: BlockIdx) -> crate::InternalResult { + debug_assert!(self[block_idx].bb_no_fallthrough()); - pub(crate) fn push_unlabeled_instr_sequence_block(&mut self) -> crate::InternalResult<()> { - instruction_sequence_label_map_push_unlabeled_block(&mut self.instr_sequence_label_map) + let result = self.blocks_new_block()?; + self.basicblock_append_block_instructions(result, block_idx)?; + Ok(result) } - fn take_recorded_instr_sequence(&mut self) -> crate::InternalResult { - let mut instr_sequence = - core::mem::replace(&mut self.instr_sequence, instruction_sequence_new()); - if let Some(mut annotations_instr_sequence) = self.annotations_instr_sequence.take() { - instruction_sequence_apply_label_map(&mut annotations_instr_sequence)?; - instruction_sequence_set_annotations_code( - &mut instr_sequence, - Some(Box::new(annotations_instr_sequence)), - ); + fn duplicate_exits_without_lineno(&mut self) -> crate::InternalResult<()> { + let mut next_lbl = get_max_label(self) + 1; + + let entryblock = BlockIdx(0); + let mut b = entryblock; + while b != BlockIdx::NULL { + let Some(last) = self[b].basicblock_last_instr().copied() else { + b = self[b].next; + continue; + }; + + if last.is_jump() { + debug_assert!(last.target != BlockIdx::NULL); + + let target = next_nonempty_block(self, last.target); + + debug_assert!(target != BlockIdx::NULL); + + if self[target].is_exit_or_eval_check_without_lineno() + && self[target].predecessors > 1 + { + let new_target = self.copy_basicblock(target)?; + self[new_target].instructions[0].instr_set_location(last.instr_location()); + let last_mut = self[b].basicblock_last_instr_mut().unwrap(); + last_mut.target = new_target; + self[target].predecessors -= 1; + self[new_target].predecessors = 1; + self[new_target].next = self[target].next; + self[new_target].cpython_label = InstructionSequenceLabel(next_lbl); + next_lbl += 1; + self[target].next = new_target; + } + } + b = self[b].next; + } + + b = entryblock; + while b != BlockIdx::NULL { + let next = self[b].next; + if self[b].bb_has_fallthrough() + && next != BlockIdx::NULL + && self[b].instruction_used != 0 + && self[next].is_exit_or_eval_check_without_lineno() + { + let last = *self[b] + .basicblock_last_instr() + .expect("block has instructions"); + self[next].instructions[0].instr_set_location(last.instr_location()); + } + b = self[b].next; } - Ok(instr_sequence) + + Ok(()) } - fn prepare_cfg_from_codegen(&mut self) -> crate::InternalResult { - // CPython compile.c optimize_and_assemble_code_unit passes - // u_instr_sequence directly into flowgraph.c _PyCfg_FromInstructionSequence(). - self.take_recorded_instr_sequence() + fn resolve_line_numbers(&mut self, _firstlineno: OneIndexed) -> crate::InternalResult<()> { + self.duplicate_exits_without_lineno()?; + self.propagate_line_numbers(); + Ok(()) } -} -fn optimize_code_unit( - metadata: &mut CodeUnitMetadata, - blocks: &mut Vec, - instr_sequence: InstructionSequence, - nlocals: usize, - nparams: usize, -) -> crate::InternalResult<()> { - // Phase 1: _PyCfg_OptimizeCodeUnit (flowgraph.c) - *blocks = cfg_from_instruction_sequence(instr_sequence)?; - translate_jump_labels_to_targets(blocks)?; - mark_except_handlers(blocks)?; - label_exception_targets(blocks)?; - optimize_cfg(metadata, blocks, metadata.firstlineno)?; - remove_unused_consts(blocks, &mut metadata.consts)?; - add_checks_for_loads_of_uninitialized_variables(blocks, nlocals, nparams)?; - // CPython inserts superinstructions in _PyCfg_OptimizeCodeUnit, before - // later jump normalization / block reordering can create adjacencies - // that never exist at this stage in flowgraph.c. - insert_superinstructions(blocks)?; - push_cold_blocks_to_end(blocks)?; - // CPython resolves line numbers again after cold-block extraction. - resolve_line_numbers(blocks, metadata.firstlineno)?; - Ok(()) -} - -fn optimize_cfg( - metadata: &mut CodeUnitMetadata, - blocks: &mut Vec, - firstlineno: OneIndexed, -) -> crate::InternalResult<()> { - // flowgraph.c optimize_cfg - // CPython optimize_cfg() starts with check_cfg() and raises - // SystemError if a jump or scope exit is not the last instruction in - // its block. - check_cfg(blocks)?; - inline_small_or_no_lineno_blocks(blocks)?; - // CPython does not re-run instruction-sequence label-map/CFG conversion - // after this point. Unreferenced label blocks left by jump inlining - // remain block boundaries and can preserve line-marker NOPs. - remove_unreachable(blocks)?; - // CPython optimize_cfg resolves line numbers before local checks and - // superinstruction insertion, so fusion decisions see propagated - // source locations. - resolve_line_numbers(blocks, firstlineno)?; - // CPython optimize_cfg() runs optimize_load_const() and then - // optimize_basic_block() after line numbers are resolved. - optimize_load_const(metadata, blocks)?; - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next_block = blocks[block_idx.idx()].next; - optimize_basic_block(blocks, metadata, block_idx)?; - block_idx = next_block; - } - remove_redundant_nops_and_pairs(blocks)?; - // CPython optimize_cfg() removes newly-unreachable blocks and - // redundant NOP/jump chains before _PyCfg_OptimizeCodeUnit() prunes - // unused constants. - remove_unreachable(blocks)?; - remove_redundant_nops_and_jumps(blocks)?; - #[cfg(debug_assertions)] - assert!(no_redundant_jumps(blocks)); - Ok(()) -} - -fn optimized_cfg_to_instruction_sequence( - metadata: &CodeUnitMetadata, - flags: CodeFlags, - blocks: &mut Vec, -) -> crate::InternalResult<(u32, usize, InstructionSequence)> { - // Phase 2: _PyCfg_OptimizedCfgToInstructionSequence (flowgraph.c) - convert_pseudo_conditional_jumps(blocks)?; - let max_stackdepth = calculate_stackdepth(blocks)?; - debug_assert!(!is_generator(flags) || max_stackdepth != 0); - let nlocalsplus = prepare_localsplus(metadata, blocks, flags)?; - // Match CPython order: pseudo ops are lowered after stackdepth and - // localsplus preparation, before normalize_jumps. - convert_pseudo_ops(blocks)?; - normalize_jumps(blocks)?; - #[cfg(debug_assertions)] - assert!(no_redundant_jumps(blocks)); - // optimize_load_fast: after normalize_jumps - optimize_load_fast(blocks)?; - - let mut instr_sequence = instruction_sequence_new(); - cfg_to_instruction_sequence(blocks, &mut instr_sequence)?; - Ok((max_stackdepth, nlocalsplus, instr_sequence)) -} + /// flowgraph.c optimize_basic_block + fn optimize_basic_block( + &mut self, + metadata: &mut CodeUnitMetadata, + block_idx: BlockIdx, + ) -> crate::InternalResult<()> { + let mut nop = InstructionInfo { + instr: Instruction::Nop.into(), + arg: OpArg::NULL, + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: None, + }; + nop.instr_set_op0(Instruction::Nop.into()); + let mut i = 0; + while i < self[block_idx].instruction_used { + let inst = self[block_idx].instructions[i]; + debug_assert!(!inst.instr.is_assembler()); + let target = if inst.instr.has_target() { + let target = inst.target; + debug_assert!(target != BlockIdx::NULL); + debug_assert!(self[target.idx()].instruction_used != 0); + debug_assert!(!self[target.idx()].instructions[0].instr.is_assembler()); + self[target.idx()].instructions[0] + } else { + nop + }; -impl CodeInfo { - #[allow(clippy::needless_range_loop)] - pub fn finalize_code( - mut self, - opts: &crate::compile::CompileOpts, - ) -> crate::InternalResult { - let instr_sequence = self.prepare_cfg_from_codegen()?; - let nlocals = self.metadata.varnames.len(); - let nparams = self.nparams; - optimize_code_unit( - &mut self.metadata, - &mut self.blocks, - instr_sequence, - nlocals, - nparams, - )?; - let (max_stackdepth, nlocalsplus, mut instr_sequence) = - optimized_cfg_to_instruction_sequence(&self.metadata, self.flags, &mut self.blocks)?; - let localsplusinfo = compute_localsplus_info(&self.metadata, nlocalsplus, self.flags)?; + let nextop = self[block_idx] + .instructions + .get(i + 1) + .and_then(|next| next.instr.real()); - let Self { - flags, - source_path, - private: _, // private is only used during compilation + match inst.instr { + AnyInstruction::Real(Instruction::BuildTuple { .. }) => { + let oparg = u32::from(inst.arg); + if matches!(nextop, Some(Instruction::UnpackSequence { .. })) + && u32::from(self[block_idx].instructions[i + 1].arg) == oparg + { + match oparg { + 1 => { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].set_to_nop(); + i += 1; + continue; + } + 2 | 3 => { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].instr = Opcode::Swap.into(); + i += 1; + continue; + } + _ => {} + } + } + fold_tuple_of_constants(metadata, &mut self[block_idx], i)?; + } + AnyInstruction::Real( + Instruction::BuildList { .. } | Instruction::BuildSet { .. }, + ) => { + optimize_lists_and_sets(metadata, &mut self[block_idx], i, nextop)?; + } + AnyInstruction::Real( + Instruction::PopJumpIfNotNone { .. } | Instruction::PopJumpIfNone { .. }, + ) if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) + && self.jump_thread(block_idx, i, &target, inst.instr)? => + { + continue; + } + AnyInstruction::Real(Instruction::PopJumpIfFalse { .. }) + if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) + && self.jump_thread(block_idx, i, &target, inst.instr)? => + { + continue; + } + AnyInstruction::Real(Instruction::PopJumpIfTrue { .. }) + if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) + && self.jump_thread(block_idx, i, &target, inst.instr)? => + { + continue; + } + AnyInstruction::Pseudo( + pseudo @ (PseudoInstruction::JumpIfFalse { .. } + | PseudoInstruction::JumpIfTrue { .. }), + ) => { + let opcode = pseudo.into(); + let opcode_is_false = matches!(pseudo, PseudoInstruction::JumpIfFalse { .. }); + match target.instr.pseudo().map(Into::into) { + Some(PseudoOpcode::Jump) + if self.jump_thread(block_idx, i, &target, opcode)? => + { + continue; + } + Some(PseudoOpcode::JumpIfFalse) + if opcode_is_false + && self.jump_thread(block_idx, i, &target, opcode)? => + { + continue; + } + Some(PseudoOpcode::JumpIfTrue) + if !opcode_is_false + && self.jump_thread(block_idx, i, &target, opcode)? => + { + continue; + } + Some(PseudoOpcode::JumpIfTrue) if opcode_is_false => { + let next = self[inst.target].next; + debug_assert!(next != BlockIdx::NULL); + debug_assert!(next != inst.target); + self[block_idx].instructions[i].target = next; + continue; + } + Some(PseudoOpcode::JumpIfFalse) if !opcode_is_false => { + let next = self[inst.target].next; + debug_assert!(next != BlockIdx::NULL); + debug_assert!(next != inst.target); + self[block_idx].instructions[i].target = next; + continue; + } + _ => {} + } + } + AnyInstruction::Pseudo( + PseudoInstruction::Jump { .. } | PseudoInstruction::JumpNoInterrupt { .. }, + ) => match target.instr.into() { + AnyOpcode::Pseudo(PseudoOpcode::Jump) + if self.jump_thread( + block_idx, + i, + &target, + PseudoOpcode::Jump.into(), + )? => + { + continue; + } + AnyOpcode::Pseudo(PseudoOpcode::JumpNoInterrupt) + if self.jump_thread(block_idx, i, &target, inst.instr)? => + { + continue; + } + _ => {} + }, + // CPython leaves FOR_ITER jump threading disabled. + AnyInstruction::Real(Instruction::ForIter { .. }) => {} + AnyInstruction::Real(Instruction::StoreFast { .. }) + if matches!(nextop, Some(Instruction::StoreFast { .. })) + && u32::from(inst.arg) + == u32::from(self[block_idx].instructions[i + 1].arg) + && self[block_idx].instructions[i].instruction_lineno() + == self[block_idx].instructions[i + 1].instruction_lineno() => + { + self[block_idx].instructions[i].instr = Instruction::PopTop.into(); + self[block_idx].instructions[i].arg = OpArg::NULL; + } + AnyInstruction::Real(Instruction::Swap { .. }) if u32::from(inst.arg) == 1 => { + self[block_idx].instructions[i].set_to_nop(); + } + AnyInstruction::Real(Instruction::LoadGlobal { .. }) + if matches!(nextop, Some(Instruction::PushNull)) + && (u32::from(inst.arg) & 1) == 0 => + { + self[block_idx].instructions[i] + .instr_set_op1(inst.instr, OpArg::new(u32::from(inst.arg) | 1)); + self[block_idx].instructions[i + 1].set_to_nop(); + } + AnyInstruction::Real(Instruction::CompareOp { .. }) + if matches!(nextop, Some(Instruction::ToBool)) => + { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].instr_set_op1( + inst.instr, + OpArg::new(u32::from(inst.arg) | oparg::COMPARE_OP_BOOL_MASK), + ); + i += 1; + continue; + } + AnyInstruction::Real(Instruction::ContainsOp { .. } | Instruction::IsOp { .. }) + if matches!(nextop, Some(Instruction::ToBool)) => + { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].instr_set_op1(inst.instr, inst.arg); + i += 1; + continue; + } + AnyInstruction::Real(Instruction::ContainsOp { .. } | Instruction::IsOp { .. }) + if matches!(nextop, Some(Instruction::UnaryNot)) => + { + self[block_idx].instructions[i].set_to_nop(); + let inverted = u32::from(inst.arg) ^ 1; + debug_assert!(inverted == 0 || inverted == 1); + self[block_idx].instructions[i + 1] + .instr_set_op1(inst.instr, OpArg::new(inverted)); + i += 1; + continue; + } + AnyInstruction::Real(Instruction::ToBool) + if matches!(nextop, Some(Instruction::ToBool)) => + { + self[block_idx].instructions[i].set_to_nop(); + i += 1; + continue; + } + AnyInstruction::Real(Instruction::UnaryNot) => { + if matches!(nextop, Some(Instruction::ToBool)) { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].instr_set_op0(inst.instr); + i += 1; + continue; + } + if matches!(nextop, Some(Instruction::UnaryNot)) { + self[block_idx].instructions[i].set_to_nop(); + self[block_idx].instructions[i + 1].set_to_nop(); + i += 1; + continue; + } + fold_const_unaryop(metadata, &mut self[block_idx], i)?; + } + AnyInstruction::Real(Instruction::UnaryInvert | Instruction::UnaryNegative) => { + fold_const_unaryop(metadata, &mut self[block_idx], i)?; + } + AnyInstruction::Real(Instruction::CallIntrinsic1 { func }) => { + match func.get(inst.arg) { + IntrinsicFunction1::ListToTuple => { + if matches!(nextop, Some(Instruction::GetIter)) { + self[block_idx].instructions[i].set_to_nop(); + } else { + fold_constant_intrinsic_list_to_tuple( + metadata, + &mut self[block_idx], + i, + )?; + } + } + IntrinsicFunction1::UnaryPositive => { + fold_const_unaryop(metadata, &mut self[block_idx], i)?; + } + _ => {} + } + } + AnyInstruction::Real(Instruction::BinaryOp { .. }) => { + fold_const_binop(metadata, &mut self[block_idx], i)?; + } + _ => {} + } - blocks: _, - current_block: _, - instr_sequence: _, - instr_sequence_label_map: _, - annotations_instr_sequence: _, - metadata, - static_attributes: _, - in_inlined_comp: _, - fblock: _, - symbol_table_index: _, - nparams: _, - in_conditional_block: _, - next_conditional_annotation_index: _, - } = self; + i += 1; + } + self[block_idx].apply_static_swaps_block()?; + Ok(()) + } - let CodeUnitMetadata { - name: obj_name, - qualname, - consts: constants, - names: name_cache, - varnames: varname_cache, - cellvars: _, - freevars: freevar_cache, - fast_hidden: _, - fast_hidden_final: _, - argcount: arg_count, - posonlyargcount: posonlyarg_count, - kwonlyargcount: kwonlyarg_count, - firstlineno: first_line_number, - } = metadata; + /// flowgraph.c _PyCfg_ToInstructionSequence + fn cfg_to_instruction_sequence( + &mut self, + instr_sequence: &mut InstructionSequence, + ) -> crate::InternalResult<()> { + let mut label_id = 0; + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + self[block_idx].cpython_label = InstructionSequenceLabel::from_index(label_id); + label_id += 1; + block_idx = self[block_idx].next; + } - resolve_unconditional_jumps(&mut instr_sequence)?; - resolve_jump_offsets(&mut instr_sequence)?; - let assembled = assemble_emit( - &mut instr_sequence, - first_line_number.get() as i32, - opts.debug_ranges, - )?; - let locations = rustpython_compiler_core::marshal::linetable_to_locations( - &assembled.linetable, - first_line_number.get() as i32, - assembled.instructions.len(), - ); + block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let block_label = self[block_idx].cpython_label; + debug_assert!(is_label(block_label)); + instruction_sequence_use_label(instr_sequence, block_label)?; + + let instr_count = self[block_idx].instruction_used; + for i in 0..instr_count { + if self[block_idx].instructions[i].instr.has_target() { + let target_block = self[block_idx].instructions[i].target; + debug_assert!(target_block != BlockIdx::NULL); + let lbl = self[target_block].cpython_label; + debug_assert!(is_label(lbl)); + self[block_idx].instructions[i].arg = OpArg::new(lbl.0 as u32); + } - Ok(CodeObject { - flags, - posonlyarg_count, - arg_count, - kwonlyarg_count, - source_path, - first_line_number: Some(first_line_number), - obj_name: obj_name.clone(), - qualname: qualname.unwrap_or(obj_name), + let mut info = self[block_idx].instructions[i]; + info.target = BlockIdx::NULL; + let except_handler = info.except_handler.take(); + let entry = instruction_sequence_addop(instr_sequence, info)?; + let hi = &mut entry.except_handler; + if let Some(handler) = except_handler { + debug_assert!(handler.handler_block != BlockIdx::NULL); + let lbl = self[handler.handler_block].cpython_label; + debug_assert!(is_label(lbl)); + let start_depth = self[handler.handler_block].start_depth; + debug_assert!(start_depth >= 0); + hi.h_label = lbl.0; + hi.start_depth = start_depth; + hi.preserve_lasti = i32::from(handler.preserve_lasti); + } else { + hi.h_label = NO_EXCEPTION_HANDLER_LABEL; + } + } + block_idx = self[block_idx].next; + } - max_stackdepth, - instructions: CodeUnits::from(assembled.instructions), - locations, - constants: constants.into_iter().collect(), - names: name_cache.into_iter().collect(), - varnames: varname_cache.into_iter().collect(), - cellvars: localsplusinfo.cellvars, - freevars: freevar_cache.into_iter().collect(), - localspluskinds: localsplusinfo.kinds, - linetable: assembled.linetable, - exceptiontable: assembled.exceptiontable, - }) + instruction_sequence_apply_label_map(instr_sequence); + Ok(()) } -} -/// flowgraph.c IS_GENERATOR -fn is_generator(flags: CodeFlags) -> bool { - flags.intersects(CodeFlags::GENERATOR | CodeFlags::COROUTINE | CodeFlags::ASYNC_GENERATOR) -} + fn optimize_load_fast(&mut self) -> crate::InternalResult<()> { + let mut max_instrs = 0; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + max_instrs = max_instrs.max(self[current].instruction_used); + current = self[current].next; + } -/// flowgraph.c insert_prefix_instructions -fn insert_prefix_instructions( - metadata: &CodeUnitMetadata, - blocks: &mut [Block], - cellfixedoffsets: &[i32], - nfreevars: usize, - flags: CodeFlags, -) -> crate::InternalResult<()> { - debug_assert!(!blocks.is_empty()); - let entry = &mut blocks[0]; - let ncellvars = metadata.cellvars.len(); - let firstlineno = metadata.firstlineno; - debug_assert!(firstlineno.get() > 0); - - if is_generator(flags) { - let location = SourceLocation { - line: firstlineno, - character_offset: OneIndexed::MIN, + let mut instr_flags = Vec::new(); + instr_flags + .try_reserve_exact(max_instrs) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + instr_flags.resize(max_instrs, 0u8); + let mut refs = RefStack { + refs: Vec::new(), + size: 0, + capacity: 0, }; - basicblock_insert_instruction( - entry, - 0, - InstructionInfo { - instr: Instruction::ReturnGenerator.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, - location, - end_location: location, - except_handler: None, - lineno_override: Some(LINE_ONLY_LOCATION_OVERRIDE), - }, - )?; - basicblock_insert_instruction( - entry, - 1, - InstructionInfo { - instr: Instruction::PopTop.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, - location, - end_location: location, - except_handler: None, - lineno_override: Some(LINE_ONLY_LOCATION_OVERRIDE), - }, - )?; - } + let mut worklist = self.make_cfg_traversal_stack()?; + worklist.push(BlockIdx(0)); + self[0].start_depth = 0; + self[0].visited = true; + while let Some(block_idx) = worklist.pop() { + let instr_count = self[block_idx].instruction_used; + instr_flags[..instr_count].fill(0); + debug_assert!(self[block_idx].start_depth >= 0); + let start_depth = self[block_idx].start_depth as usize; + ref_stack_clear(&mut refs); + for _ in 0..start_depth { + push_ref(&mut refs, DUMMY_INSTR, NOT_LOCAL)?; + } - if ncellvars > 0 { - let nvars = metadata.varnames.len() + ncellvars; - let mut sorted = Vec::new(); - vec_try_reserve_exact(&mut sorted, nvars)?; - sorted.resize(nvars, 0i32); - for i in 0..ncellvars { - sorted[cellfixedoffsets[i] as usize] = i as i32 + 1; + for i in 0..instr_count { + let info = self[block_idx].instructions[i]; + let instr = info.instr; + let arg_u32 = u32::from(info.arg); + debug_assert!(!matches!(instr.real(), Some(Instruction::ExtendedArg))); + + match instr { + AnyInstruction::Real(Instruction::DeleteFast { var_num }) => { + kill_local( + &mut instr_flags, + &refs, + local_as_ref_local(usize::from(var_num.get(info.arg))), + ); + } + AnyInstruction::Real(Instruction::LoadFast { var_num }) => { + push_ref( + &mut refs, + i as isize, + local_as_ref_local(usize::from(var_num.get(info.arg))), + )?; + } + AnyInstruction::Real(Instruction::LoadFastAndClear { var_num }) => { + let local = local_as_ref_local(usize::from(var_num.get(info.arg))); + kill_local(&mut instr_flags, &refs, local); + push_ref(&mut refs, i as isize, local)?; + } + AnyInstruction::Real(Instruction::LoadFastLoadFast { .. }) => { + let local1 = (arg_u32 >> 4) as isize; + let local2 = (arg_u32 & 15) as isize; + push_ref(&mut refs, i as isize, local1)?; + push_ref(&mut refs, i as isize, local2)?; + } + AnyInstruction::Real(Instruction::StoreFast { var_num }) => { + let r = ref_stack_pop(&mut refs); + store_local( + &mut instr_flags, + &refs, + local_as_ref_local(usize::from(var_num.get(info.arg))), + r, + ); + } + AnyInstruction::Real(Instruction::StoreFastLoadFast { .. }) => { + let r = ref_stack_pop(&mut refs); + store_local(&mut instr_flags, &refs, (arg_u32 >> 4) as isize, r); + push_ref(&mut refs, i as isize, (arg_u32 & 15) as isize)?; + } + AnyInstruction::Real(Instruction::StoreFastStoreFast { .. }) => { + let r1 = ref_stack_pop(&mut refs); + store_local(&mut instr_flags, &refs, (arg_u32 >> 4) as isize, r1); + let r2 = ref_stack_pop(&mut refs); + store_local(&mut instr_flags, &refs, (arg_u32 & 15) as isize, r2); + } + AnyInstruction::Real(Instruction::Copy { i: _ }) => { + let depth = arg_u32 as usize; + assert!(depth > 0); + assert!(refs.size >= depth); + let r = ref_stack_at(&refs, refs.size - depth); + push_ref(&mut refs, r.instr, r.local)?; + } + AnyInstruction::Real(Instruction::Swap { i: _ }) => { + let depth = arg_u32 as usize; + assert!(depth >= 2); + assert!(refs.size >= depth); + ref_stack_swap_top(&mut refs, depth); + } + AnyInstruction::Real( + Instruction::FormatSimple + | Instruction::GetAnext + | Instruction::GetLen + | Instruction::GetYieldFromIter + | Instruction::ImportFrom { .. } + | Instruction::MatchKeys + | Instruction::MatchMapping + | Instruction::MatchSequence + | Instruction::WithExceptStart, + ) => { + let effect = instr.stack_effect_info(arg_u32); + let net_pushed = effect.pushed() as isize - effect.popped() as isize; + debug_assert!(net_pushed >= 0); + // CPython optimize_load_fast() shadows the outer + // instruction index in this produced-value loop. + for produced in 0..net_pushed { + push_ref(&mut refs, produced, NOT_LOCAL)?; + } + } + AnyInstruction::Real( + Instruction::DictMerge { .. } + | Instruction::DictUpdate { .. } + | Instruction::ListAppend { .. } + | Instruction::ListExtend { .. } + | Instruction::MapAdd { .. } + | Instruction::Reraise { .. } + | Instruction::SetAdd { .. } + | Instruction::SetUpdate { .. }, + ) => { + let effect = instr.stack_effect_info(arg_u32); + let net_popped = effect.popped() as isize - effect.pushed() as isize; + debug_assert!(net_popped > 0); + for _ in 0..net_popped { + let _ = ref_stack_pop(&mut refs); + } + } + AnyInstruction::Real( + Instruction::EndSend | Instruction::SetFunctionAttribute { .. }, + ) => { + let effect = instr.stack_effect_info(arg_u32); + debug_assert_eq!(effect.popped(), 2); + debug_assert_eq!(effect.pushed(), 1); + let tos = ref_stack_pop(&mut refs); + let _ = ref_stack_pop(&mut refs); + push_ref(&mut refs, tos.instr, tos.local)?; + } + AnyInstruction::Real(Instruction::CheckExcMatch) => { + let _ = ref_stack_pop(&mut refs); + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + } + AnyInstruction::Real(Instruction::ForIter { .. }) => { + let target = info.target; + debug_assert!(target != BlockIdx::NULL); + load_fast_push_block(&mut worklist, self, target, refs.size + 1); + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + } + AnyInstruction::Real( + Instruction::LoadAttr { .. } | Instruction::LoadSuperAttr { .. }, + ) => { + let self_ref = ref_stack_pop(&mut refs); + if matches!(instr.real(), Some(Instruction::LoadSuperAttr { .. })) { + let _ = ref_stack_pop(&mut refs); + let _ = ref_stack_pop(&mut refs); + } + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + if arg_u32 & 1 != 0 { + push_ref(&mut refs, self_ref.instr, self_ref.local)?; + } + } + AnyInstruction::Real( + Instruction::LoadSpecial { .. } | Instruction::PushExcInfo, + ) => { + let tos = ref_stack_pop(&mut refs); + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + push_ref(&mut refs, tos.instr, tos.local)?; + } + AnyInstruction::Real(Instruction::Send { .. }) => { + let target = info.target; + debug_assert!(target != BlockIdx::NULL); + load_fast_push_block(&mut worklist, self, target, refs.size); + let _ = ref_stack_pop(&mut refs); + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + } + _ => { + let effect = instr.stack_effect_info(arg_u32); + let num_popped = effect.popped() as usize; + let num_pushed = effect.pushed() as usize; + let target = info.target; + if instr.has_target() { + debug_assert!(target != BlockIdx::NULL); + debug_assert!(refs.size >= num_popped); + let target_depth = refs.size - num_popped + num_pushed; + load_fast_push_block(&mut worklist, self, target, target_depth); + } + if !info.is_block_push() { + for _ in 0..num_popped { + let _ = ref_stack_pop(&mut refs); + } + for _ in 0..num_pushed { + push_ref(&mut refs, i as isize, NOT_LOCAL)?; + } + } + } + } + } + + let fallthrough = self[block_idx].next; + let term = self[block_idx].basicblock_last_instr().copied(); + if let Some(term) = term + && fallthrough != BlockIdx::NULL + && !term.instr.is_unconditional_jump() + && !term.instr.is_scope_exit() + { + debug_assert!(self[block_idx].bb_has_fallthrough()); + load_fast_push_block(&mut worklist, self, fallthrough, refs.size); + } + + for i in 0..refs.size { + let r = ref_stack_at(&refs, i); + if r.instr != DUMMY_INSTR { + instr_flags[r.instr as usize] |= LoadFastInstrFlag::RefUnconsumed as u8; + } + } + + let block = &mut self[block_idx]; + let iused = block.instruction_used; + let mut i = 0; + while i < iused { + let info = &mut block.instructions[i]; + if instr_flags[i] != 0 { + i += 1; + continue; + } + + match info.instr.real_opcode() { + Some(Opcode::LoadFast) => { + info.instr = Opcode::LoadFastBorrow.into(); + } + Some(Opcode::LoadFastLoadFast) => { + info.instr = Opcode::LoadFastBorrowLoadFastBorrow.into(); + } + _ => {} + } + i += 1; + } } - let mut ncellsused = 0; - let mut i = 0; - while ncellsused < ncellvars { - let oldindex = sorted[i] - 1; - i += 1; - if oldindex == -1 { + + Ok(()) + } + + fn propagate_line_numbers(&mut self) { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let Some(last) = self[current].basicblock_last_instr().copied() else { + current = self[current].next; continue; + }; + + let mut prev_location = no_instruction_location(); + for i in 0..self[current].instruction_used { + if self[current].instructions[i].instruction_is_no_location() { + self[current].instructions[i].instr_set_location(prev_location); + } else { + prev_location = self[current].instructions[i].instr_location(); + } } - basicblock_insert_instruction( - entry, - ncellsused, - InstructionInfo { - instr: Instruction::MakeCell { i: Arg::marker() }.into(), - arg: OpArg::new(oldindex as u32), - target: BlockIdx::NULL, - location: SourceLocation::default(), - end_location: SourceLocation::default(), - except_handler: None, - lineno_override: Some(NO_LOCATION_OVERRIDE), - }, - )?; - ncellsused += 1; + + let next = self[current].next; + if self[current].bb_has_fallthrough() { + debug_assert!(next != BlockIdx::NULL); + if next != BlockIdx::NULL + && self[next].predecessors == 1 + && self[next].instruction_used != 0 + && self[next].instructions[0].instruction_is_no_location() + { + self[next].instructions[0].instr_set_location(prev_location); + } + } + + if last.is_jump() { + let target = last.target; + debug_assert!(target != BlockIdx::NULL); + if self[target].predecessors == 1 { + let instr = self[target].basicblock_raw_first_instr_mut(); + if instr.instruction_is_no_location() { + instr.instr_set_location(prev_location); + } + } + } + current = self[current].next; } } - if nfreevars > 0 { - basicblock_insert_instruction( - entry, - 0, - InstructionInfo { - instr: Instruction::CopyFreeVars { n: Arg::marker() }.into(), - arg: OpArg::new(nfreevars as u32), - target: BlockIdx::NULL, - location: SourceLocation::default(), - end_location: SourceLocation::default(), - except_handler: None, - lineno_override: Some(NO_LOCATION_OVERRIDE), - }, - )?; - } - Ok(()) -} + /// flowgraph.c remove_redundant_nops_and_pairs + fn remove_redundant_nops_and_pairs(&mut self) { + let mut done = false; -/// flowgraph.c prepare_localsplus -fn prepare_localsplus( - metadata: &CodeUnitMetadata, - blocks: &mut [Block], - flags: CodeFlags, -) -> crate::InternalResult { - let nlocals = metadata.varnames.len(); - let ncellvars = metadata.cellvars.len(); - let nfreevars = metadata.freevars.len(); - let int_max = i32::MAX as usize; - debug_assert!(nlocals < int_max); - debug_assert!(ncellvars < int_max); - debug_assert!(nfreevars < int_max); - debug_assert!(int_max - nlocals - ncellvars > 0); - debug_assert!(int_max - nlocals - ncellvars - nfreevars > 0); - let mut nlocalsplus = nlocals + ncellvars + nfreevars; - let mut cellfixedoffsets = build_cellfixedoffsets(metadata)?; + while !done { + done = true; + let mut instr: Option<(BlockIdx, usize)> = None; + let mut block_idx = BlockIdx::new(0); - // This must be called before fix_cell_offsets(). - insert_prefix_instructions(metadata, blocks, &cellfixedoffsets, nfreevars, flags)?; + while block_idx != BlockIdx::NULL { + self.basicblock_remove_redundant_nops(block_idx); + if is_label(self[block_idx].cpython_label) { + instr = None; + } - let numdropped = fix_cell_offsets(metadata, blocks, &mut cellfixedoffsets); - nlocalsplus -= numdropped; - Ok(nlocalsplus) -} + let len = self[block_idx].instruction_used; + for instr_idx in 0..len { + let prev_instr = instr; + instr = Some((block_idx, instr_idx)); + let instr_info = self[block_idx].instructions[instr_idx]; + let mut prev_opcode = None; + let prev_oparg = if let Some((prev_block, prev_instr_idx)) = prev_instr { + let prev_info = self[prev_block].instructions[prev_instr_idx]; + prev_opcode = prev_info.instr.real_opcode(); + match prev_info.instr.real() { + Some(Instruction::Copy { i }) => i.get(prev_info.arg), + _ => u32::from(prev_info.arg), + } + } else { + 0 + }; -/// flowgraph.c remove_unreachable -fn remove_unreachable(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - blocks[block_idx.idx()].predecessors = 0; - block_idx = blocks[block_idx.idx()].next; - } + let opcode = instr_info.instr.real_opcode(); + let is_redundant_pair = matches!(opcode, Some(Opcode::PopTop)) + && (matches!(prev_opcode, Some(Opcode::LoadConst | Opcode::LoadSmallInt)) + || (prev_oparg == 1 && matches!(prev_opcode, Some(Opcode::Copy)))); + + if is_redundant_pair { + let (prev_block, prev_instr_idx) = + prev_instr.expect("redundant pair has previous"); + self[prev_block].instructions[prev_instr_idx].set_to_nop(); + self[block_idx].instructions[instr_idx].set_to_nop(); + done = false; + } + } - let mut stack = make_cfg_traversal_stack(blocks)?; - blocks[0].predecessors = 1; - stack.push(BlockIdx(0)); - blocks[0].visited = true; - while let Some(current) = stack.pop() { - let idx = current.idx(); - let next = blocks[idx].next; - if next != BlockIdx::NULL && bb_has_fallthrough(&blocks[idx]) { - if !blocks[next.idx()].visited { - debug_assert_eq!(blocks[next.idx()].predecessors, 0); - stack.push(next); - blocks[next.idx()].visited = true; + let instr_is_jump = instr.is_some_and(|(instr_block, instr_idx)| { + self[instr_block].instructions[instr_idx].is_jump() + }); + + let block = &self[block_idx]; + if instr_is_jump || !block.bb_has_fallthrough() { + instr = None; + } + block_idx = block.next; } - blocks[next.idx()].predecessors += 1; } + } - let instr_count = blocks[idx].instruction_used; - for i in 0..instr_count { - let instr = blocks[idx].instructions[i]; - if is_jump(&instr) || is_block_push(&instr) { - let target = instr.target; - debug_assert!(target != BlockIdx::NULL); - let target_idx = target.idx(); - if !blocks[target_idx].visited { - stack.push(target); - blocks[target_idx].visited = true; + /// flowgraph.c calculate_stackdepth + fn calculate_stackdepth(&mut self) -> crate::InternalResult { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + self[current.idx()].start_depth = START_DEPTH_UNSET; + current = self[current.idx()].next; + } + let mut stack = self.make_cfg_traversal_stack()?; + let mut maxdepth = 0i32; + stackdepth_push(&mut stack, self, BlockIdx(0), 0)?; + while let Some(block_idx) = stack.pop() { + let mut depth = self[block_idx].start_depth; + debug_assert!(depth >= 0); + let mut next = self[block_idx].next; + let instr_count = self[block_idx].instruction_used; + for i in 0..instr_count { + let ins = self[block_idx].instructions[i]; + let instr = &ins.instr; + let effects = get_stack_effects(*instr, ins.arg, 0)?; + let new_depth = depth + effects.net; + if new_depth < 0 { + return Err(InternalError::StackUnderflow); + } + maxdepth = maxdepth.max(depth); + if instr.has_target() && !matches!(instr.real(), Some(Instruction::EndAsyncFor)) { + debug_assert!(ins.target != BlockIdx::NULL); + let effects = get_stack_effects(*instr, ins.arg, 1)?; + let target_depth = depth + effects.net; + debug_assert!(target_depth >= 0); + maxdepth = maxdepth.max(depth); + stackdepth_push(&mut stack, self, ins.target, target_depth)?; + } + depth = new_depth; + debug_assert!(!instr.is_assembler()); + if instr.is_unconditional_jump() || instr.is_scope_exit() { + next = BlockIdx::NULL; + break; } - blocks[target_idx].predecessors += 1; + } + + if next != BlockIdx::NULL { + debug_assert!(self[block_idx].bb_has_fallthrough()); + stackdepth_push(&mut stack, self, next, depth)?; } } + + let stackdepth = maxdepth; + Ok(stackdepth as u32) } - block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let i = block_idx.idx(); - let next = blocks[i].next; - if blocks[i].predecessors == 0 { - let block = &mut blocks[i]; - basicblock_clear(block); - block.except_handler = false; + /// flowgraph.c make_cfg_traversal_stack + fn make_cfg_traversal_stack(&mut self) -> crate::InternalResult { + debug_assert!(!self.is_empty()); + + let mut nblocks = 0; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + self[current].visited = false; + nblocks += 1; + current = self[current].next; } - block_idx = next; + debug_assert!(nblocks > 0); + let mut stack = Vec::new(); + stack + .try_reserve_exact(nblocks) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + stack.resize(nblocks, BlockIdx::NULL); + let stack = CfgTraversalStack { stack, sp: 0 }; + debug_assert_eq!(stack.capacity(), nblocks); + Ok(stack) } - Ok(()) -} -/// flowgraph.c eval_const_unaryop -fn eval_const_unaryop( - operand: &ConstantData, - op: Instruction, - intrinsic: Option, -) -> Option { - match (operand, op, intrinsic) { - (ConstantData::Integer { value }, Instruction::UnaryNegative, None) => { - Some(ConstantData::Integer { value: -value }) + /// flowgraph.c normalize_jumps + fn normalize_jumps(&mut self) -> crate::InternalResult<()> { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + self[current].visited = false; + current = self[current].next; } - (ConstantData::Float { value }, Instruction::UnaryNegative, None) => { - Some(ConstantData::Float { value: -value }) + + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + self[current].visited = true; + self.normalize_jumps_in_block(current)?; + current = self[current].next; } - (ConstantData::Complex { value }, Instruction::UnaryNegative, None) => { - Some(ConstantData::Complex { value: -value }) + + Ok(()) + } + + /// flowgraph.c remove_unused_consts + fn remove_unused_consts(&mut self, consts: &mut ConstantPool) -> crate::InternalResult<()> { + let nconsts = consts.len(); + if nconsts == 0 { + return Ok(()); } - (ConstantData::Boolean { value }, Instruction::UnaryNegative, None) => { - Some(ConstantData::Integer { - value: BigInt::from(-i32::from(*value)), - }) + + let mut index_map = Vec::new(); + index_map + .try_reserve_exact(nconsts) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + index_map.resize(nconsts, 0isize); + + index_map[1..nconsts].fill(-1); + + // The first constant may be docstring; keep it always. + index_map[0] = 0; + + // Mark used consts. + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let block = &self[block_idx]; + for i in 0..block.instruction_used { + let instr = &block.instructions[i]; + if instr.instr.has_const() { + let index = u32::from(instr.arg) as usize; + debug_assert!(index < nconsts); + index_map[index] = index as isize; + } + } + block_idx = block.next; } - (ConstantData::Integer { value }, Instruction::UnaryInvert, None) => { - Some(ConstantData::Integer { value: !value }) + + // Now index_map[i] == i if consts[i] is used, -1 otherwise. + // Condense consts. + let mut n_used_consts = 0; + for i in 0..nconsts { + if index_map[i] != -1 { + debug_assert_eq!(index_map[i], i as isize); + index_map[n_used_consts] = index_map[i]; + n_used_consts += 1; + } } - (ConstantData::Boolean { .. }, Instruction::UnaryInvert, None) => None, - (_, Instruction::UnaryNot, None) => Some(ConstantData::Boolean { - value: !constant_truthiness(operand), - }), - ( - ConstantData::Integer { value }, - Instruction::CallIntrinsic1 { .. }, - Some(oparg::IntrinsicFunction1::UnaryPositive), - ) => Some(ConstantData::Integer { - value: value.clone(), - }), - ( - ConstantData::Float { value }, - Instruction::CallIntrinsic1 { .. }, - Some(oparg::IntrinsicFunction1::UnaryPositive), - ) => Some(ConstantData::Float { value: *value }), - ( - ConstantData::Boolean { value }, - Instruction::CallIntrinsic1 { .. }, - Some(oparg::IntrinsicFunction1::UnaryPositive), - ) => Some(ConstantData::Integer { - value: BigInt::from(i32::from(*value)), - }), - ( - ConstantData::Complex { value }, - Instruction::CallIntrinsic1 { .. }, - Some(oparg::IntrinsicFunction1::UnaryPositive), - ) => Some(ConstantData::Complex { value: *value }), - _ => None, - } -} -fn constant_truthiness(constant: &ConstantData) -> bool { - match constant { - ConstantData::Tuple { elements } | ConstantData::Frozenset { elements } => { - !elements.is_empty() + if n_used_consts == nconsts { + return Ok(()); } - ConstantData::Integer { value } => !value.is_zero(), - ConstantData::Float { value } => *value != 0.0, - ConstantData::Complex { value } => value.re != 0.0 || value.im != 0.0, - ConstantData::Boolean { value } => *value, - ConstantData::Str { value } => !value.is_empty(), - ConstantData::Bytes { value } => !value.is_empty(), - ConstantData::Code { .. } | ConstantData::Slice { .. } | ConstantData::Ellipsis => true, - ConstantData::None => false, - } -} -fn load_const_truthiness( - instr: Instruction, - arg: OpArg, - metadata: &CodeUnitMetadata, -) -> Option { - match instr { - Instruction::LoadConst { consti } => { - let constant = &metadata.consts[consti.get(arg).as_usize()]; - Some(constant_truthiness(constant)) + // Move all used consts to the beginning of the consts list. + debug_assert!(n_used_consts < nconsts); + for (i, item) in index_map.iter().enumerate().take(n_used_consts) { + let old_index = *item as usize; + debug_assert!(i <= old_index && old_index < nconsts); + if i != old_index { + let value = consts.constants[old_index].clone(); + consts.constants[i] = value; + } } - Instruction::LoadSmallInt { i } => Some(i.get(arg) != 0), - _ => None, - } -} - -/// flowgraph.c add_const -fn add_const( - metadata: &mut CodeUnitMetadata, - constant: ConstantData, -) -> crate::InternalResult { - Ok(metadata.consts.try_insert_full(constant)?.0) -} -fn instr_make_load_const( - metadata: &mut CodeUnitMetadata, - instr: &mut InstructionInfo, - constant: ConstantData, -) -> crate::InternalResult<()> { - if maybe_instr_make_load_smallint(instr, &constant) { - return Ok(()); - } + // Truncate the consts list at its new size. + consts.constants.truncate(n_used_consts); - let const_idx = add_const(metadata, constant)?; - instr_set_op1( - instr, - Instruction::LoadConst { - consti: Arg::marker(), - } - .into(), - OpArg::new(const_idx as u32), - ); - Ok(()) -} + // Adjust const indices in the bytecode. + let mut reverse_index_map = Vec::new(); + reverse_index_map + .try_reserve_exact(nconsts) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + reverse_index_map.resize(nconsts, 0isize); -/// flowgraph.c fold_const_unaryop -fn fold_const_unaryop( - metadata: &mut CodeUnitMetadata, - block: &mut Block, - i: usize, -) -> crate::InternalResult { - let instr = &block.instructions[i]; - let (op, intrinsic) = match instr.instr.real() { - Some(Instruction::UnaryNegative) => (Instruction::UnaryNegative, None), - Some(Instruction::UnaryInvert) => (Instruction::UnaryInvert, None), - Some(Instruction::UnaryNot) => (Instruction::UnaryNot, None), - Some(Instruction::CallIntrinsic1 { func }) - if matches!( - func.get(instr.arg), - oparg::IntrinsicFunction1::UnaryPositive - ) => - { - ( - Instruction::CallIntrinsic1 { - func: Arg::marker(), - }, - Some(func.get(instr.arg)), - ) + reverse_index_map[..nconsts].fill(-1); + for (i, old_index) in index_map.iter().enumerate().take(n_used_consts) { + debug_assert!(*old_index != -1); + let old_index = *old_index as usize; + debug_assert_eq!(reverse_index_map[old_index], -1); + reverse_index_map[old_index] = i as isize; } - _ => return Ok(false), - }; - let Some(operand_index) = (if let Some(start) = i.checked_sub(1) { - get_const_loading_instrs(block, start, 1)? - } else { - None - }) - .and_then(|indices| indices.into_iter().next()) else { - return Ok(false); - }; - let operand = get_const_value(metadata, &block.instructions[operand_index]); - let Some(operand) = operand else { - return Ok(false); - }; - let Some(folded_const) = eval_const_unaryop(&operand, op, intrinsic) else { - return Ok(false); - }; - nop_out(block, &[operand_index]); - instr_make_load_const(metadata, &mut block.instructions[i], folded_const)?; - Ok(true) -} -/// flowgraph.c get_const_loading_instrs -fn get_const_loading_instrs( - block: &Block, - mut start: usize, - size: usize, -) -> crate::InternalResult>> { - let mut indices = Vec::new(); - indices - .try_reserve_exact(size) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - loop { - if start >= block.instruction_used { - return Ok(None); - } - let instr = &block.instructions[start]; - if !matches!(instr.instr.real(), Some(Instruction::Nop)) { - if !loads_const(instr) { - return Ok(None); - } - indices.push(start); - if indices.len() == size { - break; + block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next_block = self[block_idx].next; + let block = &mut self[block_idx]; + for i in 0..block.instruction_used { + let instr = &mut block.instructions[i]; + if instr.instr.has_const() { + let index = u32::from(instr.arg) as usize; + debug_assert!(reverse_index_map[index] >= 0); + debug_assert!(reverse_index_map[index] < n_used_consts as isize); + instr.arg = OpArg::new(reverse_index_map[index] as u32); + } } + block_idx = next_block; } - let Some(prev) = start.checked_sub(1) else { - return Ok(None); - }; - start = prev; + Ok(()) } - indices.reverse(); - Ok(Some(indices)) -} -/// flowgraph.c nop_out -fn nop_out(block: &mut Block, instrs: &[usize]) { - for &i in instrs { - nop_out_no_location(&mut block.instructions[i]); - } -} + /// flowgraph.c insert_superinstructions + fn insert_superinstructions(&mut self) -> usize { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next_block = self[block_idx].next; + let block = &mut self[block_idx]; + for i in 0..block.instruction_used { + let nextop = (i + 1 < block.instruction_used) + .then(|| block.instructions[i + 1].instr.real_opcode()) + .flatten(); + + let super_op = match (block.instructions[i].instr.real_opcode(), nextop) { + (Some(Opcode::LoadFast), Some(Opcode::LoadFast)) => { + Some(Opcode::LoadFastLoadFast) + } -/// flowgraph.c fold_const_binop -fn fold_const_binop( - metadata: &mut CodeUnitMetadata, - block: &mut Block, - i: usize, -) -> crate::InternalResult { - use oparg::BinaryOperator as BinOp; + (Some(Opcode::StoreFast), Some(Opcode::LoadFast)) => { + Some(Opcode::StoreFastLoadFast) + } - let Some(Instruction::BinaryOp { .. }) = block.instructions[i].instr.real() else { - return Ok(false); - }; - let Some(operand_indices) = (if let Some(start) = i.checked_sub(1) { - get_const_loading_instrs(block, start, 2)? - } else { - None - }) else { - return Ok(false); - }; - let op_raw = u32::from(block.instructions[i].arg); - let Ok(op) = BinOp::try_from(op_raw) else { - return Ok(false); - }; - let left = get_const_value(metadata, &block.instructions[operand_indices[0]]); - let right = get_const_value(metadata, &block.instructions[operand_indices[1]]); - let (Some(left_val), Some(right_val)) = (left, right) else { - return Ok(false); - }; - let Some(result_const) = eval_const_binop(&left_val, &right_val, op) else { - return Ok(false); - }; - nop_out(block, &operand_indices); - instr_make_load_const(metadata, &mut block.instructions[i], result_const)?; - Ok(true) -} + (Some(Opcode::StoreFast), Some(Opcode::StoreFast)) => { + Some(Opcode::StoreFastStoreFast) + } -/// flowgraph.c loads_const -fn loads_const(info: &InstructionInfo) -> bool { - info.instr.has_const() || matches!(info.instr.real(), Some(Instruction::LoadSmallInt { .. })) -} + (_, _) => None, + }; -/// flowgraph.c get_const_value -fn get_const_value(metadata: &CodeUnitMetadata, info: &InstructionInfo) -> Option { - match info.instr.real() { - Some(Instruction::LoadSmallInt { .. }) => { - let v = u32::from(info.arg) as i32; - Some(ConstantData::Integer { - value: BigInt::from(v), - }) - } - _ if info.instr.has_const() => { - let idx = u32::from(info.arg) as usize; - metadata.consts.get_index(idx).cloned() + if let Some(super_op) = super_op { + let (inst1, rest) = block.instructions[i..].split_at_mut(1); + + InstructionInfo::make_super_instruction( + &mut inst1[0], + &mut rest[0], + super_op.into(), + ); + } + } + + block_idx = next_block; } - _ => None, + + let res = self.remove_redundant_nops(); + + #[cfg(debug_assertions)] + assert!(self.no_redundant_nops()); + + res } -} -/// flowgraph.c const_folding_check_complexity -fn const_folding_check_complexity(obj: &ConstantData, mut limit: isize) -> Option { - if let ConstantData::Tuple { elements } = obj { - limit -= isize::try_from(elements.len()).ok()?; - if limit < 0 { - return None; + /// Mark exception handler target blocks. + /// flowgraph.c mark_except_handlers + pub(crate) fn mark_except_handlers(&mut self) { + #[cfg(debug_assertions)] + { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + assert!(!self[block_idx].except_handler); + block_idx = self[block_idx].next; + } } - for element in elements { - limit = const_folding_check_complexity(element, limit)?; + + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next = self[block_idx].next; + let instr_count = self[block_idx].instruction_used; + for i in 0..instr_count { + let instr = self[block_idx].instructions[i]; + if instr.is_block_push() { + debug_assert!(instr.target != BlockIdx::NULL); + self[instr.target].except_handler = true; + } + } + block_idx = next; } } - Some(limit) -} -fn repeat_wtf8(value: &Wtf8Buf, n: usize) -> Option { - let mut result = Wtf8Buf::new(); - result.try_reserve_exact(value.len().checked_mul(n)?).ok()?; - for _ in 0..n { - result.push_wtf8(value); - } - Some(result) -} + /// flowgraph.c mark_cold (two-pass). + /// + /// Phase 1 (mark_warm): propagate "warm" from entry via fall-through and + /// jump targets. The pass asserts while visiting warm blocks that they are not + /// exception handlers. + /// + /// Phase 2 (mark_cold): propagate "cold" from except_handler blocks via + /// forward edges. Blocks reached only via runtime exception dispatch are + /// marked cold and pushed to the end by push_cold_blocks_to_end. + /// + /// Blocks reached by neither phase remain `cold=false`. They are typically + /// empty unreachable placeholders left by remove_unreachable; they stay in + /// their original chain position (e.g. between entry and the post-try + /// continuation for a nested try/except whose inner_end was emptied by + /// optimize_cfg). This is necessary for + /// optimize_load_fast to terminate fall-through at those placeholders. + /// flowgraph.c mark_warm + fn mark_warm(&mut self) -> crate::InternalResult<()> { + let mut stack = self.make_cfg_traversal_stack()?; + stack.push(BlockIdx(0)); + self[0].visited = true; + while let Some(block_idx) = stack.pop() { + debug_assert!(!self[block_idx].except_handler); + self[block_idx].warm = true; + + let next = self[block_idx].next; + if next != BlockIdx::NULL && self[block_idx].bb_has_fallthrough() && !self[next].visited + { + stack.push(next); + self[next].visited = true; + } -fn checked_repeat_count(n: &BigInt, item_size: usize) -> Option { - let n = n.to_isize()?; - if item_size != 0 && (n < 0 || n as usize > MAX_STR_SIZE / item_size) { - return None; + let instr_count = self[block_idx].instruction_used; + for i in 0..instr_count { + let instr = self[block_idx].instructions[i]; + if instr.is_jump() { + let target = instr.target; + debug_assert!(target != BlockIdx::NULL); + if !self[target].visited { + stack.push(target); + self[target].visited = true; + } + } + } + } + Ok(()) } - Some(n.max(0) as usize) -} -/// flowgraph.c const_folding_safe_multiply -fn const_folding_safe_multiply(left: &ConstantData, right: &ConstantData) -> Option { - match (left, right) { - (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { - if !l.is_zero() && !r.is_zero() && l.bits() + r.bits() > MAX_INT_SIZE { - return None; - } - Some(ConstantData::Integer { value: l * r }) + fn mark_cold(&mut self) -> crate::InternalResult<()> { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let block = &mut self[block_idx]; + debug_assert!(!block.cold); + debug_assert!(!block.warm); + block_idx = block.next; } - (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { - Some(ConstantData::Float { value: l * r }) + + self.mark_warm()?; + + let mut cold_stack = self.make_cfg_traversal_stack()?; + block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next = self[block_idx].next; + let block = &self[block_idx]; + if block.except_handler { + debug_assert!(!block.warm); + cold_stack.push(block_idx); + self[block_idx].visited = true; + } + block_idx = next; } - (ConstantData::Str { value: s }, ConstantData::Integer { value: n }) => { - let n = checked_repeat_count(n, s.code_points().count())?; - Some(ConstantData::Str { - value: repeat_wtf8(s, n)?, - }) + + while let Some(block_idx) = cold_stack.pop() { + self[block_idx].cold = true; + let next = self[block_idx].next; + if next != BlockIdx::NULL + && self[block_idx].bb_has_fallthrough() + && !self[next].warm + && !self[next].visited + { + cold_stack.push(next); + self[next].visited = true; + } + + let instr_count = self[block_idx].instruction_used; + for i in 0..instr_count { + let instr = self[block_idx].instructions[i]; + if instr.is_jump() { + debug_assert_eq!(i, instr_count - 1); + let target = instr.target; + debug_assert!(target != BlockIdx::NULL); + if !self[target].warm && !self[target].visited { + cold_stack.push(target); + self[target].visited = true; + } + } + } } - (ConstantData::Integer { .. }, ConstantData::Str { .. }) => { - const_folding_safe_multiply(right, left) + Ok(()) + } + + /// flowgraph.c push_cold_blocks_to_end + fn push_cold_blocks_to_end(&mut self) -> crate::InternalResult<()> { + if self[0].next == BlockIdx::NULL { + return Ok(()); } - (ConstantData::Bytes { value: b }, ConstantData::Integer { value: n }) => { - let n = checked_repeat_count(n, b.len())?; - let mut value = Vec::new(); - value.try_reserve_exact(b.len().checked_mul(n)?).ok()?; - for _ in 0..n { - value.extend_from_slice(b); + + self.mark_cold()?; + let mut next_label = get_max_label(self) + 1; + + // If a cold block falls through to a warm block, add an explicit jump + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next = self[block_idx].next; + if self[block_idx].cold + && self[block_idx].bb_has_fallthrough() + && next != BlockIdx::NULL + && self[next].warm + { + let explicit_jump = self.blocks_new_block()?; + if !is_label(self[next].cpython_label) { + self[next].cpython_label = InstructionSequenceLabel::from_index(next_label); + next_label += 1; + } + let jump_label = self[next].cpython_label; + debug_assert!(is_label(jump_label)); + self[explicit_jump].basicblock_addop(InstructionInfo { + instr: PseudoOpcode::JumpNoInterrupt.into(), + arg: instruction_sequence_label_oparg(jump_label), + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: Some(NO_LOCATION_OVERRIDE), + })?; + self[explicit_jump].cold = true; + self[explicit_jump].next = next; + self[explicit_jump].predecessors = 1; + self[block_idx].next = explicit_jump; + let target = self[explicit_jump].next; + let last = self[explicit_jump] + .basicblock_last_instr_mut() + .expect("missing explicit jump"); + last.target = target; } - Some(ConstantData::Bytes { value }) + block_idx = self[block_idx].next; } - (ConstantData::Integer { .. }, ConstantData::Bytes { .. }) => { - const_folding_safe_multiply(right, left) - } - (ConstantData::Tuple { elements }, ConstantData::Integer { value: n }) => { - let n = n.to_usize()?; - if n != 0 && !elements.is_empty() { - if n > MAX_COLLECTION_SIZE / elements.len() { - return None; - } - const_folding_check_complexity( - &ConstantData::Tuple { - elements: elements.clone(), - }, - MAX_TOTAL_ITEMS / isize::try_from(n).ok()?, - )?; + + assert!(!self[0].cold); + let mut cold_blocks: BlockIdx = BlockIdx::NULL; + let mut cold_blocks_tail: BlockIdx = BlockIdx::NULL; + let mut block_idx = BlockIdx(0); + + while self[block_idx].next != BlockIdx::NULL { + debug_assert!(!self[block_idx].cold); + while self[block_idx].next != BlockIdx::NULL && !self[self[block_idx].next].cold { + block_idx = self[block_idx].next; } - let mut result = Vec::new(); - result - .try_reserve_exact(elements.len().checked_mul(n)?) - .ok()?; - for _ in 0..n { - result.extend(elements.iter().cloned()); + + if self[block_idx].next == BlockIdx::NULL { + break; } - Some(ConstantData::Tuple { elements: result }) + + debug_assert!(!self[block_idx].cold); + debug_assert!(self[self[block_idx].next].cold); + + let mut block_end = self[block_idx].next; + while self[block_end].next != BlockIdx::NULL && self[self[block_end].next].cold { + block_end = self[block_end].next; + } + + debug_assert!(self[block_end].cold); + debug_assert!( + self[block_end].next == BlockIdx::NULL || !self[self[block_end].next].cold + ); + + if cold_blocks == BlockIdx::NULL { + cold_blocks = self[block_idx].next; + } else { + self[cold_blocks_tail].next = self[block_idx].next; + } + + cold_blocks_tail = block_end; + self[block_idx].next = self[block_end].next; + self[block_end].next = BlockIdx::NULL; } - (ConstantData::Integer { .. }, ConstantData::Tuple { .. }) => { - const_folding_safe_multiply(right, left) + + debug_assert!(self[block_idx].next == BlockIdx::NULL); + self[block_idx].next = cold_blocks; + + if cold_blocks != BlockIdx::NULL { + self.remove_redundant_nops_and_jumps()?; } - _ => None, + Ok(()) } -} -/// flowgraph.c const_folding_safe_power -fn const_folding_safe_power(left: &ConstantData, right: &ConstantData) -> Option { - match (left, right) { - (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { - if r < &BigInt::from(0) { - if l.is_zero() { - return None; - } - let base = l.to_f64()?; - if !base.is_finite() { - return None; - } - let result = if let Some(exp) = r.to_i32() { - base.powi(exp) - } else { - base.powf(r.to_f64()?) - }; - if !result.is_finite() { - return None; + /// flowgraph.c check_cfg + fn check_cfg(&self) -> crate::InternalResult<()> { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let block = &self[block_idx]; + for i in 0..block.instruction_used { + let opcode = block.instructions[i].instr; + debug_assert!(!opcode.is_assembler()); + if opcode.is_terminator() && i != block.instruction_used - 1 { + return Err(InternalError::MalformedControlFlowGraph); } - return Some(ConstantData::Float { value: result }); - } - let exp: u64 = r.try_into().ok()?; - let exp_usize = usize::try_from(exp).ok()?; - if !l.is_zero() && exp > 0 && l.bits() > MAX_INT_SIZE / exp { - return None; } - Some(ConstantData::Integer { - value: num_traits::pow::pow(l.clone(), exp_usize), - }) - } - (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { - let result = l.powf(*r); - result - .is_finite() - .then_some(ConstantData::Float { value: result }) + block_idx = block.next; } - _ => None, + Ok(()) } -} -/// flowgraph.c const_folding_safe_lshift -fn const_folding_safe_lshift(left: &ConstantData, right: &ConstantData) -> Option { - let (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) = (left, right) - else { - return None; - }; - let shift: u64 = r.try_into().ok()?; - let shift_usize = usize::try_from(shift).ok()?; - if shift > MAX_INT_SIZE || (!l.is_zero() && l.bits() > MAX_INT_SIZE - shift) { - return None; - } - Some(ConstantData::Integer { - value: l << shift_usize, - }) -} + /// flowgraph.c jump_thread + fn jump_thread( + &mut self, + block_idx: BlockIdx, + instr_idx: usize, + target: &InstructionInfo, + opcode: AnyInstruction, + ) -> crate::InternalResult { + debug_assert!(self[block_idx].instructions[instr_idx].is_jump()); + debug_assert!(target.is_jump()); + debug_assert_eq!(instr_idx + 1, self[block_idx].instruction_used); + debug_assert!(target.target != BlockIdx::NULL); + + if self[block_idx].instructions[instr_idx].target != target.target { + self[block_idx].instructions[instr_idx].set_to_nop(); + self.basicblock_add_jump(block_idx, opcode, target.target, target)?; + return Ok(true); + } -/// flowgraph.c const_folding_safe_mod -fn const_folding_safe_mod(left: &ConstantData, right: &ConstantData) -> Option { - if matches!(left, ConstantData::Str { .. } | ConstantData::Bytes { .. }) { - return None; + Ok(false) } - match (left, right) { - (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { - if r.is_zero() { - return None; - } - let rem = l.clone() % r.clone(); - let value = if !rem.is_zero() && (rem < BigInt::from(0)) != (*r < BigInt::from(0)) { - rem + r - } else { - rem - }; - Some(ConstantData::Integer { value }) - } - (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { - let (_, modulo) = float_div_mod(*l, *r)?; - Some(ConstantData::Float { value: modulo }) + /// flowgraph.c basicblock_add_jump + fn basicblock_add_jump( + &mut self, + block_idx: BlockIdx, + instr: AnyInstruction, + target: BlockIdx, + loc_source: &InstructionInfo, + ) -> crate::InternalResult<()> { + let last = self[block_idx].basicblock_last_instr(); + if last.is_some_and(|l| l.is_jump()) { + return Err(InternalError::MalformedControlFlowGraph); } - _ => None, + debug_assert!(target != BlockIdx::NULL); + let label = self[target].cpython_label; + debug_assert!(is_label(label)); + let arg = instruction_sequence_label_oparg(label); + let block = &mut self[block_idx]; + block.basicblock_addop(InstructionInfo { + instr, + arg, + target: BlockIdx::NULL, + location: loc_source.location, + end_location: loc_source.end_location, + except_handler: None, + lineno_override: loc_source.lineno_override, + })?; + let last = block.basicblock_last_instr_mut().expect("missing jump"); + debug_assert!(match (last.instr, instr) { + (AnyInstruction::Real(last), AnyInstruction::Real(opcode)) => + last.as_opcode() == opcode.as_opcode(), + (AnyInstruction::Pseudo(last), AnyInstruction::Pseudo(opcode)) => + last.as_opcode() == opcode.as_opcode(), + _ => false, + }); + last.target = target; + Ok(()) } -} -fn float_div_mod(left: f64, right: f64) -> Option<(f64, f64)> { - if right == 0.0 { - return None; + /// flowgraph.c convert_pseudo_conditional_jumps + fn convert_pseudo_conditional_jumps(&mut self) -> crate::InternalResult<()> { + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next = self[block_idx].next; + let block = &mut self[block_idx]; + let mut i = 0; + while i < block.instruction_used { + let instr = block.instructions[i]; + let opcode = instr.instr; + if matches!( + opcode.pseudo_opcode(), + Some(PseudoOpcode::JumpIfFalse | PseudoOpcode::JumpIfTrue) + ) { + debug_assert_eq!(i, block.instruction_used - 1); + block.instructions[i].instr = + if matches!(opcode.pseudo_opcode(), Some(PseudoOpcode::JumpIfFalse)) { + Opcode::PopJumpIfFalse + } else { + Opcode::PopJumpIfTrue + } + .into(); + + let location = instr.location; + let end_location = instr.end_location; + let except_handler = instr.except_handler; + let lineno_override = instr.lineno_override; + let copy = InstructionInfo { + instr: Opcode::Copy.into(), + arg: OpArg::new(1), + target: BlockIdx::NULL, + location, + end_location, + except_handler, + lineno_override, + }; + block.basicblock_insert_instruction(i, copy)?; + i += 1; + + let to_bool = InstructionInfo { + instr: Opcode::ToBool.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location, + end_location, + except_handler, + lineno_override, + }; + block.basicblock_insert_instruction(i, to_bool)?; + i += 1; + } + i += 1; + } + block_idx = next; + } + Ok(()) } - let mut modulo = left % right; - let div = (left - modulo) / right; - let floordiv = if modulo != 0.0 { - let div = if (right < 0.0) != (modulo < 0.0) { - modulo += right; - div - 1.0 - } else { - div + /// flowgraph.c normalize_jumps_in_block + fn normalize_jumps_in_block(&mut self, block_idx: BlockIdx) -> crate::InternalResult<()> { + let Some(last_ins) = self[block_idx].basicblock_last_instr().copied() else { + return Ok(()); }; - let mut floordiv = div.floor(); - if div - floordiv > 0.5 { - floordiv += 1.0; + if !is_conditional_jump_opcode(last_ins.instr) { + return Ok(()); } - floordiv - } else { - modulo = 0.0f64.copysign(right); - 0.0f64.copysign(left / right) - }; - - Some((floordiv, modulo)) -} + debug_assert!(!last_ins.instr.is_assembler()); -/// flowgraph.c eval_const_binop complex result construction -fn eval_const_complex_const(value: Complex) -> Option { - (value.re.is_finite() && value.im.is_finite()).then_some(ConstantData::Complex { value }) -} - -/// flowgraph.c eval_const_binop complex operations -fn eval_const_complex_binop( - left: Complex, - right: Complex, - op: oparg::BinaryOperator, -) -> Option { - use oparg::BinaryOperator as BinOp; + debug_assert!(last_ins.target != BlockIdx::NULL); + let is_forward = !self[last_ins.target].visited; - let value = match op { - BinOp::Add => left + right, - BinOp::Subtract => { - let re = left.re - right.re; - // Preserve CPython's signed-zero behavior for real-zero - // minus zero-complex expressions such as `0 - 0j`. - let im = if left.re == 0.0 - && left.im == 0.0 - && right.re == 0.0 - && right.im == 0.0 - && !right.im.is_sign_negative() - { - -0.0 - } else { - left.im - right.im + if is_forward { + // Insert NOT_TAKEN after forward conditional jump. + let not_taken = InstructionInfo { + instr: Opcode::NotTaken.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location: last_ins.location, + end_location: last_ins.end_location, + except_handler: None, + lineno_override: last_ins.lineno_override, }; - Complex::new(re, im) - } - BinOp::Multiply => left * right, - BinOp::TrueDivide => { - if right == Complex::new(0.0, 0.0) { - return None; - } - left / right + + self[block_idx].basicblock_addop(not_taken)?; + return Ok(()); } - BinOp::Power => { - if left == Complex::new(0.0, 0.0) { - if right.im != 0.0 || right.re < 0.0 { - return None; - } - return eval_const_complex_const(if right.re == 0.0 { - Complex::new(1.0, 0.0) - } else { - Complex::new(0.0, 0.0) - }); - } + let reversed_opcode = match last_ins.instr.real_opcode() { + Some(Opcode::PopJumpIfNotNone) => Opcode::PopJumpIfNone.into(), + Some(Opcode::PopJumpIfNone) => Opcode::PopJumpIfNotNone.into(), + Some(Opcode::PopJumpIfFalse) => Opcode::PopJumpIfTrue.into(), + Some(Opcode::PopJumpIfTrue) => Opcode::PopJumpIfFalse.into(), + _ => unreachable!("conditional jump has reverse opcode"), + }; - if right.im == 0.0 - && right.re.fract() == 0.0 - && right.re >= f64::from(i32::MIN) - && right.re <= f64::from(i32::MAX) - { - left.powi(right.re as i32) - } else { - left.powc(right) - } - } - _ => return None, - }; - eval_const_complex_const(value) -} + // Transform 'conditional jump T' to 'reversed_jump b_next' followed by + // 'jump_backwards T'. + let loc = last_ins.location; + let end_loc = last_ins.end_location; -/// flowgraph.c eval_const_binop subscript index conversion -fn constant_as_index(value: &ConstantData) -> Option { - match value { - ConstantData::Integer { value } => value.to_i64().or_else(|| { - if value < &BigInt::from(0) { - Some(i64::MIN) - } else { - Some(i64::MAX) - } - }), - ConstantData::Boolean { value } => Some(i64::from(*value)), - _ => None, - } -} + let target = last_ins.target; + let backwards_jump_idx = self.blocks_new_block()?; -/// flowgraph.c eval_const_binop subscript slice bound conversion -fn slice_bound(value: &ConstantData) -> Option> { - match value { - ConstantData::None => Some(None), - _ => constant_as_index(value).map(Some), - } -} + self[backwards_jump_idx].basicblock_addop(InstructionInfo { + instr: Opcode::NotTaken.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location: loc, + end_location: end_loc, + except_handler: None, + lineno_override: last_ins.lineno_override, + })?; + self.basicblock_add_jump( + backwards_jump_idx, + PseudoOpcode::Jump.into(), + target, + &last_ins, + )?; + self[backwards_jump_idx].start_depth = self[target].start_depth; -/// flowgraph.c eval_const_binop subscript slice index adjustment -fn adjusted_slice_indices(len: usize, slice: &[ConstantData; 3]) -> Option> { - let len = i64::try_from(len).ok()?; - let start = slice_bound(&slice[0])?; - let stop = slice_bound(&slice[1])?; - let step = slice_bound(&slice[2])?.unwrap_or(1); - if step == 0 || step == i64::MIN { - return None; + let old_next = self[block_idx].next; + debug_assert!(old_next != BlockIdx::NULL); + + let last_mut = self[block_idx].basicblock_last_instr_mut().unwrap(); + last_mut.instr = reversed_opcode; + last_mut.target = old_next; + + self[backwards_jump_idx].cold = self[block_idx].cold; + self[backwards_jump_idx].next = old_next; + self[block_idx].next = backwards_jump_idx; + Ok(()) } - let step_is_negative = step < 0; - let lower = if step_is_negative { -1 } else { 0 }; - let upper = if step_is_negative { len - 1 } else { len }; - let adjust = |value: Option, default: i64| { - let mut value = value.unwrap_or(default); - if value < 0 { - value = value.saturating_add(len); - if value < 0 { - value = lower; - } - } else if value >= len { - value = upper; - } - value - }; - let start = adjust(start, if step_is_negative { upper } else { lower }); - let stop = adjust(stop, if step_is_negative { lower } else { upper }); + /// flowgraph.c basicblock_inline_small_or_no_lineno_blocks + fn basicblock_inline_small_or_no_lineno_blocks( + &mut self, + block_idx: BlockIdx, + ) -> crate::InternalResult { + let Some(last) = self[block_idx].basicblock_last_instr().copied() else { + return Ok(false); + }; - let mut index = i128::from(start); - let stop = i128::from(stop); - let step = i128::from(step); - let slice_len = if step > 0 { - if index < stop { - usize::try_from((stop - index - 1) / step + 1).ok()? - } else { - 0 - } - } else if index > stop { - usize::try_from((index - stop - 1) / -step + 1).ok()? - } else { - 0 - }; - let mut indices = Vec::new(); - indices.try_reserve_exact(slice_len).ok()?; - if step > 0 { - while index < stop { - indices.push(usize::try_from(index).ok()?); - index += step; + if !last.instr.is_unconditional_jump() { + return Ok(false); } - } else { - while index > stop { - indices.push(usize::try_from(index).ok()?); - index += step; + + let target = last.target; + debug_assert!(target != BlockIdx::NULL); + let small_exit_block = + self[target].basicblock_exits_scope() && self[target].instruction_used <= MAX_COPY_SIZE; + let no_lineno_no_fallthrough = + self[target].basicblock_has_no_lineno() && !self[target].bb_has_fallthrough(); + if small_exit_block || no_lineno_no_fallthrough { + debug_assert!(last.is_jump()); + let removed_jump_opcode = last.instr; + let last = self[block_idx] + .basicblock_last_instr_mut() + .expect("non-empty block has last instruction"); + last.set_to_nop(); + self.basicblock_append_block_instructions(block_idx, target)?; + if no_lineno_no_fallthrough { + let last = self[block_idx].basicblock_last_instr_mut().unwrap(); + if last.instr.is_unconditional_jump() + && matches!( + removed_jump_opcode.into(), + AnyOpcode::Pseudo(PseudoOpcode::Jump) + ) + { + last.instr = PseudoOpcode::Jump.into(); + } + } + self[target].predecessors -= 1; + return Ok(true); } + Ok(false) } - Some(indices) -} -/// flowgraph.c eval_const_binop subscript index adjustment -fn adjusted_const_index(len: usize, index: &ConstantData) -> Option { - let len = i64::try_from(len).ok()?; - let index = constant_as_index(index)?; - let index = if index < 0 { - index.saturating_add(len) - } else { - index - }; - if index < 0 || index >= len { - return None; - } - usize::try_from(index).ok() -} + /// flowgraph.c inline_small_or_no_lineno_blocks + fn inline_small_or_no_lineno_blocks(&mut self) -> crate::InternalResult { + loop { + let mut changes = false; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let next = self[current].next; + let res = self.basicblock_inline_small_or_no_lineno_blocks(current)?; + if res { + changes = true; + } -/// flowgraph.c eval_const_binop NB_SUBSCR -fn eval_const_subscript(container: &ConstantData, index: &ConstantData) -> Option { - match (container, index) { - ( - ConstantData::Str { value }, - ConstantData::Integer { .. } | ConstantData::Boolean { .. }, - ) => { - let string = value.to_string(); - if string.contains(char::REPLACEMENT_CHARACTER) { - return None; + current = next; + } + if !changes { + return Ok(changes); } - let mut chars = Vec::new(); - chars.try_reserve_exact(string.chars().count()).ok()?; - chars.extend(string.chars()); - let index = adjusted_const_index(chars.len(), index)?; - Some(ConstantData::Str { - value: chars[index].to_string().into(), - }) } - (ConstantData::Str { value }, ConstantData::Slice { elements }) => { - let string = value.to_string(); - if string.contains(char::REPLACEMENT_CHARACTER) { - return None; + } + + /// flowgraph.c basicblock_remove_redundant_nops + fn basicblock_remove_redundant_nops(&mut self, block_idx: BlockIdx) -> usize { + let mut dest = 0; + let mut prev_lineno = -1i32; + let instr_count = self[block_idx].instruction_used; + + for src in 0..instr_count { + let instr = self[block_idx].instructions[src]; + let lineno = instr.instruction_lineno(); + + if matches!(instr.instr.real(), Some(Instruction::Nop)) { + if lineno < 0 { + continue; + } + if prev_lineno == lineno { + continue; + } + if src < instr_count - 1 { + let next_lineno = self[block_idx].instructions[src + 1].instruction_lineno(); + if next_lineno == lineno { + continue; + } + if next_lineno < 0 { + self[block_idx].instructions[src + 1].instr_set_loc( + instr.location, + instr.end_location, + instr.lineno_override, + ); + continue; + } + } else { + let next = next_nonempty_block(self, self[block_idx].next); + if next != BlockIdx::NULL { + let mut next_loc = no_linetable_location(); + let mut next_i = 0; + while next_i < self[next].instruction_used { + let instr = self[next].instructions[next_i]; + if matches!(instr.instr.real(), Some(Instruction::Nop)) + && instr.instruction_lineno() < 0 + { + next_i += 1; + continue; + } + next_loc = instr.instruction_linetable_location(); + break; + } + if lineno == next_loc.line { + continue; + } + } + } } - let mut chars = Vec::new(); - chars.try_reserve_exact(string.chars().count()).ok()?; - chars.extend(string.chars()); - let indices = adjusted_slice_indices(chars.len(), elements)?; - let capacity = indices.iter().try_fold(0usize, |capacity, &index| { - capacity.checked_add(chars[index].len_utf8()) - })?; - let mut result = String::new(); - result.try_reserve_exact(capacity).ok()?; - for index in indices { - result.push(chars[index]); + + if dest != src { + self[block_idx].instructions[dest] = self[block_idx].instructions[src]; } - Some(ConstantData::Str { - value: result.into(), - }) + dest += 1; + prev_lineno = lineno; } - ( - ConstantData::Bytes { value }, - ConstantData::Integer { .. } | ConstantData::Boolean { .. }, - ) => { - let index = adjusted_const_index(value.len(), index)?; - Some(ConstantData::Integer { - value: BigInt::from(value[index]), - }) + + debug_assert!(dest <= instr_count); + let num_removed = instr_count - dest; + self[block_idx].instruction_used = dest; + num_removed + } + + /// flowgraph.c remove_redundant_nops + fn remove_redundant_nops(&mut self) -> usize { + let mut changes = 0; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let next = self[current].next; + let change = self.basicblock_remove_redundant_nops(current); + changes += change; + current = next; } - (ConstantData::Bytes { value }, ConstantData::Slice { elements }) => { - let indices = adjusted_slice_indices(value.len(), elements)?; - let mut result = Vec::new(); - result.try_reserve_exact(indices.len()).ok()?; - for index in indices { - result.push(value[index]); + changes + } + + /// flowgraph.c no_redundant_nops + #[cfg(debug_assertions)] + fn no_redundant_nops(&mut self) -> bool { + self.remove_redundant_nops() == 0 + } + + /// flowgraph.c remove_redundant_jumps + fn remove_redundant_jumps(&mut self) -> crate::InternalResult { + let mut changes = 0; + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let Some(last) = self[current].basicblock_last_instr().copied() else { + current = self[current].next; + continue; + }; + + debug_assert!(!last.instr.is_assembler()); + if last.instr.is_unconditional_jump() { + let jump_target = next_nonempty_block(self, last.target); + if jump_target == BlockIdx::NULL { + return Err(InternalError::MalformedControlFlowGraph); + } + let next = next_nonempty_block(self, self[current].next); + if jump_target == next { + changes += 1; + let last = self[current].basicblock_last_instr_mut().unwrap(); + last.set_to_nop(); + } } - Some(ConstantData::Bytes { value: result }) + current = self[current].next; } - ( - ConstantData::Tuple { elements }, - ConstantData::Integer { .. } | ConstantData::Boolean { .. }, - ) => { - let index = adjusted_const_index(elements.len(), index)?; - Some(elements[index].clone()) + Ok(changes) + } + + /// flowgraph.c no_redundant_jumps + #[cfg(debug_assertions)] + fn no_redundant_jumps(&self) -> bool { + let mut current = BlockIdx(0); + while current != BlockIdx::NULL { + let block = &self[current]; + if let Some(last) = block.basicblock_last_instr() + && last.instr.is_unconditional_jump() + { + let next = next_nonempty_block(self, block.next); + let jump_target = next_nonempty_block(self, last.target); + if jump_target == next { + assert!(next != BlockIdx::NULL); + if last.instruction_lineno() == self[next].instructions[0].instruction_lineno() + { + assert_ne!( + last.instruction_lineno(), + self[next].instructions[0].instruction_lineno(), + "redundant jump has same line as fallthrough target" + ); + return false; + } + } + } + current = block.next; } - (ConstantData::Tuple { elements }, ConstantData::Slice { elements: slice }) => { - let indices = adjusted_slice_indices(elements.len(), slice)?; - let mut result = Vec::new(); - result.try_reserve_exact(indices.len()).ok()?; - for index in indices { - result.push(elements[index].clone()); + true + } + + fn remove_redundant_nops_and_jumps(&mut self) -> crate::InternalResult<()> { + loop { + // Convergence is guaranteed because the number of redundant jumps and + // nops only decreases. + let removed_nops = self.remove_redundant_nops(); + let removed_jumps = self.remove_redundant_jumps()?; + if removed_nops + removed_jumps == 0 { + break; } - Some(ConstantData::Tuple { elements: result }) } - _ => None, + Ok(()) + } + + fn blocks_new_block(&mut self) -> crate::InternalResult { + self.try_reserve(1) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + let block_idx = BlockIdx( + self.len() + .to_u32() + .ok_or(InternalError::MalformedControlFlowGraph)?, + ); + self.push(Block::default()); + Ok(block_idx) } } -/// flowgraph.c eval_const_binop bool/int coercion -fn constant_as_int(value: &ConstantData) -> Option<(BigInt, bool)> { - match value { - ConstantData::Boolean { value } => Some((BigInt::from(u8::from(*value)), true)), - ConstantData::Integer { value } => Some((value.clone(), false)), - _ => None, +impl From<[Block; N]> for Blocks { + fn from(value: [Block; N]) -> Self { + Self(value.into()) } } -/// flowgraph.c eval_const_binop -fn eval_const_binop( - left: &ConstantData, - right: &ConstantData, - op: oparg::BinaryOperator, -) -> Option { - use oparg::BinaryOperator as BinOp; +impl Deref for Blocks { + type Target = [Block]; - if matches!(op, BinOp::Subscr) { - return eval_const_subscript(left, right); + fn deref(&self) -> &Self::Target { + &self.0 } +} - if let (Some((left_int, left_is_bool)), Some((right_int, right_is_bool))) = - (constant_as_int(left), constant_as_int(right)) - && (left_is_bool || right_is_bool) - { - if left_is_bool && right_is_bool { - match op { - BinOp::And => { - return Some(ConstantData::Boolean { - value: !left_int.is_zero() & !right_int.is_zero(), - }); - } - BinOp::Or => { - return Some(ConstantData::Boolean { - value: !left_int.is_zero() | !right_int.is_zero(), - }); - } - BinOp::Xor => { - return Some(ConstantData::Boolean { - value: !left_int.is_zero() ^ !right_int.is_zero(), - }); - } - _ => {} - } - } +impl DerefMut for Blocks { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} - return eval_const_binop( - &ConstantData::Integer { value: left_int }, - &ConstantData::Integer { value: right_int }, - op, - ); +impl Index for Blocks { + type Output = Block; + + fn index(&self, idx: usize) -> &Self::Output { + &self.0[idx] } +} - match (left, right) { - (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { - let result = match op { - BinOp::Add => l + r, - BinOp::Subtract => l - r, - BinOp::Multiply => { - return const_folding_safe_multiply(left, right); - } - BinOp::TrueDivide => { - if r.is_zero() { - return None; - } - let l_f = l.to_f64()?; - let r_f = r.to_f64()?; - let result = l_f / r_f; - if !result.is_finite() { - return None; - } - return Some(ConstantData::Float { value: result }); - } - BinOp::FloorDivide => { - if r.is_zero() { - return None; - } - // Python floor division: round towards negative infinity - let (q, rem) = (l.clone() / r.clone(), l.clone() % r.clone()); - if !rem.is_zero() && (rem < BigInt::from(0)) != (*r < BigInt::from(0)) { - q - 1 - } else { - q - } - } - BinOp::Remainder => return const_folding_safe_mod(left, right), - BinOp::Power => return const_folding_safe_power(left, right), - BinOp::Lshift => return const_folding_safe_lshift(left, right), - BinOp::Rshift => { - let shift: u32 = r.try_into().ok()?; - l >> (shift as usize) - } - BinOp::And => l & r, - BinOp::Or => l | r, - BinOp::Xor => l ^ r, - _ => return None, - }; - Some(ConstantData::Integer { value: result }) - } - (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { - let result = match op { - BinOp::Add => l + r, - BinOp::Subtract => l - r, - BinOp::Multiply => return const_folding_safe_multiply(left, right), - BinOp::TrueDivide => { - if *r == 0.0 { - return None; - } - l / r - } - BinOp::FloorDivide => { - let (floordiv, _) = float_div_mod(*l, *r)?; - floordiv - } - BinOp::Remainder => return const_folding_safe_mod(left, right), - BinOp::Power => return const_folding_safe_power(left, right), - _ => return None, - }; - if matches!(op, BinOp::Power) && !result.is_finite() { - return None; - } - Some(ConstantData::Float { value: result }) - } - // Int op Float or Float op Int → Float - (ConstantData::Integer { value: l }, ConstantData::Float { value: r }) => { - let l_f = l.to_f64()?; - eval_const_binop( - &ConstantData::Float { value: l_f }, - &ConstantData::Float { value: *r }, - op, - ) - } - (ConstantData::Float { value: l }, ConstantData::Integer { value: r }) => { - let r_f = r.to_f64()?; - eval_const_binop( - &ConstantData::Float { value: *l }, - &ConstantData::Float { value: r_f }, - op, - ) - } - (ConstantData::Integer { value: l }, ConstantData::Complex { value: r }) => { - eval_const_complex_binop(Complex::new(l.to_f64()?, 0.0), *r, op) - } - (ConstantData::Complex { value: l }, ConstantData::Integer { value: r }) => { - eval_const_complex_binop(*l, Complex::new(r.to_f64()?, 0.0), op) - } - (ConstantData::Float { value: l }, ConstantData::Complex { value: r }) => { - eval_const_complex_binop(Complex::new(*l, 0.0), *r, op) - } - (ConstantData::Complex { value: l }, ConstantData::Float { value: r }) => { - eval_const_complex_binop(*l, Complex::new(*r, 0.0), op) - } - (ConstantData::Complex { value: l }, ConstantData::Complex { value: r }) => { - eval_const_complex_binop(*l, *r, op) - } - // String concatenation and repetition - (ConstantData::Str { value: l }, ConstantData::Str { value: r }) - if matches!(op, BinOp::Add) => - { - let mut result = Wtf8Buf::new(); - result - .try_reserve_exact(l.len().checked_add(r.len())?) - .ok()?; - result.push_wtf8(l); - result.push_wtf8(r); - Some(ConstantData::Str { value: result }) - } - (ConstantData::Str { .. }, ConstantData::Integer { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) - } - (ConstantData::Tuple { elements: l }, ConstantData::Tuple { elements: r }) - if matches!(op, BinOp::Add) => - { - let mut result = Vec::new(); - result - .try_reserve_exact(l.len().checked_add(r.len())?) - .ok()?; - result.extend(l.iter().cloned()); - result.extend(r.iter().cloned()); - Some(ConstantData::Tuple { elements: result }) - } - (ConstantData::Tuple { .. }, ConstantData::Integer { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) - } - (ConstantData::Integer { .. }, ConstantData::Tuple { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) - } - (ConstantData::Integer { .. }, ConstantData::Str { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) +impl IndexMut for Blocks { + fn index_mut(&mut self, idx: usize) -> &mut Self::Output { + &mut self.0[idx] + } +} + +impl Index for Blocks { + type Output = Block; + + fn index(&self, block_idx: BlockIdx) -> &Self::Output { + &self.0[block_idx.as_usize()] + } +} + +impl IndexMut for Blocks { + fn index_mut(&mut self, block_idx: BlockIdx) -> &mut Self::Output { + &mut self.0[block_idx.as_usize()] + } +} + +pub(crate) const START_DEPTH_UNSET: i32 = i32::MIN; +const CO_MAXBLOCKS: usize = 21; + +/// flowgraph.c struct _PyCfgExceptStack +#[derive(Clone, Debug)] +struct CfgExceptStack { + handlers: [BlockIdx; CO_MAXBLOCKS + 2], + depth: usize, +} + +/// flowgraph.c `basicblock **stack` +#[derive(Clone, Debug)] +struct CfgTraversalStack { + stack: Vec, + sp: usize, +} + +impl CfgTraversalStack { + fn push(&mut self, block: BlockIdx) { + debug_assert!(self.sp < self.stack.len()); + self.stack[self.sp] = block; + self.sp += 1; + } + + fn pop(&mut self) -> Option { + if self.sp == 0 { + return None; } - (ConstantData::Bytes { value: l }, ConstantData::Bytes { value: r }) - if matches!(op, BinOp::Add) => - { - let mut result = Vec::new(); - result - .try_reserve_exact(l.len().checked_add(r.len())?) - .ok()?; - result.extend_from_slice(l); - result.extend_from_slice(r); - Some(ConstantData::Bytes { value: result }) + self.sp -= 1; + Some(self.stack[self.sp]) + } + + fn capacity(&self) -> usize { + self.stack.len() + } +} + +#[derive(Clone, Debug)] +pub(crate) struct InstructionSequenceLabelMap { + block_labels: Vec, + /// Codegen-side shadow of the instruction-sequence label map. + /// + /// `_PyInstructionSequence_UseLabel()` can map multiple labels to the same + /// instruction offset before `_PyCfg_FromInstructionSequence()` materializes + /// CFG blocks. The codegen CFG path keeps the same aliasing by resolving + /// those labels to the block that owns the shared offset. + cpython_block_by_label: Vec, +} + +fn instruction_sequence_label_map_register_label( + map: &mut InstructionSequenceLabelMap, + label: InstructionSequenceLabel, +) -> crate::InternalResult<()> { + debug_assert!(is_label(label)); + let old_size = map.cpython_block_by_label.len(); + let new_allocation = c_array_ensure_capacity::( + old_size, + label.idx(), + INITIAL_INSTR_SEQUENCE_LABELS_MAP_SIZE, + )?; + if new_allocation > old_size { + if new_allocation > map.cpython_block_by_label.capacity() { + map.cpython_block_by_label + .try_reserve_exact(new_allocation - map.cpython_block_by_label.capacity()) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; } - (ConstantData::Bytes { .. }, ConstantData::Integer { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) + map.cpython_block_by_label + .resize(new_allocation, BlockIdx::NULL); + for i in old_size..map.cpython_block_by_label.len() { + map.cpython_block_by_label[i] = BlockIdx::NULL; } - (ConstantData::Integer { .. }, ConstantData::Bytes { .. }) - if matches!(op, BinOp::Multiply) => - { - const_folding_safe_multiply(left, right) + } + debug_assert!(map.cpython_block_by_label.len() > label.idx()); + Ok(()) +} + +fn instruction_sequence_label_map_ensure_label_for_block( + map: &mut InstructionSequenceLabelMap, + seq: &mut InstructionSequence, + block: BlockIdx, +) -> crate::InternalResult { + debug_assert_ne!(block, BlockIdx::NULL); + let block_label = map.block_labels[block.idx()]; + if is_label(block_label) { + return Ok(block_label); + } + let label = instruction_sequence_new_label(seq); + debug_assert_eq!(label.0, seq.next_free_label); + instruction_sequence_label_map_register_label(map, label)?; + map.cpython_block_by_label[label.idx()] = block; + map.block_labels[block.idx()] = label; + Ok(label) +} + +fn instruction_sequence_label_map_label_for_block( + map: &InstructionSequenceLabelMap, + block: BlockIdx, +) -> InstructionSequenceLabel { + debug_assert_ne!(block, BlockIdx::NULL); + map.block_labels + .get(block.idx()) + .copied() + .unwrap_or(InstructionSequenceLabel::NO_LABEL) +} + +fn instruction_sequence_label_map_block_for_label( + map: &InstructionSequenceLabelMap, + label: InstructionSequenceLabel, +) -> Option { + if !is_label(label) { + return None; + } + map.cpython_block_by_label + .get(label.idx()) + .copied() + .filter(|&block| block != BlockIdx::NULL) +} + +fn instruction_sequence_label_map_resolve_label( + map: &InstructionSequenceLabelMap, + block: BlockIdx, +) -> BlockIdx { + if block == BlockIdx::NULL { + return BlockIdx::NULL; + } + let label = instruction_sequence_label_map_label_for_block(map, block); + if !is_label(label) { + return block; + } + instruction_sequence_label_map_block_for_label(map, label).unwrap_or_else(|| { + debug_assert!( + false, + "CPython instruction-sequence label must map to a codegen CFG block" + ); + BlockIdx::NULL + }) +} + +fn instruction_sequence_label_map_resolve_label_to_block( + map: &InstructionSequenceLabelMap, + label: InstructionSequenceLabel, +) -> BlockIdx { + if !is_label(label) { + return BlockIdx::NULL; + } + instruction_sequence_label_map_block_for_label(map, label).unwrap_or_else(|| { + debug_assert!( + false, + "CPython instruction-sequence label must map to a codegen CFG block" + ); + BlockIdx::NULL + }) +} + +fn instruction_sequence_label_oparg(label: InstructionSequenceLabel) -> OpArg { + debug_assert!(is_label(label)); + OpArg::new(label.idx() as u32) +} + +fn instruction_sequence_label_map_use_label_at_block( + map: &mut InstructionSequenceLabelMap, + seq: &mut InstructionSequence, + from: BlockIdx, + to: BlockIdx, +) -> crate::InternalResult<()> { + if from == BlockIdx::NULL || from == to { + return Ok(()); + } + let from_label = instruction_sequence_label_map_ensure_label_for_block(map, seq, from)?; + debug_assert!(map.cpython_block_by_label.len() > from_label.idx()); + let to_block = instruction_sequence_label_map_resolve_label(map, to); + if to_block == BlockIdx::NULL { + debug_assert!( + false, + "CPython label target must map to a codegen CFG block" + ); + return Ok(()); + } + map.cpython_block_by_label[from_label.idx()] = to_block; + Ok(()) +} + +fn instruction_sequence_label_map_push_unlabeled_block( + map: &mut InstructionSequenceLabelMap, +) -> crate::InternalResult<()> { + map.block_labels + .try_reserve(1) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + map.block_labels.push(InstructionSequenceLabel::NO_LABEL); + Ok(()) +} + +fn instruction_sequence_label_map_push_unmapped_label( + map: &mut InstructionSequenceLabelMap, + seq: &mut InstructionSequence, +) -> crate::InternalResult<()> { + let label = instruction_sequence_new_label(seq); + debug_assert_eq!(label.0, seq.next_free_label); + instruction_sequence_label_map_register_label(map, label)?; + let block = BlockIdx( + map.block_labels + .len() + .to_u32() + .ok_or(InternalError::MalformedControlFlowGraph)?, + ); + map.cpython_block_by_label[label.idx()] = block; + map.block_labels + .try_reserve(1) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + map.block_labels.push(label); + Ok(()) +} + +impl InstructionSequenceLabelMap { + pub(crate) fn new() -> Self { + Self { + block_labels: vec![InstructionSequenceLabel::NO_LABEL], + cpython_block_by_label: Vec::new(), } - _ => None, } } -/// flowgraph.c fold_tuple_of_constants -fn fold_tuple_of_constants( - metadata: &mut CodeUnitMetadata, - block: &mut Block, - i: usize, -) -> crate::InternalResult { - let Some(Instruction::BuildTuple { .. }) = block.instructions[i].instr.real() else { - return Ok(false); - }; +pub struct CodeInfo { + pub flags: CodeFlags, + pub source_path: String, + pub private: Option, // For private name mangling, mostly for class - let tuple_size = u32::from(block.instructions[i].arg) as usize; - if tuple_size > STACK_USE_GUIDELINE { - return Ok(false); - } + pub blocks: Blocks, + pub current_block: BlockIdx, + pub(crate) instr_sequence: InstructionSequence, + pub(crate) instr_sequence_label_map: InstructionSequenceLabelMap, + pub(crate) annotations_instr_sequence: Option, - let Some(operand_indices) = (if tuple_size == 0 { - Some(Vec::new()) - } else if let Some(start) = i.checked_sub(1) { - get_const_loading_instrs(block, start, tuple_size)? - } else { - None - }) else { - return Ok(false); - }; + pub metadata: CodeUnitMetadata, - let mut elements = Vec::new(); - elements - .try_reserve_exact(tuple_size) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - for &j in &operand_indices { - let Some(element) = get_const_value(metadata, &block.instructions[j]) else { - return Ok(false); - }; - elements.push(element); - } + // For class scopes: attributes accessed via self.X + pub static_attributes: Option>, - nop_out(block, &operand_indices); - instr_make_load_const( - metadata, - &mut block.instructions[i], - ConstantData::Tuple { elements }, - )?; - Ok(true) -} + // True if compiling an inlined comprehension + pub in_inlined_comp: bool, -fn fold_constant_intrinsic_list_to_tuple( - metadata: &mut CodeUnitMetadata, - block: &mut Block, - i: usize, -) -> crate::InternalResult { - let Some(Instruction::CallIntrinsic1 { func }) = block.instructions[i].instr.real() else { - return Ok(false); - }; - if func.get(block.instructions[i].arg) != IntrinsicFunction1::ListToTuple { - return Ok(false); - } + // Block stack for tracking nested control structures + pub fblock: Vec, - let mut consts_found = 0usize; - let mut expect_append = true; - let mut pos = i; - while let Some(prev) = pos.checked_sub(1) { - pos = prev; - let instr = &block.instructions[pos]; - if matches!(instr.instr.real(), Some(Instruction::Nop)) { - continue; - } + // Reference to the symbol table for this scope + pub symbol_table_index: usize, + // compile.c uses PyList_GET_SIZE(u->u_ste->ste_varnames) + // when calling flowgraph.c _PyCfg_OptimizeCodeUnit(). + pub nparams: usize, - if matches!(instr.instr.real(), Some(Instruction::BuildList { .. })) - && u32::from(instr.arg) == 0 - { - if !expect_append { - return Ok(false); - } + // PEP 649: Track nesting depth inside conditional blocks (if/for/while/etc.) + // u_in_conditional_block + pub in_conditional_block: u32, - let mut elements = Vec::new(); - elements - .try_reserve_exact(consts_found) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - for idx in (pos..i).rev() { - if matches!(block.instructions[idx].instr.real(), Some(Instruction::Nop)) { - continue; - } - if loads_const(&block.instructions[idx]) { - let Some(value) = get_const_value(metadata, &block.instructions[idx]) else { - return Ok(false); - }; - elements.push(value); - } - nop_out_no_location(&mut block.instructions[idx]); - } - debug_assert_eq!(elements.len(), consts_found); - elements.reverse(); - instr_make_load_const( - metadata, - &mut block.instructions[i], - ConstantData::Tuple { elements }, + // PEP 649: Next index for conditional annotation tracking + // u_next_conditional_annotation_index + pub next_conditional_annotation_index: u32, +} + +impl CodeInfo { + pub(crate) fn addop_to_instr_sequence( + &mut self, + mut info: InstructionInfo, + ) -> crate::InternalResult<()> { + if info.instr.has_target() && info.target != BlockIdx::NULL { + let label = instruction_sequence_label_map_ensure_label_for_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + info.target, )?; - return Ok(true); + info.arg = instruction_sequence_label_oparg(label); + info.target = BlockIdx::NULL; } + instruction_sequence_addop(&mut self.instr_sequence, info)?; + Ok(()) + } - if expect_append { - if !matches!(instr.instr.real(), Some(Instruction::ListAppend { .. })) - || u32::from(instr.arg) != 1 - { - return Ok(false); - } - } else { - if !loads_const(instr) { - return Ok(false); - } - consts_found += 1; + pub(crate) fn addop_to_instr_sequence_with_target_label( + &mut self, + mut info: InstructionInfo, + target_label: InstructionSequenceLabel, + ) -> crate::InternalResult<()> { + if !info.instr.has_target() { + return Err(InternalError::MalformedControlFlowGraph); } - expect_append = !expect_append; + info.arg = instruction_sequence_label_oparg(target_label); + info.target = BlockIdx::NULL; + instruction_sequence_addop(&mut self.instr_sequence, info)?; + Ok(()) } - Ok(false) -} - -/// Port of CPython's flowgraph.c optimize_lists_and_sets(). -fn optimize_lists_and_sets( - metadata: &mut CodeUnitMetadata, - block: &mut Block, - i: usize, - nextop: Option, -) -> crate::InternalResult { - let Some(instr) = block.instructions[i].instr.real() else { - return Ok(false); - }; - let is_list = matches!(instr, Instruction::BuildList { .. }); - let is_set = matches!(instr, Instruction::BuildSet { .. }); - if !is_list && !is_set { - return Ok(false); + pub(crate) fn addop_to_current_block( + &mut self, + info: InstructionInfo, + ) -> crate::InternalResult<()> { + self.blocks[self.current_block].basicblock_addop(info) } - let contains_or_iter = matches!( - nextop, - Some(Instruction::GetIter | Instruction::ContainsOp { .. }) - ); - let seq_size = u32::from(block.instructions[i].arg) as usize; - if seq_size > STACK_USE_GUIDELINE || (seq_size < MIN_CONST_SEQUENCE_SIZE && !contains_or_iter) { - return Ok(false); + pub(crate) fn last_current_block_instr_mut(&mut self) -> Option<&mut InstructionInfo> { + self.blocks[self.current_block].basicblock_last_instr_mut() } - let Some(operand_indices) = (if seq_size == 0 { - Some(Vec::new()) - } else if let Some(start) = i.checked_sub(1) { - get_const_loading_instrs(block, start, seq_size)? - } else { - None - }) else { - if contains_or_iter && is_list { - let arg = block.instructions[i].arg; - instr_set_op1(&mut block.instructions[i], Opcode::BuildTuple.into(), arg); - return Ok(true); + pub(crate) fn set_last_instr_sequence_lineno_override(&mut self, lineno_override: i32) { + if let Some(last) = instruction_sequence_last_info_mut(&mut self.instr_sequence) { + last.lineno_override = Some(lineno_override); } - return Ok(false); - }; - - let mut elements = Vec::new(); - elements - .try_reserve_exact(seq_size) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - for &j in &operand_indices { - let Some(element) = get_const_value(metadata, &block.instructions[j]) else { - return Ok(false); - }; - elements.push(element); } - let const_data = if is_list { - ConstantData::Tuple { elements } - } else { - ConstantData::Frozenset { elements } - }; - let const_idx = add_const(metadata, const_data)?; - - if !contains_or_iter { - debug_assert!(i >= 2); - let folded_loc = block.instructions[i].location; - let end_loc = block.instructions[i].end_location; - - nop_out(block, &operand_indices); - - let build_instr = if is_list { - Instruction::BuildList { - count: Arg::marker(), - } - .into() - } else { - Instruction::BuildSet { - count: Arg::marker(), - } - .into() - }; - instr_set_op1(&mut block.instructions[i - 2], build_instr, OpArg::new(0)); - block.instructions[i - 2].location = folded_loc; - block.instructions[i - 2].end_location = end_loc; - block.instructions[i - 2].lineno_override = None; - - instr_set_op1( - &mut block.instructions[i - 1], - Instruction::LoadConst { - consti: Arg::marker(), - } - .into(), - OpArg::new(const_idx as u32), - ); + pub(crate) fn use_instr_sequence_label( + &mut self, + block: BlockIdx, + ) -> crate::InternalResult<()> { + let label = instruction_sequence_label_map_ensure_label_for_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + block, + )?; + instruction_sequence_use_label(&mut self.instr_sequence, label) + } - let extend_instr = if is_list { - Opcode::ListExtend - } else { - Opcode::SetUpdate - }; - instr_set_op1( - &mut block.instructions[i], - extend_instr.into(), - OpArg::new(1), - ); - return Ok(true); + pub(crate) fn new_instr_sequence_label(&mut self) -> InstructionSequenceLabel { + instruction_sequence_new_label(&mut self.instr_sequence) } - nop_out(block, &operand_indices); + pub(crate) fn use_raw_instr_sequence_label( + &mut self, + label: InstructionSequenceLabel, + ) -> crate::InternalResult<()> { + instruction_sequence_use_label(&mut self.instr_sequence, label) + } - instr_set_op1( - &mut block.instructions[i], - Instruction::LoadConst { - consti: Arg::marker(), - } - .into(), - OpArg::new(const_idx as u32), - ); - Ok(true) -} + pub(crate) fn mark_cpython_cfg_label(&mut self, block: BlockIdx) -> crate::InternalResult<()> { + let label = instruction_sequence_label_map_ensure_label_for_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + block, + )?; + self.blocks[block].cpython_label = label; + Ok(()) + } -/// flowgraph.c VISITED -const VISITED: i32 = -1; + pub(crate) fn resolve_instr_sequence_label(&self, block: BlockIdx) -> BlockIdx { + instruction_sequence_label_map_resolve_label(&self.instr_sequence_label_map, block) + } -/// flowgraph.c SWAPPABLE -fn is_swappable(instr: &AnyInstruction) -> bool { - matches!( - (*instr).into(), - AnyOpcode::Real(Opcode::StoreFast | Opcode::PopTop) - | AnyOpcode::Pseudo(PseudoOpcode::StoreFastMaybeNull) - ) -} + pub(crate) fn block_for_instr_sequence_label( + &self, + label: InstructionSequenceLabel, + ) -> BlockIdx { + instruction_sequence_label_map_resolve_label_to_block(&self.instr_sequence_label_map, label) + } -/// flowgraph.c STORES_TO -fn stores_to(info: &InstructionInfo) -> i32 { - match info.instr.into() { - AnyOpcode::Real(Opcode::StoreFast) - | AnyOpcode::Pseudo(PseudoOpcode::StoreFastMaybeNull) => u32::from(info.arg) as i32, - _ => -1, + pub(crate) fn use_instr_sequence_label_at_block( + &mut self, + from: BlockIdx, + to: BlockIdx, + ) -> crate::InternalResult<()> { + instruction_sequence_label_map_use_label_at_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + from, + to, + ) } -} -/// flowgraph.c next_swappable_instruction -fn next_swappable_instruction(block: &Block, mut i: usize, lineno: i32) -> Option { - loop { - i += 1; - if i >= block.instruction_used { - return None; - } - let info = &block.instructions[i]; - let info_lineno = instruction_lineno(info); - if lineno >= 0 && info_lineno != lineno { - return None; - } - if matches!(info.instr, AnyInstruction::Real(Instruction::Nop)) { - continue; - } - if is_swappable(&info.instr) { - return Some(i); + pub(crate) fn instr_sequence_label_for_block( + &mut self, + block: BlockIdx, + ) -> crate::InternalResult { + if block == BlockIdx::NULL { + Ok(InstructionSequenceLabel::NO_LABEL) + } else { + instruction_sequence_label_map_ensure_label_for_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + block, + ) } - return None; } -} -/// flowgraph.c swaptimize -fn swaptimize(block: &mut Block, ix: &mut usize) -> crate::InternalResult<()> { - debug_assert!(matches!( - block.instructions[*ix].instr.real(), - Some(Instruction::Swap { .. }) - )); - let mut depth = u32::from(block.instructions[*ix].arg) as usize; - let mut len = 1usize; - let mut more = false; - let limit = block.instruction_used - *ix; - while len < limit { - match block.instructions[*ix + len].instr.real() { - Some(Instruction::Swap { .. }) => { - depth = depth.max(u32::from(block.instructions[*ix + len].arg) as usize); - more = true; - len += 1; - } - Some(Instruction::Nop) => { - len += 1; - } - _ => break, - } + pub(crate) fn insert_start_setup_cleanup( + &mut self, + handler_block: BlockIdx, + ) -> crate::InternalResult<()> { + let handler_label = instruction_sequence_label_map_ensure_label_for_block( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + handler_block, + )?; + instruction_sequence_insert_instruction( + &mut self.instr_sequence, + 0, + InstructionInfo { + instr: PseudoOpcode::SetupCleanup.into(), + arg: instruction_sequence_label_oparg(handler_label), + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: Some(NO_LOCATION_OVERRIDE), + }, + ) } - if !more { - return Ok(()); + pub(crate) fn push_unmapped_instr_sequence_label(&mut self) -> crate::InternalResult<()> { + instruction_sequence_label_map_push_unmapped_label( + &mut self.instr_sequence_label_map, + &mut self.instr_sequence, + ) } - let mut stack = Vec::new(); - stack - .try_reserve_exact(depth) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - stack.resize(depth, 0); - let mut i = 0; - while i < depth { - stack[i] = i as i32; - i += 1; + pub(crate) fn push_unlabeled_instr_sequence_block(&mut self) -> crate::InternalResult<()> { + instruction_sequence_label_map_push_unlabeled_block(&mut self.instr_sequence_label_map) } - i = 0; - while i < len { - let info = &block.instructions[*ix + i]; - if matches!(info.instr.real(), Some(Instruction::Swap { .. })) { - let oparg = u32::from(info.arg) as usize; - stack.swap(0, oparg - 1); + fn take_recorded_instr_sequence(&mut self) -> InstructionSequence { + let mut instr_sequence = + core::mem::replace(&mut self.instr_sequence, instruction_sequence_new()); + if let Some(mut annotations_instr_sequence) = self.annotations_instr_sequence.take() { + instruction_sequence_apply_label_map(&mut annotations_instr_sequence); + instruction_sequence_set_annotations_code( + &mut instr_sequence, + Some(Box::new(annotations_instr_sequence)), + ); } - i += 1; + + instr_sequence } - let mut current = len as isize - 1; - for i in 0..depth { - if stack[i] == VISITED || stack[i] == i as i32 { - continue; - } - let mut j = i; - loop { - if j != 0 { - debug_assert!(current >= 0); - let out = &mut block.instructions[*ix + current as usize]; - out.instr = Opcode::Swap.into(); - out.arg = OpArg::new((j + 1) as u32); - current -= 1; - } - if stack[j] == VISITED { - debug_assert_eq!(j, i); - break; - } - let next_j = stack[j] as usize; - stack[j] = VISITED; - j = next_j; - } + fn prepare_cfg_from_codegen(&mut self) -> InstructionSequence { + // compile.c optimize_and_assemble_code_unit passes + // u_instr_sequence directly into flowgraph.c _PyCfg_FromInstructionSequence(). + self.take_recorded_instr_sequence() } +} + +fn optimize_code_unit( + metadata: &mut CodeUnitMetadata, + blocks: &mut Blocks, + instr_sequence: InstructionSequence, + nlocals: usize, + nparams: usize, +) -> crate::InternalResult<()> { + // Phase 1: _PyCfg_OptimizeCodeUnit (flowgraph.c) + *blocks = cfg_from_instruction_sequence(instr_sequence)?; + translate_jump_labels_to_targets(blocks)?; + blocks.mark_except_handlers(); + label_exception_targets(blocks)?; + optimize_cfg(metadata, blocks, metadata.firstlineno)?; + blocks.remove_unused_consts(&mut metadata.consts)?; + add_checks_for_loads_of_uninitialized_variables(blocks, nlocals, nparams)?; + // Superinstructions are inserted in _PyCfg_OptimizeCodeUnit, before + // later jump normalization / block reordering can create adjacencies + // that never exist at this stage in flowgraph.c. + blocks.insert_superinstructions(); + blocks.push_cold_blocks_to_end()?; + // Line numbers are resolved again after cold-block extraction. + blocks.resolve_line_numbers(metadata.firstlineno)?; + Ok(()) +} - while current >= 0 { - set_to_nop(&mut block.instructions[*ix + current as usize]); - current -= 1; +fn optimize_cfg( + metadata: &mut CodeUnitMetadata, + blocks: &mut Blocks, + firstlineno: OneIndexed, +) -> crate::InternalResult<()> { + // flowgraph.c optimize_cfg + // optimize_cfg() starts with check_cfg() and raises + // SystemError if a jump or scope exit is not the last instruction in + // its block. + blocks.check_cfg()?; + blocks.inline_small_or_no_lineno_blocks()?; + // The instruction-sequence label-map/CFG conversion is not re-run + // after this point. Unreferenced label blocks left by jump inlining + // remain block boundaries and can preserve line-marker NOPs. + blocks.remove_unreachable()?; + // optimize_cfg resolves line numbers before local checks and + // superinstruction insertion, so fusion decisions see propagated + // source locations. + blocks.resolve_line_numbers(firstlineno)?; + // optimize_cfg() runs optimize_load_const() and then + // optimize_basic_block() after line numbers are resolved. + optimize_load_const(metadata, blocks)?; + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next_block = blocks[block_idx].next; + blocks.optimize_basic_block(metadata, block_idx)?; + block_idx = next_block; } - *ix += len - 1; + blocks.remove_redundant_nops_and_pairs(); + // optimize_cfg() removes newly-unreachable blocks and + // redundant NOP/jump chains before _PyCfg_OptimizeCodeUnit() prunes + // unused constants. + blocks.remove_unreachable()?; + blocks.remove_redundant_nops_and_jumps()?; + #[cfg(debug_assertions)] + assert!(blocks.no_redundant_jumps()); Ok(()) } -/// flowgraph.c apply_static_swaps -fn apply_static_swaps(block: &mut Block, mut i: isize) { - while i >= 0 { - let idx = i as usize; - debug_assert!(idx < block.instruction_used); - let swap_arg = match block.instructions[idx].instr.real() { - Some(Instruction::Swap { .. }) => u32::from(block.instructions[idx].arg), - Some(Instruction::Nop | Instruction::PopTop | Instruction::StoreFast { .. }) => { - i -= 1; - continue; - } - _ if matches!( - block.instructions[idx].instr.pseudo(), - Some(PseudoInstruction::StoreFastMaybeNull { .. }) - ) => - { - i -= 1; - continue; - } - _ => return, - }; +fn optimized_cfg_to_instruction_sequence( + metadata: &CodeUnitMetadata, + flags: CodeFlags, + blocks: &mut Blocks, +) -> crate::InternalResult<(u32, usize, InstructionSequence)> { + // Phase 2: _PyCfg_OptimizedCfgToInstructionSequence (flowgraph.c) + blocks.convert_pseudo_conditional_jumps()?; + let max_stackdepth = blocks.calculate_stackdepth()?; + debug_assert!(!is_generator(flags) || max_stackdepth != 0); + let nlocalsplus = prepare_localsplus(metadata, blocks, flags)?; + // Pseudo ops are lowered after stackdepth and + // localsplus preparation, before normalize_jumps. + convert_pseudo_ops(blocks)?; + blocks.normalize_jumps()?; + #[cfg(debug_assertions)] + assert!(blocks.no_redundant_jumps()); + // optimize_load_fast: after normalize_jumps + blocks.optimize_load_fast()?; - let Some(j) = next_swappable_instruction(block, idx, -1) else { - return; - }; - let lineno = instruction_lineno(&block.instructions[j]); - let mut k = j; - for _ in 1..swap_arg { - let Some(next) = next_swappable_instruction(block, k, lineno) else { - return; - }; - k = next; - } + let mut instr_sequence = instruction_sequence_new(); + blocks.cfg_to_instruction_sequence(&mut instr_sequence)?; + Ok((max_stackdepth, nlocalsplus, instr_sequence)) +} - let store_j = stores_to(&block.instructions[j]); - let store_k = stores_to(&block.instructions[k]); - if store_j >= 0 || store_k >= 0 { - if store_j == store_k { - return; - } - let mut idx = j + 1; - while idx < k { - let store_idx = stores_to(&block.instructions[idx]); - if store_idx >= 0 && (store_idx == store_j || store_idx == store_k) { - return; - } - idx += 1; - } - } +impl CodeInfo { + pub fn finalize_code( + mut self, + opts: &crate::compile::CompileOpts, + ) -> crate::InternalResult { + let instr_sequence = self.prepare_cfg_from_codegen(); + let nlocals = self.metadata.varnames.len(); + let nparams = self.nparams; + optimize_code_unit( + &mut self.metadata, + &mut self.blocks, + instr_sequence, + nlocals, + nparams, + )?; + let (max_stackdepth, nlocalsplus, mut instr_sequence) = + optimized_cfg_to_instruction_sequence(&self.metadata, self.flags, &mut self.blocks)?; + let localsplusinfo = compute_localsplus_info(&self.metadata, nlocalsplus, self.flags)?; - set_to_nop(&mut block.instructions[idx]); - block.instructions.swap(j, k); - i -= 1; - } -} + let Self { + flags, + source_path, + private: _, // private is only used during compilation -/// flowgraph.c optimize_basic_block swap pass -fn apply_static_swaps_block(block: &mut Block) -> crate::InternalResult<()> { - let mut i = 0; - while i < block.instruction_used { - if matches!( - block.instructions[i].instr.real(), - Some(Instruction::Swap { .. }) - ) { - swaptimize(block, &mut i)?; - apply_static_swaps(block, i as isize); - } - i += 1; + blocks: _, + current_block: _, + instr_sequence: _, + instr_sequence_label_map: _, + annotations_instr_sequence: _, + metadata, + static_attributes: _, + in_inlined_comp: _, + fblock: _, + symbol_table_index: _, + nparams: _, + in_conditional_block: _, + next_conditional_annotation_index: _, + } = self; + + let CodeUnitMetadata { + name: obj_name, + qualname, + consts: constants, + names: name_cache, + varnames: varname_cache, + cellvars: _, + freevars: freevar_cache, + fast_hidden: _, + fast_hidden_final: _, + argcount: arg_count, + posonlyargcount: posonlyarg_count, + kwonlyargcount: kwonlyarg_count, + firstlineno: first_line_number, + } = metadata; + let code_arg_count = posonlyarg_count + .checked_add(arg_count) + .ok_or(InternalError::MalformedControlFlowGraph)?; + + resolve_unconditional_jumps(&mut instr_sequence); + resolve_jump_offsets(&mut instr_sequence); + let assembled = assemble_emit( + &mut instr_sequence, + first_line_number.get() as i32, + opts.debug_ranges, + )?; + let locations = rustpython_compiler_core::marshal::linetable_to_locations( + &assembled.linetable, + first_line_number.get() as i32, + assembled.instructions.len(), + ); + + Ok(CodeObject { + flags, + posonlyarg_count, + arg_count: code_arg_count, + kwonlyarg_count, + source_path, + first_line_number: Some(first_line_number), + obj_name: obj_name.clone(), + qualname: qualname.unwrap_or(obj_name), + + max_stackdepth, + instructions: CodeUnits::from(assembled.instructions), + locations, + constants: constants.into_iter().collect(), + names: name_cache.into_iter().collect(), + varnames: varname_cache.into_iter().collect(), + cellvars: localsplusinfo.cellvars, + freevars: freevar_cache.into_iter().collect(), + localspluskinds: localsplusinfo.kinds, + linetable: assembled.linetable, + exceptiontable: assembled.exceptiontable, + }) } - Ok(()) } -/// flowgraph.c maybe_instr_make_load_smallint -fn maybe_instr_make_load_smallint(instr: &mut InstructionInfo, constant: &ConstantData) -> bool { - if let ConstantData::Integer { value } = constant - && let Some(small) = value.to_i32().filter(|v| (0..=255).contains(v)) - { - instr_set_op1(instr, Opcode::LoadSmallInt.into(), OpArg::new(small as u32)); - return true; - } - false +/// flowgraph.c IS_GENERATOR +fn is_generator(flags: CodeFlags) -> bool { + flags.intersects(CodeFlags::GENERATOR | CodeFlags::COROUTINE | CodeFlags::ASYNC_GENERATOR) } -/// flowgraph.c basicblock_optimize_load_const -fn basicblock_optimize_load_const( - metadata: &mut CodeUnitMetadata, - block: &mut Block, +/// flowgraph.c insert_prefix_instructions +fn insert_prefix_instructions( + metadata: &CodeUnitMetadata, + blocks: &mut Blocks, + cellfixedoffsets: &[i32], + nfreevars: usize, + flags: CodeFlags, ) -> crate::InternalResult<()> { - let mut i = 0; - let mut effective_opcode = None; - let mut effective_oparg = OpArg::new(0); - while i < block.instruction_used { - if matches!( - block.instructions[i].instr.real(), - Some(Instruction::LoadConst { .. }) - ) && let Some(constant) = get_const_value(metadata, &block.instructions[i]) - { - maybe_instr_make_load_smallint(&mut block.instructions[i], &constant); - } - - let curr = block.instructions[i]; - let curr_arg = curr.arg; + debug_assert!(!blocks.is_empty()); + let entry = &mut blocks[0]; + let ncellvars = metadata.cellvars.len(); + let firstlineno = metadata.firstlineno; + debug_assert!(firstlineno.get() > 0); - // Only combine if the source is a real instruction. - let Some(curr_instr) = curr.instr.real() else { - i += 1; - continue; + if is_generator(flags) { + let location = SourceLocation { + line: firstlineno, + character_offset: OneIndexed::MIN, }; + entry.basicblock_insert_instruction( + 0, + InstructionInfo { + instr: Instruction::ReturnGenerator.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location, + end_location: location, + except_handler: None, + lineno_override: Some(LINE_ONLY_LOCATION_OVERRIDE), + }, + )?; + entry.basicblock_insert_instruction( + 1, + InstructionInfo { + instr: Instruction::PopTop.into(), + arg: OpArg::new(0), + target: BlockIdx::NULL, + location, + end_location: location, + except_handler: None, + lineno_override: Some(LINE_ONLY_LOCATION_OVERRIDE), + }, + )?; + } - let is_copy_of_load_const = matches!( - (effective_opcode, curr_instr), - (Some(Instruction::LoadConst { .. }), Instruction::Copy { i }) if i.get(curr_arg) == 1 - ); - if !is_copy_of_load_const { - effective_opcode = Some(curr_instr); - effective_oparg = curr_arg; + if ncellvars > 0 { + let nvars = metadata.varnames.len() + ncellvars; + let mut sorted = Vec::new(); + vec_try_reserve_exact(&mut sorted, nvars)?; + sorted.resize(nvars, 0i32); + for i in 0..ncellvars { + sorted[cellfixedoffsets[i] as usize] = i as i32 + 1; } - let Some(const_instr) = effective_opcode else { - i += 1; - continue; - }; - let const_arg = effective_oparg; - - if i + 1 >= block.instruction_used { + let mut ncellsused = 0; + let mut i = 0; + while ncellsused < ncellvars { + let oldindex = sorted[i] - 1; i += 1; - continue; - } - - let next = block.instructions[i + 1]; - let next_arg = next.arg; - - if let Some(is_true) = load_const_truthiness(const_instr, const_arg, metadata) { - let const_jump = match (next.instr.real(), next.instr.pseudo()) { - (_, Some(PseudoInstruction::JumpIfTrue { .. })) => Some((true, false)), - (_, Some(PseudoInstruction::JumpIfFalse { .. })) => Some((false, false)), - (Some(Instruction::PopJumpIfTrue { .. }), _) => Some((true, true)), - (Some(Instruction::PopJumpIfFalse { .. }), _) => Some((false, true)), - _ => None, - }; - if let Some((jump_if_true, pops_condition)) = const_jump { - if pops_condition { - set_to_nop(&mut block.instructions[i]); - } - if is_true == jump_if_true { - block.instructions[i + 1].instr = PseudoInstruction::Jump { - delta: Arg::marker(), - } - .into(); - } else { - set_to_nop(&mut block.instructions[i + 1]); - } - i += 1; + if oldindex == -1 { continue; } + entry.basicblock_insert_instruction( + ncellsused, + InstructionInfo { + instr: Opcode::MakeCell.into(), + arg: OpArg::new(oldindex as u32), + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: Some(NO_LOCATION_OVERRIDE), + }, + )?; + ncellsused += 1; } + } - // The remaining combinations require both instructions to be real. - let Some(next_instr) = next.instr.real() else { - i += 1; - continue; - }; - - if let Instruction::LoadConst { consti } = const_instr { - let constant = &metadata.consts[consti.get(const_arg).as_usize()]; - if matches!(constant, ConstantData::None) - && let Instruction::IsOp { invert } = next_instr - { - let mut jump_idx = i + 2; - if jump_idx >= block.instruction_used { - i += 1; - continue; - } + if nfreevars > 0 { + entry.basicblock_insert_instruction( + 0, + InstructionInfo { + instr: Opcode::CopyFreeVars.into(), + arg: OpArg::new(nfreevars as u32), + target: BlockIdx::NULL, + location: SourceLocation::default(), + end_location: SourceLocation::default(), + except_handler: None, + lineno_override: Some(NO_LOCATION_OVERRIDE), + }, + )?; + } + Ok(()) +} - if matches!( - block.instructions[jump_idx].instr.real(), - Some(Instruction::ToBool) - ) { - set_to_nop(&mut block.instructions[jump_idx]); - jump_idx += 1; - if jump_idx >= block.instruction_used { - i += 1; - continue; - } - } +/// flowgraph.c prepare_localsplus +fn prepare_localsplus( + metadata: &CodeUnitMetadata, + blocks: &mut Blocks, + flags: CodeFlags, +) -> crate::InternalResult { + let nlocals = metadata.varnames.len(); + let ncellvars = metadata.cellvars.len(); + let nfreevars = metadata.freevars.len(); + let int_max = i32::MAX as usize; + debug_assert!(nlocals < int_max); + debug_assert!(ncellvars < int_max); + debug_assert!(nfreevars < int_max); + debug_assert!(int_max - nlocals - ncellvars > 0); + debug_assert!(int_max - nlocals - ncellvars - nfreevars > 0); + let mut nlocalsplus = nlocals + ncellvars + nfreevars; + let mut cellfixedoffsets = build_cellfixedoffsets(metadata)?; - let Some(jump_instr) = block.instructions[jump_idx].instr.real() else { - i += 1; - continue; - }; + // This must be called before fix_cell_offsets(). + insert_prefix_instructions(metadata, blocks, &cellfixedoffsets, nfreevars, flags)?; - let mut invert = matches!( - invert.get(next_arg), - rustpython_compiler_core::bytecode::Invert::Yes - ); - match jump_instr { - Instruction::PopJumpIfFalse { .. } => { - invert = !invert; - } - Instruction::PopJumpIfTrue { .. } => {} - _ => { - i += 1; - continue; - } - }; + let numdropped = fix_cell_offsets(metadata, blocks, &mut cellfixedoffsets); + nlocalsplus -= numdropped; + Ok(nlocalsplus) +} - set_to_nop(&mut block.instructions[i]); - set_to_nop(&mut block.instructions[i + 1]); - block.instructions[jump_idx].instr = if invert { - Instruction::PopJumpIfNotNone { - delta: Arg::marker(), - } - } else { - Instruction::PopJumpIfNone { - delta: Arg::marker(), - } - } - .into(); - i = jump_idx; - continue; - } +/// flowgraph.c eval_const_unaryop +fn eval_const_unaryop( + operand: &ConstantData, + op: Instruction, + intrinsic: Option, +) -> Option { + match (operand, op, intrinsic) { + (ConstantData::Integer { value }, Instruction::UnaryNegative, None) => { + Some(ConstantData::Integer { value: -value }) + } + (ConstantData::Float { value }, Instruction::UnaryNegative, None) => { + Some(ConstantData::Float { value: -value }) + } + (ConstantData::Complex { value }, Instruction::UnaryNegative, None) => { + Some(ConstantData::Complex { value: -value }) + } + (ConstantData::Boolean { value }, Instruction::UnaryNegative, None) => { + Some(ConstantData::Integer { + value: BigInt::from(-i32::from(*value)), + }) + } + (ConstantData::Integer { value }, Instruction::UnaryInvert, None) => { + Some(ConstantData::Integer { value: !value }) } + (ConstantData::Boolean { .. }, Instruction::UnaryInvert, None) => None, + (_, Instruction::UnaryNot, None) => Some(ConstantData::Boolean { + value: !operand.truthiness(), + }), + ( + ConstantData::Integer { value }, + Instruction::CallIntrinsic1 { .. }, + Some(oparg::IntrinsicFunction1::UnaryPositive), + ) => Some(ConstantData::Integer { + value: value.clone(), + }), + ( + ConstantData::Float { value }, + Instruction::CallIntrinsic1 { .. }, + Some(oparg::IntrinsicFunction1::UnaryPositive), + ) => Some(ConstantData::Float { value: *value }), + ( + ConstantData::Boolean { value }, + Instruction::CallIntrinsic1 { .. }, + Some(oparg::IntrinsicFunction1::UnaryPositive), + ) => Some(ConstantData::Integer { + value: BigInt::from(i32::from(*value)), + }), + ( + ConstantData::Complex { value }, + Instruction::CallIntrinsic1 { .. }, + Some(oparg::IntrinsicFunction1::UnaryPositive), + ) => Some(ConstantData::Complex { value: *value }), + _ => None, + } +} - if matches!( - const_instr, - Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. } - ) && matches!(next_instr, Instruction::ToBool) - && let Some(value) = load_const_truthiness(const_instr, const_arg, metadata) - { - let const_idx = add_const(metadata, ConstantData::Boolean { value })?; - set_to_nop(&mut block.instructions[i]); - instr_set_op1( - &mut block.instructions[i + 1], - Instruction::LoadConst { - consti: Arg::marker(), - } - .into(), - OpArg::new(const_idx as u32), - ); - i += 1; - continue; +fn load_const_truthiness( + instr: Instruction, + arg: OpArg, + metadata: &CodeUnitMetadata, +) -> Option { + match instr { + Instruction::LoadConst { consti } => { + let constant = &metadata.consts[consti.get(arg).as_usize()]; + Some(constant.truthiness()) } - - i += 1; + Instruction::LoadSmallInt { i } => Some(i.get(arg) != 0), + _ => None, } - Ok(()) } -/// flowgraph.c optimize_load_const -fn optimize_load_const( +/// flowgraph.c add_const +fn add_const( metadata: &mut CodeUnitMetadata, - blocks: &mut [Block], -) -> crate::InternalResult<()> { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next_block = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx]; - basicblock_optimize_load_const(metadata, block)?; - block_idx = next_block; - } - Ok(()) + constant: ConstantData, +) -> crate::InternalResult { + Ok(metadata.consts.try_insert_full(constant)?.0) } -/// flowgraph.c optimize_basic_block -fn optimize_basic_block( - blocks: &mut [Block], +fn instr_make_load_const( metadata: &mut CodeUnitMetadata, - block_idx: BlockIdx, + instr: &mut InstructionInfo, + constant: ConstantData, ) -> crate::InternalResult<()> { - let bi = block_idx.idx(); - let mut nop = InstructionInfo { - instr: Instruction::Nop.into(), - arg: OpArg::NULL, - target: BlockIdx::NULL, - location: SourceLocation::default(), - end_location: SourceLocation::default(), - except_handler: None, - lineno_override: None, - }; - instr_set_op0(&mut nop, Instruction::Nop.into()); - let mut i = 0; - while i < blocks[bi].instruction_used { - let inst = blocks[bi].instructions[i]; - debug_assert!(!inst.instr.is_assembler()); - let target = if inst.instr.has_target() { - let target = inst.target; - debug_assert!(target != BlockIdx::NULL); - debug_assert!(blocks[target.idx()].instruction_used != 0); - debug_assert!(!blocks[target.idx()].instructions[0].instr.is_assembler()); - blocks[target.idx()].instructions[0] - } else { - nop - }; - - let nextop = blocks[bi] - .instructions - .get(i + 1) - .and_then(|next| next.instr.real()); - - match inst.instr { - AnyInstruction::Real(Instruction::BuildTuple { .. }) => { - let oparg = u32::from(inst.arg); - if matches!(nextop, Some(Instruction::UnpackSequence { .. })) - && u32::from(blocks[bi].instructions[i + 1].arg) == oparg - { - match oparg { - 1 => { - set_to_nop(&mut blocks[bi].instructions[i]); - set_to_nop(&mut blocks[bi].instructions[i + 1]); - i += 1; - continue; - } - 2 | 3 => { - set_to_nop(&mut blocks[bi].instructions[i]); - blocks[bi].instructions[i + 1].instr = - Instruction::Swap { i: Arg::marker() }.into(); - i += 1; - continue; - } - _ => {} - } - } - fold_tuple_of_constants(metadata, &mut blocks[bi], i)?; - } - AnyInstruction::Real(Instruction::BuildList { .. } | Instruction::BuildSet { .. }) => { - optimize_lists_and_sets(metadata, &mut blocks[bi], i, nextop)?; - } - AnyInstruction::Real( - Instruction::PopJumpIfNotNone { .. } | Instruction::PopJumpIfNone { .. }, - ) if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) - && jump_thread(blocks, block_idx, i, &target, inst.instr)? => - { - continue; - } - AnyInstruction::Real(Instruction::PopJumpIfFalse { .. }) - if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) - && jump_thread(blocks, block_idx, i, &target, inst.instr)? => - { - continue; - } - AnyInstruction::Real(Instruction::PopJumpIfTrue { .. }) - if matches!(target.instr.into(), AnyOpcode::Pseudo(PseudoOpcode::Jump)) - && jump_thread(blocks, block_idx, i, &target, inst.instr)? => - { - continue; - } - AnyInstruction::Pseudo( - pseudo @ (PseudoInstruction::JumpIfFalse { .. } - | PseudoInstruction::JumpIfTrue { .. }), - ) => { - let opcode = pseudo.into(); - match target.instr.pseudo().map(Into::into) { - Some(PseudoOpcode::Jump) - if jump_thread(blocks, block_idx, i, &target, opcode)? => - { - continue; - } - Some(PseudoOpcode::JumpIfFalse) - if matches!( - opcode, - AnyInstruction::Pseudo(PseudoInstruction::JumpIfFalse { .. }) - ) && jump_thread(blocks, block_idx, i, &target, opcode)? => - { - continue; - } - Some(PseudoOpcode::JumpIfTrue) - if matches!( - opcode, - AnyInstruction::Pseudo(PseudoInstruction::JumpIfTrue { .. }) - ) && jump_thread(blocks, block_idx, i, &target, opcode)? => - { - continue; - } - Some(PseudoOpcode::JumpIfFalse | PseudoOpcode::JumpIfTrue) => { - let next = blocks[inst.target.idx()].next; - debug_assert!(next != BlockIdx::NULL); - debug_assert!(next != inst.target); - blocks[bi].instructions[i].target = next; - continue; - } - _ => {} - } - } - AnyInstruction::Pseudo( - PseudoInstruction::Jump { .. } | PseudoInstruction::JumpNoInterrupt { .. }, - ) => match target.instr.into() { - AnyOpcode::Pseudo(PseudoOpcode::Jump) - if jump_thread(blocks, block_idx, i, &target, PseudoOpcode::Jump.into())? => - { - continue; - } - AnyOpcode::Pseudo(PseudoOpcode::JumpNoInterrupt) - if jump_thread(blocks, block_idx, i, &target, inst.instr)? => - { - continue; - } - _ => {} - }, - // CPython leaves FOR_ITER jump threading disabled. - AnyInstruction::Real(Instruction::ForIter { .. }) => {} - AnyInstruction::Real(Instruction::StoreFast { .. }) - if matches!(nextop, Some(Instruction::StoreFast { .. })) - && u32::from(inst.arg) == u32::from(blocks[bi].instructions[i + 1].arg) - && instruction_lineno(&blocks[bi].instructions[i]) - == instruction_lineno(&blocks[bi].instructions[i + 1]) => - { - blocks[bi].instructions[i].instr = Instruction::PopTop.into(); - blocks[bi].instructions[i].arg = OpArg::NULL; - } - AnyInstruction::Real(Instruction::Swap { .. }) if u32::from(inst.arg) == 1 => { - set_to_nop(&mut blocks[bi].instructions[i]); - } - AnyInstruction::Real(Instruction::LoadGlobal { .. }) - if matches!(nextop, Some(Instruction::PushNull)) - && (u32::from(inst.arg) & 1) == 0 => - { - instr_set_op1( - &mut blocks[bi].instructions[i], - inst.instr, - OpArg::new(u32::from(inst.arg) | 1), - ); - set_to_nop(&mut blocks[bi].instructions[i + 1]); - } - AnyInstruction::Real(Instruction::CompareOp { .. }) - if matches!(nextop, Some(Instruction::ToBool)) => - { - set_to_nop(&mut blocks[bi].instructions[i]); - instr_set_op1( - &mut blocks[bi].instructions[i + 1], - inst.instr, - OpArg::new(u32::from(inst.arg) | oparg::COMPARE_OP_BOOL_MASK), - ); - i += 1; - continue; - } - AnyInstruction::Real(Instruction::ContainsOp { .. } | Instruction::IsOp { .. }) - if matches!(nextop, Some(Instruction::ToBool)) => - { - set_to_nop(&mut blocks[bi].instructions[i]); - instr_set_op1(&mut blocks[bi].instructions[i + 1], inst.instr, inst.arg); - i += 1; - continue; - } - AnyInstruction::Real(Instruction::ContainsOp { .. } | Instruction::IsOp { .. }) - if matches!(nextop, Some(Instruction::UnaryNot)) => - { - set_to_nop(&mut blocks[bi].instructions[i]); - let inverted = u32::from(inst.arg) ^ 1; - debug_assert!(inverted == 0 || inverted == 1); - instr_set_op1( - &mut blocks[bi].instructions[i + 1], - inst.instr, - OpArg::new(inverted), - ); - i += 1; - continue; - } - AnyInstruction::Real(Instruction::ToBool) - if matches!(nextop, Some(Instruction::ToBool)) => - { - set_to_nop(&mut blocks[bi].instructions[i]); - i += 1; - continue; - } - AnyInstruction::Real(Instruction::UnaryNot) => { - if matches!(nextop, Some(Instruction::ToBool)) { - set_to_nop(&mut blocks[bi].instructions[i]); - instr_set_op0(&mut blocks[bi].instructions[i + 1], inst.instr); - i += 1; - continue; - } - if matches!(nextop, Some(Instruction::UnaryNot)) { - set_to_nop(&mut blocks[bi].instructions[i]); - set_to_nop(&mut blocks[bi].instructions[i + 1]); - i += 1; - continue; - } - fold_const_unaryop(metadata, &mut blocks[bi], i)?; - } - AnyInstruction::Real(Instruction::UnaryInvert | Instruction::UnaryNegative) => { - fold_const_unaryop(metadata, &mut blocks[bi], i)?; - } - AnyInstruction::Real(Instruction::CallIntrinsic1 { func }) => { - match func.get(inst.arg) { - IntrinsicFunction1::ListToTuple => { - if matches!(nextop, Some(Instruction::GetIter)) { - set_to_nop(&mut blocks[bi].instructions[i]); - } else { - fold_constant_intrinsic_list_to_tuple(metadata, &mut blocks[bi], i)?; - } - } - IntrinsicFunction1::UnaryPositive => { - fold_const_unaryop(metadata, &mut blocks[bi], i)?; - } - _ => {} - } - } - AnyInstruction::Real(Instruction::BinaryOp { .. }) => { - fold_const_binop(metadata, &mut blocks[bi], i)?; - } - _ => {} - } - - i += 1; + if instr.maybe_instr_make_load_smallint(&constant) { + return Ok(()); } - apply_static_swaps_block(&mut blocks[block_idx])?; + + let const_idx = add_const(metadata, constant)?; + instr.instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32)); Ok(()) } -/// flowgraph.c remove_redundant_nops_and_pairs -#[allow(clippy::if_same_then_else, clippy::useless_let_if_seq)] -#[allow(clippy::unnecessary_wraps)] -fn remove_redundant_nops_and_pairs(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut done = false; +/// flowgraph.c fold_const_unaryop +fn fold_const_unaryop( + metadata: &mut CodeUnitMetadata, + block: &mut Block, + i: usize, +) -> crate::InternalResult { + let instr = &block.instructions[i]; + let (op, intrinsic) = match instr.instr.real() { + Some(Instruction::UnaryNegative) => (Instruction::UnaryNegative, None), + Some(Instruction::UnaryInvert) => (Instruction::UnaryInvert, None), + Some(Instruction::UnaryNot) => (Instruction::UnaryNot, None), + Some(Instruction::CallIntrinsic1 { func }) + if matches!( + func.get(instr.arg), + oparg::IntrinsicFunction1::UnaryPositive + ) => + { + (Opcode::CallIntrinsic1.into(), Some(func.get(instr.arg))) + } + _ => return Ok(false), + }; + let Some(operand_index) = (if let Some(start) = i.checked_sub(1) { + block.get_const_loading_instrs(start, 1)? + } else { + None + }) + .and_then(|indices| indices.into_iter().next()) else { + return Ok(false); + }; + let operand = get_const_value(metadata, &block.instructions[operand_index]); + let Some(operand) = operand else { + return Ok(false); + }; + let Some(folded_const) = eval_const_unaryop(&operand, op, intrinsic) else { + return Ok(false); + }; + block.nop_out(&[operand_index]); + instr_make_load_const(metadata, &mut block.instructions[i], folded_const)?; + Ok(true) +} - while !done { - done = true; - let mut instr: Option<(BlockIdx, usize)> = None; - let mut block_idx = BlockIdx::new(0); +/// flowgraph.c fold_const_binop +fn fold_const_binop( + metadata: &mut CodeUnitMetadata, + block: &mut Block, + i: usize, +) -> crate::InternalResult { + use oparg::BinaryOperator as BinOp; - while block_idx != BlockIdx::NULL { - basicblock_remove_redundant_nops(blocks, block_idx)?; - if is_label(blocks[block_idx.idx()].cpython_label) { - instr = None; - } + let Some(Opcode::BinaryOp) = block.instructions[i].instr.real_opcode() else { + return Ok(false); + }; - let len = blocks[block_idx.idx()].instruction_used; - for instr_idx in 0..len { - let prev_instr = instr; - instr = Some((block_idx, instr_idx)); - let instr_info = blocks[block_idx.idx()].instructions[instr_idx]; - let mut prev_opcode = None; - let mut prev_oparg = 0; - if let Some((prev_block, prev_instr_idx)) = prev_instr { - let prev_info = blocks[prev_block.idx()].instructions[prev_instr_idx]; - prev_opcode = prev_info.instr.real(); - prev_oparg = match prev_info.instr.real() { - Some(Instruction::Copy { i }) => i.get(prev_info.arg), - _ => u32::from(prev_info.arg), - }; - } - let opcode = instr_info.instr.real(); - let mut is_redundant_pair = false; - if matches!(opcode, Some(Instruction::PopTop)) { - if matches!( - prev_opcode, - Some(Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. }) - ) { - is_redundant_pair = true; - } else if matches!(prev_opcode, Some(Instruction::Copy { .. })) - && prev_oparg == 1 - { - is_redundant_pair = true; - } - } + let Some(operand_indices) = (if let Some(start) = i.checked_sub(1) { + block.get_const_loading_instrs(start, 2)? + } else { + None + }) else { + return Ok(false); + }; - if is_redundant_pair { - let (prev_block, prev_instr_idx) = - prev_instr.expect("redundant pair has previous"); - set_to_nop(&mut blocks[prev_block.idx()].instructions[prev_instr_idx]); - set_to_nop(&mut blocks[block_idx.idx()].instructions[instr_idx]); - done = false; - } - } + let op_raw = u32::from(block.instructions[i].arg); + let Ok(op) = BinOp::try_from(op_raw) else { + return Ok(false); + }; - let mut instr_is_jump = false; - if let Some((instr_block, instr_idx)) = instr { - instr_is_jump = is_jump(&blocks[instr_block.idx()].instructions[instr_idx]); - } - let block = &blocks[block_idx.idx()]; - if instr_is_jump || !bb_has_fallthrough(block) { - instr = None; - } - block_idx = block.next; - } - } - Ok(()) -} + let left = get_const_value(metadata, &block.instructions[operand_indices[0]]); + let right = get_const_value(metadata, &block.instructions[operand_indices[1]]); + let (Some(left_val), Some(right_val)) = (left, right) else { + return Ok(false); + }; -/// flowgraph.c remove_unused_consts -#[allow(clippy::needless_range_loop)] -fn remove_unused_consts( - blocks: &mut [Block], - consts: &mut ConstantPool, -) -> crate::InternalResult<()> { - let nconsts = consts.len(); - if nconsts == 0 { - return Ok(()); - } + let Some(result_const) = eval_const_binop(&left_val, &right_val, op) else { + return Ok(false); + }; - let mut index_map = Vec::new(); - index_map - .try_reserve_exact(nconsts) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - index_map.resize(nconsts, 0isize); - for i in 1..nconsts { - index_map[i] = -1; - } - // The first constant may be docstring; keep it always. - index_map[0] = 0; + block.nop_out(&operand_indices); + instr_make_load_const(metadata, &mut block.instructions[i], result_const)?; + Ok(true) +} - // Mark used consts. - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let block = &blocks[block_idx]; - for i in 0..block.instruction_used { - let instr = &block.instructions[i]; - if instr.instr.has_const() { - let index = u32::from(instr.arg) as usize; - debug_assert!(index < nconsts); - index_map[index] = index as isize; - } +/// flowgraph.c get_const_value +fn get_const_value(metadata: &CodeUnitMetadata, info: &InstructionInfo) -> Option { + match info.instr.real_opcode() { + Some(Opcode::LoadSmallInt) => { + let v = u32::from(info.arg) as i32; + Some(ConstantData::Integer { + value: BigInt::from(v), + }) } - block_idx = block.next; - } - - // Now index_map[i] == i if consts[i] is used, -1 otherwise. - // Condense consts. - let mut n_used_consts = 0; - for i in 0..nconsts { - if index_map[i] != -1 { - debug_assert_eq!(index_map[i], i as isize); - index_map[n_used_consts] = index_map[i]; - n_used_consts += 1; + _ if info.instr.has_const() => { + let idx = u32::from(info.arg) as usize; + metadata.consts.get_index(idx).cloned() } + _ => None, } +} - if n_used_consts == nconsts { - return Ok(()); - } - - // Move all used consts to the beginning of the consts list. - debug_assert!(n_used_consts < nconsts); - for i in 0..n_used_consts { - let old_index = index_map[i] as usize; - debug_assert!(i <= old_index && old_index < nconsts); - if i != old_index { - let value = consts.constants[old_index].clone(); - consts.constants[i] = value; +/// flowgraph.c const_folding_check_complexity +fn const_folding_check_complexity(obj: &ConstantData, mut limit: isize) -> Option { + if let ConstantData::Tuple { elements } = obj { + limit -= isize::try_from(elements.len()).ok()?; + if limit < 0 { + return None; } - } - - // Truncate the consts list at its new size. - consts.constants.truncate(n_used_consts); - - // Adjust const indices in the bytecode. - let mut reverse_index_map = Vec::new(); - reverse_index_map - .try_reserve_exact(nconsts) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - reverse_index_map.resize(nconsts, 0isize); - for i in 0..nconsts { - reverse_index_map[i] = -1; - } - for i in 0..n_used_consts { - let old_index = index_map[i]; - debug_assert!(old_index != -1); - let old_index = old_index as usize; - debug_assert_eq!(reverse_index_map[old_index], -1); - reverse_index_map[old_index] = i as isize; - } - - block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next_block = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx]; - for i in 0..block.instruction_used { - let instr = &mut block.instructions[i]; - if instr.instr.has_const() { - let index = u32::from(instr.arg) as usize; - debug_assert!(reverse_index_map[index] >= 0); - debug_assert!(reverse_index_map[index] < n_used_consts as isize); - instr.arg = OpArg::new(reverse_index_map[index] as u32); - } + for element in elements { + limit = const_folding_check_complexity(element, limit)?; } - block_idx = next_block; } - Ok(()) + Some(limit) } -fn optimize_load_fast(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut max_instrs = 0; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - max_instrs = max_instrs.max(blocks[current.idx()].instruction_used); - current = blocks[current.idx()].next; +fn repeat_wtf8(value: &Wtf8Buf, n: usize) -> Option { + let mut result = Wtf8Buf::new(); + result.try_reserve_exact(value.len().checked_mul(n)?).ok()?; + for _ in 0..n { + result.push_wtf8(value); } - let mut instr_flags = Vec::new(); - instr_flags - .try_reserve_exact(max_instrs) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - instr_flags.resize(max_instrs, 0u8); - let mut refs = RefStack { - refs: Vec::new(), - size: 0, - capacity: 0, - }; - let mut worklist = make_cfg_traversal_stack(blocks)?; - worklist.push(BlockIdx(0)); - blocks[0].start_depth = 0; - blocks[0].visited = true; - while let Some(block_idx) = worklist.pop() { - let block_i = block_idx.idx(); + Some(result) +} - let instr_count = blocks[block_i].instruction_used; - instr_flags[..instr_count].fill(0); - debug_assert!(blocks[block_i].start_depth >= 0); - let start_depth = blocks[block_i].start_depth as usize; - ref_stack_clear(&mut refs); - for _ in 0..start_depth { - push_ref(&mut refs, DUMMY_INSTR, NOT_LOCAL)?; - } +fn checked_repeat_count(n: &BigInt, item_size: usize) -> Option { + let n = n.to_isize()?; + if item_size != 0 && (n < 0 || n as usize > MAX_STR_SIZE / item_size) { + return None; + } + Some(n.max(0) as usize) +} - for i in 0..instr_count { - let info = blocks[block_i].instructions[i]; - let instr = info.instr; - let arg_u32 = u32::from(info.arg); - debug_assert!(!matches!(instr.real(), Some(Instruction::ExtendedArg))); - - match instr { - AnyInstruction::Real(Instruction::DeleteFast { var_num }) => { - kill_local( - &mut instr_flags, - &refs, - local_as_ref_local(usize::from(var_num.get(info.arg))), - ); - } - AnyInstruction::Real(Instruction::LoadFast { var_num }) => { - push_ref( - &mut refs, - i as isize, - local_as_ref_local(usize::from(var_num.get(info.arg))), - )?; - } - AnyInstruction::Real(Instruction::LoadFastAndClear { var_num }) => { - let local = local_as_ref_local(usize::from(var_num.get(info.arg))); - kill_local(&mut instr_flags, &refs, local); - push_ref(&mut refs, i as isize, local)?; - } - AnyInstruction::Real(Instruction::LoadFastLoadFast { .. }) => { - let local1 = (arg_u32 >> 4) as isize; - let local2 = (arg_u32 & 15) as isize; - push_ref(&mut refs, i as isize, local1)?; - push_ref(&mut refs, i as isize, local2)?; - } - AnyInstruction::Real(Instruction::StoreFast { var_num }) => { - let r = ref_stack_pop(&mut refs); - store_local( - &mut instr_flags, - &refs, - local_as_ref_local(usize::from(var_num.get(info.arg))), - r, - ); - } - AnyInstruction::Real(Instruction::StoreFastLoadFast { .. }) => { - let r = ref_stack_pop(&mut refs); - store_local(&mut instr_flags, &refs, (arg_u32 >> 4) as isize, r); - push_ref(&mut refs, i as isize, (arg_u32 & 15) as isize)?; - } - AnyInstruction::Real(Instruction::StoreFastStoreFast { .. }) => { - let r1 = ref_stack_pop(&mut refs); - store_local(&mut instr_flags, &refs, (arg_u32 >> 4) as isize, r1); - let r2 = ref_stack_pop(&mut refs); - store_local(&mut instr_flags, &refs, (arg_u32 & 15) as isize, r2); - } - AnyInstruction::Real(Instruction::Copy { i: _ }) => { - let depth = arg_u32 as usize; - assert!(depth > 0); - assert!(refs.size >= depth); - let r = ref_stack_at(&refs, refs.size - depth); - push_ref(&mut refs, r.instr, r.local)?; - } - AnyInstruction::Real(Instruction::Swap { i: _ }) => { - let depth = arg_u32 as usize; - assert!(depth >= 2); - assert!(refs.size >= depth); - ref_stack_swap_top(&mut refs, depth); - } - AnyInstruction::Real( - Instruction::FormatSimple - | Instruction::GetAnext - | Instruction::GetLen - | Instruction::GetYieldFromIter - | Instruction::ImportFrom { .. } - | Instruction::MatchKeys - | Instruction::MatchMapping - | Instruction::MatchSequence - | Instruction::WithExceptStart, - ) => { - let effect = instr.stack_effect_info(arg_u32); - let net_pushed = effect.pushed() as isize - effect.popped() as isize; - debug_assert!(net_pushed >= 0); - // CPython optimize_load_fast() shadows the outer - // instruction index in this produced-value loop. - for produced in 0..net_pushed { - push_ref(&mut refs, produced, NOT_LOCAL)?; - } - } - AnyInstruction::Real( - Instruction::DictMerge { .. } - | Instruction::DictUpdate { .. } - | Instruction::ListAppend { .. } - | Instruction::ListExtend { .. } - | Instruction::MapAdd { .. } - | Instruction::Reraise { .. } - | Instruction::SetAdd { .. } - | Instruction::SetUpdate { .. }, - ) => { - let effect = instr.stack_effect_info(arg_u32); - let net_popped = effect.popped() as isize - effect.pushed() as isize; - debug_assert!(net_popped > 0); - for _ in 0..net_popped { - let _ = ref_stack_pop(&mut refs); - } - } - AnyInstruction::Real( - Instruction::EndSend | Instruction::SetFunctionAttribute { .. }, - ) => { - let effect = instr.stack_effect_info(arg_u32); - debug_assert_eq!(effect.popped(), 2); - debug_assert_eq!(effect.pushed(), 1); - let tos = ref_stack_pop(&mut refs); - let _ = ref_stack_pop(&mut refs); - push_ref(&mut refs, tos.instr, tos.local)?; - } - AnyInstruction::Real(Instruction::CheckExcMatch) => { - let _ = ref_stack_pop(&mut refs); - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - } - AnyInstruction::Real(Instruction::ForIter { .. }) => { - let target = info.target; - debug_assert!(target != BlockIdx::NULL); - load_fast_push_block(&mut worklist, blocks, target, refs.size + 1); - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - } - AnyInstruction::Real( - Instruction::LoadAttr { .. } | Instruction::LoadSuperAttr { .. }, - ) => { - let self_ref = ref_stack_pop(&mut refs); - if matches!(instr.real(), Some(Instruction::LoadSuperAttr { .. })) { - let _ = ref_stack_pop(&mut refs); - let _ = ref_stack_pop(&mut refs); - } - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - if arg_u32 & 1 != 0 { - push_ref(&mut refs, self_ref.instr, self_ref.local)?; - } - } - AnyInstruction::Real( - Instruction::LoadSpecial { .. } | Instruction::PushExcInfo, - ) => { - let tos = ref_stack_pop(&mut refs); - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - push_ref(&mut refs, tos.instr, tos.local)?; - } - AnyInstruction::Real(Instruction::Send { .. }) => { - let target = info.target; - debug_assert!(target != BlockIdx::NULL); - load_fast_push_block(&mut worklist, blocks, target, refs.size); - let _ = ref_stack_pop(&mut refs); - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - } - _ => { - let effect = instr.stack_effect_info(arg_u32); - let num_popped = effect.popped() as usize; - let num_pushed = effect.pushed() as usize; - let target = info.target; - if instr.has_target() { - debug_assert!(target != BlockIdx::NULL); - debug_assert!(refs.size >= num_popped); - let target_depth = refs.size - num_popped + num_pushed; - load_fast_push_block(&mut worklist, blocks, target, target_depth); - } - if !is_block_push(&info) { - for _ in 0..num_popped { - let _ = ref_stack_pop(&mut refs); - } - for _ in 0..num_pushed { - push_ref(&mut refs, i as isize, NOT_LOCAL)?; - } - } - } +/// flowgraph.c const_folding_safe_multiply +fn const_folding_safe_multiply(left: &ConstantData, right: &ConstantData) -> Option { + match (left, right) { + (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { + if !l.is_zero() && !r.is_zero() && l.bits() + r.bits() > MAX_INT_SIZE { + return None; } + Some(ConstantData::Integer { value: l * r }) } - - let fallthrough = blocks[block_i].next; - let term = basicblock_last_instr(&blocks[block_i]).copied(); - if let Some(term) = term - && fallthrough != BlockIdx::NULL - && !term.instr.is_unconditional_jump() - && !term.instr.is_scope_exit() - { - debug_assert!(bb_has_fallthrough(&blocks[block_i])); - load_fast_push_block(&mut worklist, blocks, fallthrough, refs.size); + (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { + Some(ConstantData::Float { value: l * r }) } - - for i in 0..refs.size { - let r = ref_stack_at(&refs, i); - if r.instr != DUMMY_INSTR { - instr_flags[r.instr as usize] |= LoadFastInstrFlag::RefUnconsumed as u8; - } + (ConstantData::Str { value: s }, ConstantData::Integer { value: n }) => { + let n = checked_repeat_count(n, s.code_points().count())?; + Some(ConstantData::Str { + value: repeat_wtf8(s, n)?, + }) } - - let block = &mut blocks[block_idx]; - let iused = block.instruction_used; - let mut i = 0; - while i < iused { - let info = &mut block.instructions[i]; - if instr_flags[i] != 0 { - i += 1; - continue; - } - match info.instr.real() { - Some(Instruction::LoadFast { .. }) => { - info.instr = Instruction::LoadFastBorrow { - var_num: Arg::marker(), - } - .into(); - } - Some(Instruction::LoadFastLoadFast { .. }) => { - info.instr = Instruction::LoadFastBorrowLoadFastBorrow { - var_nums: Arg::marker(), - } - .into(); - } - _ => {} + (ConstantData::Integer { .. }, ConstantData::Str { .. }) => { + const_folding_safe_multiply(right, left) + } + (ConstantData::Bytes { value: b }, ConstantData::Integer { value: n }) => { + let n = checked_repeat_count(n, b.len())?; + let mut value = Vec::new(); + value.try_reserve_exact(b.len().checked_mul(n)?).ok()?; + for _ in 0..n { + value.extend_from_slice(b); } - i += 1; + Some(ConstantData::Bytes { value }) } - } - Ok(()) -} - -/// flowgraph.c calculate_stackdepth -fn calculate_stackdepth(blocks: &mut [Block]) -> crate::InternalResult { - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - blocks[current.idx()].start_depth = START_DEPTH_UNSET; - current = blocks[current.idx()].next; - } - let mut stack = make_cfg_traversal_stack(blocks)?; - let mut maxdepth = 0i32; - stackdepth_push(&mut stack, blocks, BlockIdx(0), 0)?; - while let Some(block_idx) = stack.pop() { - let idx = block_idx.idx(); - let mut depth = blocks[idx].start_depth; - debug_assert!(depth >= 0); - let mut next = blocks[idx].next; - let instr_count = blocks[idx].instruction_used; - for i in 0..instr_count { - let ins = blocks[idx].instructions[i]; - let instr = &ins.instr; - let effects = get_stack_effects(*instr, ins.arg, 0)?; - let new_depth = depth + effects.net; - if new_depth < 0 { - return Err(InternalError::StackUnderflow); + (ConstantData::Integer { .. }, ConstantData::Bytes { .. }) => { + const_folding_safe_multiply(right, left) + } + (ConstantData::Tuple { elements }, ConstantData::Integer { value: n }) => { + if elements.is_empty() { + return Some(ConstantData::Tuple { + elements: Vec::new(), + }); } - maxdepth = maxdepth.max(depth); - if instr.has_target() && !matches!(instr.real(), Some(Instruction::EndAsyncFor)) { - debug_assert!(ins.target != BlockIdx::NULL); - let effects = get_stack_effects(*instr, ins.arg, 1)?; - let target_depth = depth + effects.net; - debug_assert!(target_depth >= 0); - maxdepth = maxdepth.max(depth); - stackdepth_push(&mut stack, blocks, ins.target, target_depth)?; + let n = n.to_usize()?; + if n != 0 { + if n > MAX_COLLECTION_SIZE / elements.len() { + return None; + } + const_folding_check_complexity( + &ConstantData::Tuple { + elements: elements.clone(), + }, + MAX_TOTAL_ITEMS / isize::try_from(n).ok()?, + )?; } - depth = new_depth; - debug_assert!(!instr.is_assembler()); - if instr.is_unconditional_jump() || instr.is_scope_exit() { - next = BlockIdx::NULL; - break; + let mut result = Vec::new(); + result + .try_reserve_exact(elements.len().checked_mul(n)?) + .ok()?; + for _ in 0..n { + result.extend(elements.iter().cloned()); } + Some(ConstantData::Tuple { elements: result }) } - if next != BlockIdx::NULL { - debug_assert!(bb_has_fallthrough(&blocks[idx])); - stackdepth_push(&mut stack, blocks, next, depth)?; + (ConstantData::Integer { .. }, ConstantData::Tuple { .. }) => { + const_folding_safe_multiply(right, left) } + _ => None, } - - let stackdepth = maxdepth; - Ok(stackdepth as u32) } -#[cfg(test)] -impl CodeInfo { - fn debug_block_dump(&self) -> String { - let mut out = String::new(); - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - use core::fmt::Write; - let block = &self.blocks[block_idx.idx()]; - let block_return = if basicblock_returns(block) { - " return" - } else { - "" - }; - let _ = writeln!( - out, - "block {} next={} cold={} except={} preserve_lasti={} start_depth={}{}", - u32::from(block_idx), - if block.next == BlockIdx::NULL { - String::from("NULL") - } else { - u32::from(block.next).to_string() - }, - block.cold, - block.except_handler, - block.preserve_lasti, - if block.start_depth < 0 { - String::from("None") +/// flowgraph.c const_folding_safe_power +fn const_folding_safe_power(left: &ConstantData, right: &ConstantData) -> Option { + match (left, right) { + (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { + if r < &BigInt::from(0) { + if l.is_zero() { + return None; + } + let base = l.to_f64()?; + if !base.is_finite() { + return None; + } + let result = if let Some(exp) = r.to_i32() { + base.powi(exp) } else { - block.start_depth.to_string() - }, - block_return, - ); - for info in &block.instructions[..block.instruction_used] { - let lineno = instruction_lineno(info); - let _ = writeln!( - out, - " [disp={}:{} raw={}:{}-{}:{} override={:?}] {:?} arg={} target={}", - lineno, - info.location.character_offset.get(), - info.location.line.get(), - info.location.character_offset.get(), - info.end_location.line.get(), - info.end_location.character_offset.get(), - info.lineno_override, - info.instr, - u32::from(info.arg), - if info.target == BlockIdx::NULL { - String::from("NULL") - } else { - u32::from(info.target).to_string() - } - ); + base.powf(r.to_f64()?) + }; + if !result.is_finite() { + return None; + } + return Some(ConstantData::Float { value: result }); } - block_idx = block.next; + let exp: u64 = r.try_into().ok()?; + let exp_usize = usize::try_from(exp).ok()?; + if !l.is_zero() && exp > 0 && l.bits() > MAX_INT_SIZE / exp { + return None; + } + Some(ConstantData::Integer { + value: num_traits::pow::pow(l.clone(), exp_usize), + }) + } + (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { + let result = l.powf(*r); + result + .is_finite() + .then_some(ConstantData::Float { value: result }) } - out + _ => None, } +} - pub(crate) fn debug_late_cfg_trace(mut self) -> crate::InternalResult> { - let mut trace = Vec::new(); - trace.push(("initial".to_owned(), self.debug_block_dump())); - - let instr_sequence = self.prepare_cfg_from_codegen()?; - self.blocks = cfg_from_instruction_sequence(instr_sequence)?; - trace.push(( - "after_cfg_from_instruction_sequence".to_owned(), - self.debug_block_dump(), - )); - translate_jump_labels_to_targets(&mut self.blocks)?; - mark_except_handlers(&mut self.blocks)?; - label_exception_targets(&mut self.blocks)?; - check_cfg(&self.blocks)?; - inline_small_or_no_lineno_blocks(&mut self.blocks)?; - trace.push(( - "after_inline_small_or_no_lineno_blocks".to_owned(), - self.debug_block_dump(), - )); - remove_unreachable(&mut self.blocks)?; - resolve_line_numbers(&mut self.blocks, self.metadata.firstlineno)?; - optimize_load_const(&mut self.metadata, &mut self.blocks)?; - trace.push(( - "after_optimize_load_const".to_owned(), - self.debug_block_dump(), - )); - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next_block = self.blocks[block_idx.idx()].next; - optimize_basic_block(&mut self.blocks, &mut self.metadata, block_idx)?; - block_idx = next_block; - } - trace.push(( - "after_optimize_basic_block".to_owned(), - self.debug_block_dump(), - )); - remove_redundant_nops_and_pairs(&mut self.blocks)?; - remove_unreachable(&mut self.blocks)?; - remove_redundant_nops_and_jumps(&mut self.blocks)?; - #[cfg(debug_assertions)] - assert!(no_redundant_jumps(&self.blocks)); - remove_unused_consts(&mut self.blocks, &mut self.metadata.consts)?; - trace.push(( - "after_optimize_cfg_cleanup".to_owned(), - self.debug_block_dump(), - )); - let nlocals = self.metadata.varnames.len(); - let nparams = self.nparams; - add_checks_for_loads_of_uninitialized_variables(&mut self.blocks, nlocals, nparams)?; - insert_superinstructions(&mut self.blocks)?; - push_cold_blocks_to_end(&mut self.blocks)?; - trace.push(( - "after_push_cold_before_chain_reorder".to_owned(), - self.debug_block_dump(), - )); - resolve_line_numbers(&mut self.blocks, self.metadata.firstlineno)?; - trace.push(( - "after_push_cold_resolve_line_numbers".to_owned(), - self.debug_block_dump(), - )); - - trace.push(( - "after_push_cold_blocks_to_end".to_owned(), - self.debug_block_dump(), - )); - - convert_pseudo_conditional_jumps(&mut self.blocks)?; - trace.push(( - "after_convert_pseudo_conditional_jumps".to_owned(), - self.debug_block_dump(), - )); - - let _max_stackdepth = calculate_stackdepth(&mut self.blocks)?; - let _nlocalsplus = prepare_localsplus(&self.metadata, &mut self.blocks, self.flags)?; - convert_pseudo_ops(&mut self.blocks)?; - trace.push(( - "after_convert_pseudo_ops".to_owned(), - self.debug_block_dump(), - )); - - normalize_jumps(&mut self.blocks)?; - #[cfg(debug_assertions)] - assert!(no_redundant_jumps(&self.blocks)); - trace.push(("after_normalize_jumps".to_owned(), self.debug_block_dump())); - optimize_load_fast(&mut self.blocks)?; - trace.push(( - "after_optimize_load_fast".to_owned(), - self.debug_block_dump(), - )); - - Ok(trace) +/// flowgraph.c const_folding_safe_lshift +fn const_folding_safe_lshift(left: &ConstantData, right: &ConstantData) -> Option { + let (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) = (left, right) + else { + return None; + }; + let shift: u64 = r.try_into().ok()?; + let shift_usize = usize::try_from(shift).ok()?; + if shift > MAX_INT_SIZE || (!l.is_zero() && l.bits() > MAX_INT_SIZE - shift) { + return None; } + Some(ConstantData::Integer { + value: l << shift_usize, + }) } -impl InstrDisplayContext for CodeInfo { - type Constant = ConstantData; - - fn get_constant(&self, consti: oparg::ConstIdx) -> &ConstantData { - &self.metadata.consts[consti.as_usize()] +/// flowgraph.c const_folding_safe_mod +fn const_folding_safe_mod(left: &ConstantData, right: &ConstantData) -> Option { + if matches!(left, ConstantData::Str { .. } | ConstantData::Bytes { .. }) { + return None; } - fn get_name(&self, i: usize) -> &str { - self.metadata.names[i].as_ref() + match (left, right) { + (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { + if r.is_zero() { + return None; + } + let rem = l.clone() % r.clone(); + let value = if !rem.is_zero() && (rem < BigInt::from(0)) != (*r < BigInt::from(0)) { + rem + r + } else { + rem + }; + Some(ConstantData::Integer { value }) + } + (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { + let (_, modulo) = float_div_mod(*l, *r)?; + Some(ConstantData::Float { value: modulo }) + } + _ => None, } +} - fn get_varname(&self, var_num: oparg::VarNum) -> &str { - self.metadata.varnames[var_num.as_usize()].as_ref() +fn float_div_mod(left: f64, right: f64) -> Option<(f64, f64)> { + if right == 0.0 { + return None; } - fn get_localsplus_name(&self, var_num: oparg::VarNum) -> &str { - let idx = var_num.as_usize(); - let nlocals = self.metadata.varnames.len(); - if idx < nlocals { - self.metadata.varnames[idx].as_ref() + let mut modulo = left % right; + let div = (left - modulo) / right; + let floordiv = if modulo != 0.0 { + let div = if (right < 0.0) != (modulo < 0.0) { + modulo += right; + div - 1.0 } else { - let cell_idx = idx - nlocals; - self.metadata - .cellvars - .get_index(cell_idx) - .unwrap_or_else(|| &self.metadata.freevars[cell_idx - self.metadata.cellvars.len()]) - .as_ref() + div + }; + let mut floordiv = div.floor(); + if div - floordiv > 0.5 { + floordiv += 1.0; } - } -} + floordiv + } else { + modulo = 0.0f64.copysign(right); + 0.0f64.copysign(left / right) + }; -const NOT_LOCAL: isize = -1; -const DUMMY_INSTR: isize = -1; + Some((floordiv, modulo)) +} -/// flowgraph.c make_super_instruction -fn make_super_instruction( - inst1: &mut InstructionInfo, - inst2: &mut InstructionInfo, - super_op: AnyInstruction, -) { - let line1 = instruction_lineno(inst1); - let line2 = instruction_lineno(inst2); - if line1 >= 0 && line2 >= 0 && line1 != line2 { - return; - } - let arg1 = u32::from(inst1.arg); - let arg2 = u32::from(inst2.arg); - if arg1 >= 16 || arg2 >= 16 { - return; - } - instr_set_op1(inst1, super_op, OpArg::new((arg1 << 4) | arg2)); - set_to_nop(inst2); +/// flowgraph.c eval_const_binop complex result construction +fn eval_const_complex_const(value: Complex) -> Option { + (value.re.is_finite() && value.im.is_finite()).then_some(ConstantData::Complex { value }) } -/// flowgraph.c insert_superinstructions -fn insert_superinstructions(blocks: &mut [Block]) -> crate::InternalResult { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next_block = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx]; - for i in 0..block.instruction_used { - let nextop = (i + 1 < block.instruction_used) - .then(|| block.instructions[i + 1].instr.real()) - .flatten(); - match block.instructions[i].instr.real() { - Some(Instruction::LoadFast { .. }) => { - if matches!(nextop, Some(Instruction::LoadFast { .. })) { - let (inst1, rest) = block.instructions[i..].split_at_mut(1); - make_super_instruction( - &mut inst1[0], - &mut rest[0], - Instruction::LoadFastLoadFast { - var_nums: Arg::marker(), - } - .into(), - ); - } - } - Some(Instruction::StoreFast { .. }) => match nextop { - Some(Instruction::LoadFast { .. }) => { - let (inst1, rest) = block.instructions[i..].split_at_mut(1); - make_super_instruction( - &mut inst1[0], - &mut rest[0], - Instruction::StoreFastLoadFast { - var_nums: Arg::marker(), - } - .into(), - ); - } - Some(Instruction::StoreFast { .. }) => { - let (inst1, rest) = block.instructions[i..].split_at_mut(1); - make_super_instruction( - &mut inst1[0], - &mut rest[0], - Instruction::StoreFastStoreFast { - var_nums: Arg::marker(), - } - .into(), - ); - } - _ => {} - }, - _ => {} +/// flowgraph.c eval_const_binop complex operations +fn eval_const_complex_binop( + left: Complex, + right: Complex, + op: oparg::BinaryOperator, +) -> Option { + use oparg::BinaryOperator as BinOp; + + let value = match op { + BinOp::Add => left + right, + BinOp::Subtract => { + let re = left.re - right.re; + // Preserve signed-zero behavior for real-zero + // minus zero-complex expressions such as `0 - 0j`. + let im = if left.re == 0.0 + && left.im == 0.0 + && right.re == 0.0 + && right.im == 0.0 + && !right.im.is_sign_negative() + { + -0.0 + } else { + left.im - right.im + }; + Complex::new(re, im) + } + BinOp::Multiply => left * right, + BinOp::TrueDivide => { + if right == Complex::new(0.0, 0.0) { + return None; } + left / right } - block_idx = next_block; - } - let res = remove_redundant_nops(blocks)?; - #[cfg(debug_assertions)] - assert!(no_redundant_nops(blocks)); - Ok(res) -} - -/// flowgraph.c LoadFastInstrFlag -#[repr(u8)] -enum LoadFastInstrFlag { - SupportKilled = 1, - StoredAsLocal = 2, - RefUnconsumed = 4, -} + BinOp::Power => { + if left == Complex::new(0.0, 0.0) { + if right.im != 0.0 || right.re < 0.0 { + return None; + } -/// flowgraph.c ref -#[derive(Clone, Copy)] -struct Ref { - instr: isize, - local: isize, -} + return eval_const_complex_const(if right.re == 0.0 { + Complex::new(1.0, 0.0) + } else { + Complex::new(0.0, 0.0) + }); + } -/// flowgraph.c ref_stack -struct RefStack { - refs: Vec, - size: usize, - capacity: usize, + if right.im == 0.0 + && right.re.fract() == 0.0 + && right.re >= f64::from(i32::MIN) + && right.re <= f64::from(i32::MAX) + { + left.powi(right.re as i32) + } else { + left.powc(right) + } + } + _ => return None, + }; + eval_const_complex_const(value) } -/// flowgraph.c ref_stack_push -fn ref_stack_push(stack: &mut RefStack, r: Ref) -> crate::InternalResult<()> { - debug_assert_eq!(stack.refs.len(), stack.capacity); - if stack.size == stack.capacity { - let doubled = stack.capacity * 2; - let new_cap = 32.max(doubled); - stack - .refs - .try_reserve_exact(new_cap - stack.capacity) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - stack.refs.resize(new_cap, Ref { instr: 0, local: 0 }); - stack.capacity = new_cap; +/// flowgraph.c eval_const_binop subscript index conversion +fn constant_as_index(value: &ConstantData) -> Option { + match value { + ConstantData::Integer { value } => value.to_i64().or_else(|| { + if value < &BigInt::from(0) { + Some(i64::MIN) + } else { + Some(i64::MAX) + } + }), + ConstantData::Boolean { value } => Some(i64::from(*value)), + _ => None, } - stack.refs[stack.size] = r; - stack.size += 1; - Ok(()) } -/// flowgraph.c ref_stack_pop -fn ref_stack_pop(stack: &mut RefStack) -> Ref { - assert!(stack.size > 0); - stack.size -= 1; - stack.refs[stack.size] +/// flowgraph.c eval_const_binop subscript slice bound conversion +fn slice_bound(value: &ConstantData) -> Option> { + match value { + ConstantData::None => Some(None), + _ => constant_as_index(value).map(Some), + } } -/// flowgraph.c ref_stack_swap_top -fn ref_stack_swap_top(stack: &mut RefStack, off: usize) { - assert!(off >= 2 && stack.size >= off); - let top = stack.size - 1; - let other = stack.size - off; - stack.refs.swap(top, other); -} +/// flowgraph.c eval_const_binop subscript slice index adjustment +fn adjusted_slice_indices(len: usize, slice: &[ConstantData; 3]) -> Option> { + let len = i64::try_from(len).ok()?; + let start = slice_bound(&slice[0])?; + let stop = slice_bound(&slice[1])?; + let step = slice_bound(&slice[2])?.unwrap_or(1); + if step == 0 || step == i64::MIN { + return None; + } -/// flowgraph.c ref_stack_at -fn ref_stack_at(stack: &RefStack, idx: usize) -> Ref { - assert!(idx < stack.size); - stack.refs[idx] -} + let step_is_negative = step < 0; + let lower = if step_is_negative { -1 } else { 0 }; + let upper = if step_is_negative { len - 1 } else { len }; + let adjust = |value: Option, default: i64| { + let mut value = value.unwrap_or(default); + if value < 0 { + value = value.saturating_add(len); + if value < 0 { + value = lower; + } + } else if value >= len { + value = upper; + } + value + }; + let start = adjust(start, if step_is_negative { upper } else { lower }); + let stop = adjust(stop, if step_is_negative { lower } else { upper }); -/// flowgraph.c ref_stack_clear -fn ref_stack_clear(stack: &mut RefStack) { - stack.size = 0; + let mut index = i128::from(start); + let stop = i128::from(stop); + let step = i128::from(step); + let slice_len = if step > 0 { + if index < stop { + usize::try_from((stop - index - 1) / step + 1).ok()? + } else { + 0 + } + } else if index > stop { + usize::try_from((index - stop - 1) / -step + 1).ok()? + } else { + 0 + }; + let mut indices = Vec::new(); + indices.try_reserve_exact(slice_len).ok()?; + if step > 0 { + while index < stop { + indices.push(usize::try_from(index).ok()?); + index += step; + } + } else { + while index > stop { + indices.push(usize::try_from(index).ok()?); + index += step; + } + } + Some(indices) } -/// flowgraph.c optimize_load_fast PUSH_REF -fn push_ref(stack: &mut RefStack, instr: isize, local: isize) -> crate::InternalResult<()> { - ref_stack_push(stack, Ref { instr, local }) +/// flowgraph.c eval_const_binop subscript index adjustment +fn adjusted_const_index(len: usize, index: &ConstantData) -> Option { + let len = i64::try_from(len).ok()?; + let index = constant_as_index(index)?; + let index = if index < 0 { + index.saturating_add(len) + } else { + index + }; + if index < 0 || index >= len { + return None; + } + usize::try_from(index).ok() } -/// flowgraph.c kill_local -fn kill_local(instr_flags: &mut [u8], refs: &RefStack, local: isize) { - for i in 0..refs.size { - let r = ref_stack_at(refs, i); - if r.local != local { - continue; +/// flowgraph.c eval_const_binop NB_SUBSCR +fn eval_const_subscript(container: &ConstantData, index: &ConstantData) -> Option { + match (container, index) { + ( + ConstantData::Str { value }, + ConstantData::Integer { .. } | ConstantData::Boolean { .. }, + ) => { + let string = value.to_string(); + if string.contains(char::REPLACEMENT_CHARACTER) { + return None; + } + let mut chars = Vec::new(); + chars.try_reserve_exact(string.chars().count()).ok()?; + chars.extend(string.chars()); + let index = adjusted_const_index(chars.len(), index)?; + Some(ConstantData::Str { + value: chars[index].to_string().into(), + }) } - debug_assert!(r.instr >= 0); - instr_flags[r.instr as usize] |= LoadFastInstrFlag::SupportKilled as u8; + (ConstantData::Str { value }, ConstantData::Slice { elements }) => { + let string = value.to_string(); + if string.contains(char::REPLACEMENT_CHARACTER) { + return None; + } + let mut chars = Vec::new(); + chars.try_reserve_exact(string.chars().count()).ok()?; + chars.extend(string.chars()); + let indices = adjusted_slice_indices(chars.len(), elements)?; + let capacity = indices.iter().try_fold(0usize, |capacity, &index| { + capacity.checked_add(chars[index].len_utf8()) + })?; + let mut result = String::new(); + result.try_reserve_exact(capacity).ok()?; + for index in indices { + result.push(chars[index]); + } + Some(ConstantData::Str { + value: result.into(), + }) + } + ( + ConstantData::Bytes { value }, + ConstantData::Integer { .. } | ConstantData::Boolean { .. }, + ) => { + let index = adjusted_const_index(value.len(), index)?; + Some(ConstantData::Integer { + value: BigInt::from(value[index]), + }) + } + (ConstantData::Bytes { value }, ConstantData::Slice { elements }) => { + let indices = adjusted_slice_indices(value.len(), elements)?; + let mut result = Vec::new(); + result.try_reserve_exact(indices.len()).ok()?; + for index in indices { + result.push(value[index]); + } + Some(ConstantData::Bytes { value: result }) + } + ( + ConstantData::Tuple { elements }, + ConstantData::Integer { .. } | ConstantData::Boolean { .. }, + ) => { + let index = adjusted_const_index(elements.len(), index)?; + Some(elements[index].clone()) + } + (ConstantData::Tuple { elements }, ConstantData::Slice { elements: slice }) => { + let indices = adjusted_slice_indices(elements.len(), slice)?; + let mut result = Vec::new(); + result.try_reserve_exact(indices.len()).ok()?; + for index in indices { + result.push(elements[index].clone()); + } + Some(ConstantData::Tuple { elements: result }) + } + _ => None, } } -/// flowgraph.c store_local -fn store_local(instr_flags: &mut [u8], refs: &RefStack, local: isize, r: Ref) { - kill_local(instr_flags, refs, local); - if r.instr != DUMMY_INSTR { - instr_flags[r.instr as usize] |= LoadFastInstrFlag::StoredAsLocal as u8; +/// flowgraph.c eval_const_binop bool/int coercion +fn constant_as_int(value: &ConstantData) -> Option<(BigInt, bool)> { + match value { + ConstantData::Boolean { value } => Some((BigInt::from(u8::from(*value)), true)), + ConstantData::Integer { value } => Some((value.clone(), false)), + _ => None, } } -fn local_as_ref_local(local: usize) -> isize { - local as isize -} +/// flowgraph.c eval_const_binop +fn eval_const_binop( + left: &ConstantData, + right: &ConstantData, + op: oparg::BinaryOperator, +) -> Option { + use oparg::BinaryOperator as BinOp; + + if matches!(op, BinOp::Subscr) { + return eval_const_subscript(left, right); + } + + if let (Some((left_int, left_is_bool)), Some((right_int, right_is_bool))) = + (constant_as_int(left), constant_as_int(right)) + && (left_is_bool || right_is_bool) + { + if left_is_bool && right_is_bool { + match op { + BinOp::And => { + return Some(ConstantData::Boolean { + value: !left_int.is_zero() & !right_int.is_zero(), + }); + } + BinOp::Or => { + return Some(ConstantData::Boolean { + value: !left_int.is_zero() | !right_int.is_zero(), + }); + } + BinOp::Xor => { + return Some(ConstantData::Boolean { + value: !left_int.is_zero() ^ !right_int.is_zero(), + }); + } + _ => {} + } + } -/// flowgraph.c load_fast_push_block -fn load_fast_push_block( - worklist: &mut CfgTraversalStack, - blocks: &mut [Block], - target: BlockIdx, - start_depth: usize, -) { - debug_assert!(target != BlockIdx::NULL); - debug_assert!(blocks[target.idx()].start_depth >= 0); - debug_assert_eq!(blocks[target.idx()].start_depth as usize, start_depth,); - if !blocks[target.idx()].visited { - blocks[target.idx()].visited = true; - worklist.push(target); + return eval_const_binop( + &ConstantData::Integer { value: left_int }, + &ConstantData::Integer { value: right_int }, + op, + ); } -} -fn stackdepth_push( - stack: &mut CfgTraversalStack, - blocks: &mut [Block], - target: BlockIdx, - depth: i32, -) -> crate::InternalResult<()> { - let idx = target.idx(); - let block_depth = &mut blocks[idx].start_depth; - if !(*block_depth < 0 || *block_depth == depth) { - return Err(InternalError::InconsistentStackDepth); - } - if *block_depth < depth && *block_depth < 100 { - debug_assert!(*block_depth < 0); - *block_depth = depth; - stack.push(target); + match (left, right) { + (ConstantData::Integer { value: l }, ConstantData::Integer { value: r }) => { + let result = match op { + BinOp::Add => l + r, + BinOp::Subtract => l - r, + BinOp::Multiply => { + return const_folding_safe_multiply(left, right); + } + BinOp::TrueDivide => { + if r.is_zero() { + return None; + } + let l_f = l.to_f64()?; + let r_f = r.to_f64()?; + let result = l_f / r_f; + if !result.is_finite() { + return None; + } + return Some(ConstantData::Float { value: result }); + } + BinOp::FloorDivide => { + if r.is_zero() { + return None; + } + // Python floor division: round towards negative infinity + let (q, rem) = (l.clone() / r.clone(), l.clone() % r.clone()); + if !rem.is_zero() && (rem < BigInt::from(0)) != (*r < BigInt::from(0)) { + q - 1 + } else { + q + } + } + BinOp::Remainder => return const_folding_safe_mod(left, right), + BinOp::Power => return const_folding_safe_power(left, right), + BinOp::Lshift => return const_folding_safe_lshift(left, right), + BinOp::Rshift => { + let shift: u32 = r.try_into().ok()?; + l >> (shift as usize) + } + BinOp::And => l & r, + BinOp::Or => l | r, + BinOp::Xor => l ^ r, + _ => return None, + }; + Some(ConstantData::Integer { value: result }) + } + (ConstantData::Float { value: l }, ConstantData::Float { value: r }) => { + let result = match op { + BinOp::Add => l + r, + BinOp::Subtract => l - r, + BinOp::Multiply => return const_folding_safe_multiply(left, right), + BinOp::TrueDivide => { + if *r == 0.0 { + return None; + } + l / r + } + BinOp::FloorDivide => { + let (floordiv, _) = float_div_mod(*l, *r)?; + floordiv + } + BinOp::Remainder => return const_folding_safe_mod(left, right), + BinOp::Power => return const_folding_safe_power(left, right), + _ => return None, + }; + if matches!(op, BinOp::Power) && !result.is_finite() { + return None; + } + Some(ConstantData::Float { value: result }) + } + // Int op Float or Float op Int → Float + (ConstantData::Integer { value: l }, ConstantData::Float { value: r }) => { + let l_f = l.to_f64()?; + eval_const_binop( + &ConstantData::Float { value: l_f }, + &ConstantData::Float { value: *r }, + op, + ) + } + (ConstantData::Float { value: l }, ConstantData::Integer { value: r }) => { + let r_f = r.to_f64()?; + eval_const_binop( + &ConstantData::Float { value: *l }, + &ConstantData::Float { value: r_f }, + op, + ) + } + (ConstantData::Integer { value: l }, ConstantData::Complex { value: r }) => { + eval_const_complex_binop(Complex::new(l.to_f64()?, 0.0), *r, op) + } + (ConstantData::Complex { value: l }, ConstantData::Integer { value: r }) => { + eval_const_complex_binop(*l, Complex::new(r.to_f64()?, 0.0), op) + } + (ConstantData::Float { value: l }, ConstantData::Complex { value: r }) => { + eval_const_complex_binop(Complex::new(*l, 0.0), *r, op) + } + (ConstantData::Complex { value: l }, ConstantData::Float { value: r }) => { + eval_const_complex_binop(*l, Complex::new(*r, 0.0), op) + } + (ConstantData::Complex { value: l }, ConstantData::Complex { value: r }) => { + eval_const_complex_binop(*l, *r, op) + } + // String concatenation and repetition + (ConstantData::Str { value: l }, ConstantData::Str { value: r }) + if matches!(op, BinOp::Add) => + { + let mut result = Wtf8Buf::new(); + result + .try_reserve_exact(l.len().checked_add(r.len())?) + .ok()?; + result.push_wtf8(l); + result.push_wtf8(r); + Some(ConstantData::Str { value: result }) + } + (ConstantData::Str { .. }, ConstantData::Integer { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + (ConstantData::Tuple { elements: l }, ConstantData::Tuple { elements: r }) + if matches!(op, BinOp::Add) => + { + let mut result = Vec::new(); + result + .try_reserve_exact(l.len().checked_add(r.len())?) + .ok()?; + result.extend(l.iter().cloned()); + result.extend(r.iter().cloned()); + Some(ConstantData::Tuple { elements: result }) + } + (ConstantData::Tuple { .. }, ConstantData::Integer { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + (ConstantData::Integer { .. }, ConstantData::Tuple { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + (ConstantData::Integer { .. }, ConstantData::Str { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + (ConstantData::Bytes { value: l }, ConstantData::Bytes { value: r }) + if matches!(op, BinOp::Add) => + { + let mut result = Vec::new(); + result + .try_reserve_exact(l.len().checked_add(r.len())?) + .ok()?; + result.extend_from_slice(l); + result.extend_from_slice(r); + Some(ConstantData::Bytes { value: result }) + } + (ConstantData::Bytes { .. }, ConstantData::Integer { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + (ConstantData::Integer { .. }, ConstantData::Bytes { .. }) + if matches!(op, BinOp::Multiply) => + { + const_folding_safe_multiply(left, right) + } + _ => None, } - Ok(()) -} - -/// flowgraph.c stack_effects -struct StackEffects { - net: i32, } -/// flowgraph.c get_stack_effects -#[allow(clippy::unnecessary_wraps)] -fn get_stack_effects( - instr: AnyInstruction, - oparg: OpArg, - jump: i32, -) -> crate::InternalResult { - if instr - .real() - .is_some_and(|op| op.as_opcode().deopt().is_some()) - { - return Err(InternalError::InvalidStackEffect); - } - let oparg = u32::from(oparg); - let net = if instr.is_block_push() && jump == 0 { - 0 - } else if jump != 0 { - instr.stack_effect_jump(oparg) - } else { - instr.stack_effect(oparg) +/// flowgraph.c fold_tuple_of_constants +fn fold_tuple_of_constants( + metadata: &mut CodeUnitMetadata, + block: &mut Block, + i: usize, +) -> crate::InternalResult { + let Some(Opcode::BuildTuple) = block.instructions[i].instr.real_opcode() else { + return Ok(false); }; - Ok(StackEffects { net }) -} - -fn vec_try_reserve_exact(vec: &mut Vec, additional: usize) -> crate::InternalResult<()> { - vec.try_reserve_exact(additional) - .map_err(|_| InternalError::MalformedControlFlowGraph) -} -fn vec_try_resize_to_double_capacity(vec: &mut Vec) -> crate::InternalResult<()> { - let capacity = vec.capacity(); - debug_assert!(capacity > 0); - let len = capacity - .checked_mul(core::mem::size_of::()) - .ok_or(InternalError::MalformedControlFlowGraph)?; - if capacity == 0 || len > usize::MAX / 2 { - return Err(InternalError::MalformedControlFlowGraph); + let tuple_size = u32::from(block.instructions[i].arg) as usize; + if tuple_size > STACK_USE_GUIDELINE { + return Ok(false); } - let new_capacity = capacity * 2; - let additional = new_capacity - .checked_sub(vec.len()) - .ok_or(InternalError::MalformedControlFlowGraph)?; - vec_try_reserve_exact(vec, additional) -} -/// assemble.c write_location_first_byte -fn write_location_first_byte(linetable: &mut Vec, code: u8, length: usize) { - linetable.extend(write_location_entry_start(code, length)); -} + let Some(operand_indices) = (if tuple_size == 0 { + Some(Vec::new()) + } else if let Some(start) = i.checked_sub(1) { + block.get_const_loading_instrs(start, tuple_size)? + } else { + None + }) else { + return Ok(false); + }; -/// pycore_code.h write_location_entry_start -fn write_location_entry_start(code: u8, length: usize) -> [u8; 1] { - debug_assert!(length > 0 && length <= 8); - debug_assert_eq!(code & 15, code); - [0x80 | (code << 3) | ((length - 1) as u8)] -} + let mut elements = Vec::new(); + elements + .try_reserve_exact(tuple_size) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + for &j in &operand_indices { + let Some(element) = get_const_value(metadata, &block.instructions[j]) else { + return Ok(false); + }; + elements.push(element); + } -/// assemble.c write_location_byte -fn write_location_byte(linetable: &mut Vec, value: u8) { - linetable.push(value); + block.nop_out(&operand_indices); + instr_make_load_const( + metadata, + &mut block.instructions[i], + ConstantData::Tuple { elements }, + )?; + Ok(true) } -/// assemble.c write_location_varint -fn write_location_varint(linetable: &mut Vec, value: u32) { - write_varint(linetable, value); -} +fn fold_constant_intrinsic_list_to_tuple( + metadata: &mut CodeUnitMetadata, + block: &mut Block, + i: usize, +) -> crate::InternalResult { + let Some(Instruction::CallIntrinsic1 { func }) = block.instructions[i].instr.real() else { + return Ok(false); + }; + if func.get(block.instructions[i].arg) != IntrinsicFunction1::ListToTuple { + return Ok(false); + } -/// assemble.c write_location_signed_varint -fn write_location_signed_varint(linetable: &mut Vec, value: i32) { - write_signed_varint(linetable, value); -} + let mut consts_found = 0usize; + let mut expect_append = true; + let mut pos = i; + while let Some(prev) = pos.checked_sub(1) { + pos = prev; + let instr = &block.instructions[pos]; + if matches!(instr.instr.real(), Some(Instruction::Nop)) { + continue; + } -/// assemble.c write_location_info_short_form -fn write_location_info_short_form( - linetable: &mut Vec, - length: usize, - column: i32, - end_column: i32, -) { - debug_assert!(length > 0 && length <= 8); - debug_assert!(column < 80); - debug_assert!(end_column >= column); - debug_assert!(end_column - column < 16); - let column_low_bits = column & 7; - let column_group = column >> 3; - let code = PyCodeLocationInfoKind::Short0 as u8 + column_group as u8; - write_location_first_byte(linetable, code, length); - write_location_byte( - linetable, - ((column_low_bits as u8) << 4) | ((end_column - column) as u8), - ); -} + if matches!(instr.instr.real(), Some(Instruction::BuildList { .. })) + && u32::from(instr.arg) == 0 + { + if !expect_append { + return Ok(false); + } -/// assemble.c write_location_info_oneline_form -fn write_location_info_oneline_form( - linetable: &mut Vec, - length: usize, - line_delta: i32, - column: i32, - end_column: i32, -) { - debug_assert!(length > 0 && length <= 8); - debug_assert!((0..3).contains(&line_delta)); - debug_assert!(column < 128); - debug_assert!(end_column < 128); - let code = PyCodeLocationInfoKind::OneLine0 as u8 + line_delta as u8; - write_location_first_byte(linetable, code, length); - write_location_byte(linetable, column as u8); - write_location_byte(linetable, end_column as u8); -} + let mut elements = Vec::new(); + elements + .try_reserve_exact(consts_found) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + for idx in (pos..i).rev() { + if matches!(block.instructions[idx].instr.real(), Some(Instruction::Nop)) { + continue; + } + if block.instructions[idx].loads_const() { + let Some(value) = get_const_value(metadata, &block.instructions[idx]) else { + return Ok(false); + }; + elements.push(value); + } + block.instructions[idx].nop_out_no_location(); + } + debug_assert_eq!(elements.len(), consts_found); + elements.reverse(); + instr_make_load_const( + metadata, + &mut block.instructions[i], + ConstantData::Tuple { elements }, + )?; + return Ok(true); + } -/// assemble.c write_location_info_long_form -fn write_location_info_long_form( - linetable: &mut Vec, - loc: LineTableLocation, - length: usize, - line_delta: i32, -) { - debug_assert!(length > 0 && length <= 8); - write_location_first_byte(linetable, PyCodeLocationInfoKind::Long as u8, length); - write_location_signed_varint(linetable, line_delta); - debug_assert!(loc.end_line >= loc.line); - write_location_varint(linetable, (loc.end_line - loc.line) as u32); - write_location_varint( - linetable, - if loc.col < 0 { 0 } else { (loc.col as u32) + 1 }, - ); - write_location_varint( - linetable, - if loc.end_col < 0 { - 0 + if expect_append { + if !matches!(instr.instr.real(), Some(Instruction::ListAppend { .. })) + || u32::from(instr.arg) != 1 + { + return Ok(false); + } } else { - (loc.end_col as u32) + 1 - }, - ); -} - -/// assemble.c write_location_info_none -fn write_location_info_none(linetable: &mut Vec, length: usize) { - write_location_first_byte(linetable, PyCodeLocationInfoKind::None as u8, length); -} + if !instr.loads_const() { + return Ok(false); + } + consts_found += 1; + } + expect_append = !expect_append; + } -/// assemble.c write_location_info_no_column -fn write_location_info_no_column(linetable: &mut Vec, length: usize, line_delta: i32) { - write_location_first_byte(linetable, PyCodeLocationInfoKind::NoColumns as u8, length); - write_location_signed_varint(linetable, line_delta); + Ok(false) } -/// assemble.c write_location_info_entry -fn write_location_info_entry( - linetable: &mut Vec, - loc: LineTableLocation, - length: usize, - prev_line: &mut i32, - debug_ranges: bool, -) -> crate::InternalResult<()> { - const THEORETICAL_MAX_ENTRY_SIZE: usize = 25; - if linetable - .len() - .checked_add(THEORETICAL_MAX_ENTRY_SIZE) - .ok_or(InternalError::MalformedControlFlowGraph)? - >= linetable.capacity() - { - debug_assert!(linetable.capacity() > THEORETICAL_MAX_ENTRY_SIZE); - vec_try_resize_to_double_capacity(linetable)?; - } - if loc.line == NO_LOCATION_OVERRIDE { - write_location_info_none(linetable, length); - return Ok(()); +/// Port of flowgraph.c optimize_lists_and_sets(). +fn optimize_lists_and_sets( + metadata: &mut CodeUnitMetadata, + block: &mut Block, + i: usize, + nextop: Option, +) -> crate::InternalResult { + let Some(instr) = block.instructions[i].instr.real() else { + return Ok(false); + }; + let is_list = matches!(instr, Instruction::BuildList { .. }); + let is_set = matches!(instr, Instruction::BuildSet { .. }); + if !is_list && !is_set { + return Ok(false); } - let line_delta = loc.line - *prev_line; - let column = loc.col; - let end_column = loc.end_col; - if !debug_ranges - || ((column < 0 || end_column < 0) && (loc.end_line == loc.line || loc.end_line < 0)) - { - write_location_info_no_column(linetable, length, line_delta); - *prev_line = loc.line; - return Ok(()); + let contains_or_iter = matches!( + nextop, + Some(Instruction::GetIter | Instruction::ContainsOp { .. }) + ); + let seq_size = u32::from(block.instructions[i].arg) as usize; + if seq_size > STACK_USE_GUIDELINE || (seq_size < MIN_CONST_SEQUENCE_SIZE && !contains_or_iter) { + return Ok(false); } - if loc.end_line == loc.line { - if line_delta == 0 && column < 80 && end_column - column < 16 && end_column >= column { - write_location_info_short_form(linetable, length, column, end_column); - return Ok(()); + let Some(operand_indices) = (if seq_size == 0 { + Some(Vec::new()) + } else if let Some(start) = i.checked_sub(1) { + block.get_const_loading_instrs(start, seq_size)? + } else { + None + }) else { + if contains_or_iter && is_list { + let arg = block.instructions[i].arg; + block.instructions[i].instr_set_op1(Opcode::BuildTuple.into(), arg); + return Ok(true); } - if (0..3).contains(&line_delta) && column < 128 && end_column < 128 { - write_location_info_oneline_form(linetable, length, line_delta, column, end_column); - *prev_line = loc.line; - return Ok(()); + return Ok(false); + }; + + let mut elements = Vec::new(); + elements + .try_reserve_exact(seq_size) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + for &j in &operand_indices { + let Some(element) = get_const_value(metadata, &block.instructions[j]) else { + return Ok(false); + }; + elements.push(element); + } + + let const_data = if is_list { + ConstantData::Tuple { elements } + } else { + ConstantData::Frozenset { elements } + }; + let const_idx = add_const(metadata, const_data)?; + + if !contains_or_iter { + debug_assert!(i >= 2); + let folded_loc = block.instructions[i].instr_location(); + + block.nop_out(&operand_indices); + + let build_instr = if is_list { + Opcode::BuildList + } else { + Opcode::BuildSet } - } + .into(); + block.instructions[i - 2].instr_set_op1(build_instr, OpArg::new(0)); + block.instructions[i - 2].instr_set_location(folded_loc); - write_location_info_long_form(linetable, loc, length, line_delta); - *prev_line = loc.line; - Ok(()) -} + block.instructions[i - 1] + .instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32)); -/// assemble.c assemble_emit_location -fn assemble_emit_location( - linetable: &mut Vec, - loc: LineTableLocation, - mut size: usize, - prev_line: &mut i32, - debug_ranges: bool, -) -> crate::InternalResult<()> { - if size == 0 { - return Ok(()); - } - while size > 8 { - write_location_info_entry(linetable, loc, 8, prev_line, debug_ranges)?; - size -= 8; + let extend_instr = if is_list { + Opcode::ListExtend + } else { + Opcode::SetUpdate + }; + block.instructions[i].instr_set_op1(extend_instr.into(), OpArg::new(1)); + return Ok(true); } - write_location_info_entry(linetable, loc, size, prev_line, debug_ranges) -} -fn no_linetable_location() -> LineTableLocation { - LineTableLocation { - line: NO_LOCATION_OVERRIDE, - end_line: NO_LOCATION_OVERRIDE, - col: NO_LOCATION_OVERRIDE, - end_col: NO_LOCATION_OVERRIDE, - } -} + block.nop_out(&operand_indices); -fn next_linetable_location() -> LineTableLocation { - LineTableLocation { - line: NEXT_LOCATION_OVERRIDE, - end_line: NEXT_LOCATION_OVERRIDE, - col: NEXT_LOCATION_OVERRIDE, - end_col: NEXT_LOCATION_OVERRIDE, - } + block.instructions[i].instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32)); + Ok(true) } -/// assemble.c assemble_emit_exception_table_item -fn assemble_emit_exception_table_item(table: &mut Vec, value: i32, mut msb: u8) { - debug_assert!((msb | 128) == 128); - debug_assert!((0..(1 << 30)).contains(&value)); - let value = value as u32; - const CONTINUATION_BIT: u8 = 64; - if value >= 1 << 24 { - table.push(((value >> 24) as u8) | CONTINUATION_BIT | msb); - msb = 0; - } - if value >= 1 << 18 { - table.push((((value >> 18) & 0x3f) as u8) | CONTINUATION_BIT | msb); - msb = 0; - } - if value >= 1 << 12 { - table.push((((value >> 12) & 0x3f) as u8) | CONTINUATION_BIT | msb); - msb = 0; - } - if value >= 1 << 6 { - table.push((((value >> 6) & 0x3f) as u8) | CONTINUATION_BIT | msb); - msb = 0; - } - table.push(((value & 0x3f) as u8) | msb); +/// flowgraph.c VISITED +const VISITED: i32 = -1; + +/// flowgraph.c SWAPPABLE +fn is_swappable(instr: AnyInstruction) -> bool { + matches!( + instr.into(), + AnyOpcode::Real(Opcode::StoreFast | Opcode::PopTop) + | AnyOpcode::Pseudo(PseudoOpcode::StoreFastMaybeNull) + ) } -/// assemble.c assemble_emit_exception_table_entry -fn assemble_emit_exception_table_entry( - table: &mut Vec, - start: i32, - end: i32, - handler_offset: i32, - handler: InstructionSequenceExceptHandlerInfo, +/// flowgraph.c basicblock_optimize_load_const +fn basicblock_optimize_load_const( + metadata: &mut CodeUnitMetadata, + block: &mut Block, ) -> crate::InternalResult<()> { - const MAX_SIZE_OF_ENTRY: usize = 20; - if table - .len() - .checked_add(MAX_SIZE_OF_ENTRY) - .ok_or(InternalError::MalformedControlFlowGraph)? - >= table.capacity() - { - vec_try_resize_to_double_capacity(table)?; - } - let size = end - start; - debug_assert!(end > start); - let target = handler_offset; - let mut depth = handler.start_depth - 1; - if handler.preserve_lasti > 0 { - depth -= 1; - } - debug_assert!(depth >= 0); - let depth_lasti = (depth << 1) | handler.preserve_lasti; - assemble_emit_exception_table_item(table, start, 1 << 7); - assemble_emit_exception_table_item(table, size, 0); - assemble_emit_exception_table_item(table, target, 0); - assemble_emit_exception_table_item(table, depth_lasti, 0); - Ok(()) -} + let mut i = 0; + let mut effective_opcode = Instruction::Nop.into(); + let mut effective_oparg = OpArg::new(0); + while i < block.instruction_used { + if matches!( + block.instructions[i].instr.real(), + Some(Instruction::LoadConst { .. }) + ) && let Some(constant) = get_const_value(metadata, &block.instructions[i]) + { + block.instructions[i].maybe_instr_make_load_smallint(&constant); + } -/// assemble.c assemble_exception_table -fn assemble_exception_table( - instrs: &[InstructionSequenceEntry], -) -> crate::InternalResult> { - let mut table = Vec::new(); - vec_try_reserve_exact(&mut table, DEFAULT_LNOTAB_SIZE)?; - let mut handler = InstructionSequenceExceptHandlerInfo { - h_label: NO_EXCEPTION_HANDLER_LABEL, - start_depth: -1, - preserve_lasti: -1, - }; - let mut start = -1; - let mut ioffset = 0i32; + let curr = block.instructions[i]; + let curr_arg = curr.arg; - for i in 0..instrs.len() { - let instr = &instrs[i]; - if instr.except_handler.h_label != handler.h_label { - if handler.h_label >= 0 { - let handler_offset = instrs[handler.h_label as usize].i_offset; - assemble_emit_exception_table_entry( - &mut table, - start, - ioffset, - handler_offset, - handler, - )?; + let is_copy_of_load_const = matches!( + (effective_opcode, curr.instr.real()), + (AnyInstruction::Real(Instruction::LoadConst { .. }), Some(Instruction::Copy { i })) + if i.get(curr_arg) == 1 + ); + if !is_copy_of_load_const { + effective_opcode = curr.instr; + effective_oparg = curr_arg; + } + debug_assert!(!effective_opcode.is_assembler()); + let Some(const_instr @ (Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. })) = + effective_opcode.real() + else { + i += 1; + continue; + }; + let const_arg = effective_oparg; + + if i + 1 >= block.instruction_used { + i += 1; + continue; + } + + let next = block.instructions[i + 1]; + let next_arg = next.arg; + + if let Some(is_true) = load_const_truthiness(const_instr, const_arg, metadata) { + let const_jump = match (next.instr.real_opcode(), next.instr.pseudo_opcode()) { + (_, Some(PseudoOpcode::JumpIfTrue)) => Some((true, false)), + (_, Some(PseudoOpcode::JumpIfFalse)) => Some((false, false)), + (Some(Opcode::PopJumpIfTrue), _) => Some((true, true)), + (Some(Opcode::PopJumpIfFalse), _) => Some((false, true)), + _ => None, + }; + if let Some((jump_if_true, pops_condition)) = const_jump { + if pops_condition { + block.instructions[i].set_to_nop(); + } + if is_true == jump_if_true { + block.instructions[i + 1].instr = PseudoOpcode::Jump.into(); + } else { + block.instructions[i + 1].set_to_nop(); + } + i += 1; + continue; } - start = ioffset; - handler = instr.except_handler; } - ioffset += instr_size(&instr.info) as i32; - } - if handler.h_label >= 0 { - let handler_offset = instrs[handler.h_label as usize].i_offset; - assemble_emit_exception_table_entry(&mut table, start, ioffset, handler_offset, handler)?; - } + // The remaining combinations require both instructions to be real. + let Some(next_instr) = next.instr.real() else { + i += 1; + continue; + }; - Ok(table.into_boxed_slice()) -} + if let Instruction::LoadConst { consti } = const_instr { + let constant = &metadata.consts[consti.get(const_arg).as_usize()]; + if matches!(constant, ConstantData::None) + && let Instruction::IsOp { invert } = next_instr + { + let mut jump_idx = i + 2; + if jump_idx >= block.instruction_used { + i += 1; + continue; + } + + if matches!( + block.instructions[jump_idx].instr.real(), + Some(Instruction::ToBool) + ) { + block.instructions[jump_idx].set_to_nop(); + jump_idx += 1; + if jump_idx >= block.instruction_used { + i += 1; + continue; + } + } + + let Some(jump_instr) = block.instructions[jump_idx].instr.real() else { + i += 1; + continue; + }; + + let mut invert = matches!( + invert.get(next_arg), + rustpython_compiler_core::bytecode::Invert::Yes + ); + match jump_instr { + Instruction::PopJumpIfFalse { .. } => { + invert = !invert; + } + Instruction::PopJumpIfTrue { .. } => {} + _ => { + i += 1; + continue; + } + }; + + block.instructions[i].set_to_nop(); + block.instructions[i + 1].set_to_nop(); + block.instructions[jump_idx].instr = if invert { + Opcode::PopJumpIfNotNone + } else { + Opcode::PopJumpIfNone + } + .into(); + i += 1; + continue; + } + } + + if matches!( + const_instr, + Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. } + ) && matches!(next_instr, Instruction::ToBool) + && let Some(value) = load_const_truthiness(const_instr, const_arg, metadata) + { + let const_idx = add_const(metadata, ConstantData::Boolean { value })?; + block.instructions[i].set_to_nop(); -/// Mark exception handler target blocks. -/// flowgraph.c mark_except_handlers -#[allow(clippy::unnecessary_wraps)] -pub(crate) fn mark_except_handlers(blocks: &mut [Block]) -> crate::InternalResult<()> { - #[cfg(debug_assertions)] - { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - assert!(!blocks[block_idx.idx()].except_handler); - block_idx = blocks[block_idx.idx()].next; + block.instructions[i + 1] + .instr_set_op1(Opcode::LoadConst.into(), OpArg::new(const_idx as u32)); + i += 1; + continue; } + + i += 1; } + Ok(()) +} +/// flowgraph.c optimize_load_const +fn optimize_load_const( + metadata: &mut CodeUnitMetadata, + blocks: &mut Blocks, +) -> crate::InternalResult<()> { let mut block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - let instr_count = blocks[block_idx.idx()].instruction_used; - for i in 0..instr_count { - let instr = blocks[block_idx.idx()].instructions[i]; - if is_block_push(&instr) { - debug_assert!(instr.target != BlockIdx::NULL); - blocks[instr.target.idx()].except_handler = true; - } - } - block_idx = next; + let next_block = blocks[block_idx].next; + let block = &mut blocks[block_idx]; + basicblock_optimize_load_const(metadata, block)?; + block_idx = next_block; } Ok(()) } -/// flowgraph.c mark_cold (two-pass to match CPython). -/// -/// Phase 1 (mark_warm): propagate "warm" from entry via fall-through and -/// jump targets. CPython asserts while visiting warm blocks that they are not -/// exception handlers. -/// -/// Phase 2 (mark_cold): propagate "cold" from except_handler blocks via -/// forward edges. Blocks reached only via runtime exception dispatch are -/// marked cold and pushed to the end by push_cold_blocks_to_end. -/// -/// Blocks reached by neither phase remain `cold=false`. They are typically -/// empty unreachable placeholders left by remove_unreachable; they stay in -/// their original chain position (e.g. between entry and the post-try -/// continuation for a nested try/except whose inner_end was emptied by -/// optimize_cfg). This matches CPython's behavior and is necessary for -/// optimize_load_fast to terminate fall-through at those placeholders. -/// flowgraph.c mark_warm -fn mark_warm(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut stack = make_cfg_traversal_stack(blocks)?; - stack.push(BlockIdx(0)); - blocks[0].visited = true; - while let Some(block_idx) = stack.pop() { - let idx = block_idx.idx(); - debug_assert!(!blocks[idx].except_handler); - blocks[idx].warm = true; +#[cfg(test)] +impl CodeInfo { + fn debug_block_dump(&self) -> String { + let mut out = String::new(); + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + use core::fmt::Write; + let block = &self.blocks[block_idx]; + let block_return = if block.basicblock_returns() { + " return" + } else { + "" + }; + let _ = writeln!( + out, + "block {} next={} cold={} except={} preserve_lasti={} start_depth={}{}", + u32::from(block_idx), + if block.next == BlockIdx::NULL { + String::from("NULL") + } else { + u32::from(block.next).to_string() + }, + block.cold, + block.except_handler, + block.preserve_lasti, + if block.start_depth < 0 { + String::from("None") + } else { + block.start_depth.to_string() + }, + block_return, + ); - let next = blocks[idx].next; - if next != BlockIdx::NULL && bb_has_fallthrough(&blocks[idx]) && !blocks[next.idx()].visited - { - stack.push(next); - blocks[next.idx()].visited = true; + for info in &block.instructions[..block.instruction_used] { + let lineno = info.instruction_lineno(); + let _ = writeln!( + out, + " [disp={}:{} raw={}:{}-{}:{} override={:?}] {:?} arg={} target={}", + lineno, + info.location.character_offset.get(), + info.location.line.get(), + info.location.character_offset.get(), + info.end_location.line.get(), + info.end_location.character_offset.get(), + info.lineno_override, + info.instr, + u32::from(info.arg), + if info.target == BlockIdx::NULL { + String::from("NULL") + } else { + u32::from(info.target).to_string() + } + ); + } + block_idx = block.next; } + out + } - let instr_count = blocks[idx].instruction_used; - for i in 0..instr_count { - let instr = blocks[idx].instructions[i]; - if is_jump(&instr) { - let target = instr.target; - debug_assert!(target != BlockIdx::NULL); - if !blocks[target.idx()].visited { - stack.push(target); - blocks[target.idx()].visited = true; - } - } + pub(crate) fn debug_late_cfg_trace(mut self) -> crate::InternalResult> { + let mut trace = Vec::new(); + trace.push(("initial".to_owned(), self.debug_block_dump())); + + let instr_sequence = self.prepare_cfg_from_codegen(); + self.blocks = cfg_from_instruction_sequence(instr_sequence)?; + trace.push(( + "after_cfg_from_instruction_sequence".to_owned(), + self.debug_block_dump(), + )); + translate_jump_labels_to_targets(&mut self.blocks)?; + self.blocks.mark_except_handlers(); + label_exception_targets(&mut self.blocks)?; + self.blocks.check_cfg()?; + self.blocks.inline_small_or_no_lineno_blocks()?; + trace.push(( + "after_inline_small_or_no_lineno_blocks".to_owned(), + self.debug_block_dump(), + )); + self.blocks.remove_unreachable()?; + self.blocks + .resolve_line_numbers(self.metadata.firstlineno)?; + optimize_load_const(&mut self.metadata, &mut self.blocks)?; + trace.push(( + "after_optimize_load_const".to_owned(), + self.debug_block_dump(), + )); + let mut block_idx = BlockIdx(0); + while block_idx != BlockIdx::NULL { + let next_block = self.blocks[block_idx].next; + self.blocks + .optimize_basic_block(&mut self.metadata, block_idx)?; + block_idx = next_block; } + trace.push(( + "after_optimize_basic_block".to_owned(), + self.debug_block_dump(), + )); + self.blocks.remove_redundant_nops_and_pairs(); + self.blocks.remove_unreachable()?; + self.blocks.remove_redundant_nops_and_jumps()?; + + #[cfg(debug_assertions)] + assert!(self.blocks.no_redundant_jumps()); + + self.blocks + .remove_unused_consts(&mut self.metadata.consts)?; + trace.push(( + "after_optimize_cfg_cleanup".to_owned(), + self.debug_block_dump(), + )); + let nlocals = self.metadata.varnames.len(); + let nparams = self.nparams; + add_checks_for_loads_of_uninitialized_variables(&mut self.blocks, nlocals, nparams)?; + self.blocks.insert_superinstructions(); + self.blocks.push_cold_blocks_to_end()?; + trace.push(( + "after_push_cold_before_chain_reorder".to_owned(), + self.debug_block_dump(), + )); + self.blocks + .resolve_line_numbers(self.metadata.firstlineno)?; + trace.push(( + "after_push_cold_resolve_line_numbers".to_owned(), + self.debug_block_dump(), + )); + + trace.push(( + "after_push_cold_blocks_to_end".to_owned(), + self.debug_block_dump(), + )); + + self.blocks.convert_pseudo_conditional_jumps()?; + trace.push(( + "after_convert_pseudo_conditional_jumps".to_owned(), + self.debug_block_dump(), + )); + + let _max_stackdepth = self.blocks.calculate_stackdepth()?; + let _nlocalsplus = prepare_localsplus(&self.metadata, &mut self.blocks, self.flags)?; + convert_pseudo_ops(&mut self.blocks)?; + trace.push(( + "after_convert_pseudo_ops".to_owned(), + self.debug_block_dump(), + )); + + self.blocks.normalize_jumps()?; + + #[cfg(debug_assertions)] + assert!(self.blocks.no_redundant_jumps()); + + trace.push(("after_normalize_jumps".to_owned(), self.debug_block_dump())); + self.blocks.optimize_load_fast()?; + trace.push(( + "after_optimize_load_fast".to_owned(), + self.debug_block_dump(), + )); + + Ok(trace) } - Ok(()) } -fn mark_cold(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let block = &mut blocks[block_idx.idx()]; - debug_assert!(!block.cold); - debug_assert!(!block.warm); - block_idx = block.next; +impl InstrDisplayContext for CodeInfo { + type Constant = ConstantData; + + fn get_constant(&self, consti: oparg::ConstIdx) -> &ConstantData { + &self.metadata.consts[consti.as_usize()] } - mark_warm(blocks)?; + fn get_name(&self, i: usize) -> &str { + self.metadata.names[i].as_ref() + } - let mut cold_stack = make_cfg_traversal_stack(blocks)?; - block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let i = block_idx.idx(); - let next = blocks[i].next; - let block = &blocks[i]; - if block.except_handler { - debug_assert!(!block.warm); - cold_stack.push(block_idx); - blocks[i].visited = true; - } - block_idx = next; + fn get_varname(&self, var_num: oparg::VarNum) -> &str { + self.metadata.varnames[var_num.as_usize()].as_ref() } - while let Some(block_idx) = cold_stack.pop() { - let idx = block_idx.idx(); - blocks[idx].cold = true; - let next = blocks[idx].next; - if next != BlockIdx::NULL && bb_has_fallthrough(&blocks[idx]) { - let next_idx = next.idx(); - if !blocks[next_idx].warm && !blocks[next_idx].visited { - cold_stack.push(next); - blocks[next_idx].visited = true; - } - } - let instr_count = blocks[idx].instruction_used; - for i in 0..instr_count { - let instr = blocks[idx].instructions[i]; - if is_jump(&instr) { - debug_assert_eq!(i, instr_count - 1); - let target = instr.target; - debug_assert!(target != BlockIdx::NULL); - if !blocks[target.idx()].warm && !blocks[target.idx()].visited { - cold_stack.push(target); - blocks[target.idx()].visited = true; - } - } + fn get_localsplus_name(&self, var_num: oparg::VarNum) -> &str { + let idx = var_num.as_usize(); + let nlocals = self.metadata.varnames.len(); + if idx < nlocals { + self.metadata.varnames[idx].as_ref() + } else { + let cell_idx = idx - nlocals; + self.metadata + .cellvars + .get_index(cell_idx) + .unwrap_or_else(|| &self.metadata.freevars[cell_idx - self.metadata.cellvars.len()]) + .as_ref() } } - Ok(()) } -/// flowgraph.c push_cold_blocks_to_end -fn push_cold_blocks_to_end(blocks: &mut Vec) -> crate::InternalResult<()> { - if blocks[0].next == BlockIdx::NULL { - return Ok(()); - } +const NOT_LOCAL: isize = -1; +const DUMMY_INSTR: isize = -1; - mark_cold(blocks)?; - let mut next_label = get_max_label(blocks) + 1; +/// flowgraph.c LoadFastInstrFlag +#[derive(Clone, Copy, Eq, PartialEq)] +#[repr(u8)] +enum LoadFastInstrFlag { + SupportKilled = 1, + StoredAsLocal = 2, + RefUnconsumed = 4, +} - // If a cold block falls through to a warm block, add an explicit jump - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - if blocks[block_idx.idx()].cold - && bb_has_fallthrough(&blocks[block_idx.idx()]) - && next != BlockIdx::NULL - && blocks[next.idx()].warm - { - let explicit_jump = blocks_new_block(blocks)?; - if !is_label(blocks[next.idx()].cpython_label) { - blocks[next.idx()].cpython_label = InstructionSequenceLabel::from_index(next_label); - next_label += 1; - } - let jump_label = blocks[next.idx()].cpython_label; - debug_assert!(is_label(jump_label)); - basicblock_addop( - &mut blocks[explicit_jump.idx()], - InstructionInfo { - instr: PseudoOpcode::JumpNoInterrupt.into(), - arg: instruction_sequence_label_oparg(jump_label), - target: BlockIdx::NULL, - location: SourceLocation::default(), - end_location: SourceLocation::default(), - except_handler: None, - lineno_override: Some(NO_LOCATION_OVERRIDE), - }, - )?; - blocks[explicit_jump.idx()].cold = true; - blocks[explicit_jump.idx()].next = next; - blocks[explicit_jump.idx()].predecessors = 1; - blocks[block_idx.idx()].next = explicit_jump; - let target = blocks[explicit_jump.idx()].next; - let last = basicblock_last_instr_mut(&mut blocks[explicit_jump.idx()]) - .expect("missing explicit jump"); - last.target = target; - } - block_idx = blocks[block_idx.idx()].next; +/// flowgraph.c ref +#[derive(Clone, Copy)] +struct Ref { + instr: isize, + local: isize, +} + +/// flowgraph.c ref_stack +struct RefStack { + refs: Vec, + size: usize, + capacity: usize, +} + +/// flowgraph.c ref_stack_push +fn ref_stack_push(stack: &mut RefStack, r: Ref) -> crate::InternalResult<()> { + debug_assert_eq!(stack.refs.len(), stack.capacity); + if stack.size == stack.capacity { + let doubled = stack.capacity * 2; + let new_cap = 32.max(doubled); + stack + .refs + .try_reserve_exact(new_cap - stack.capacity) + .map_err(|_| InternalError::MalformedControlFlowGraph)?; + stack.refs.resize(new_cap, Ref { instr: 0, local: 0 }); + stack.capacity = new_cap; } + stack.refs[stack.size] = r; + stack.size += 1; + Ok(()) +} - assert!(!blocks[0].cold); - let mut cold_blocks: BlockIdx = BlockIdx::NULL; - let mut cold_blocks_tail: BlockIdx = BlockIdx::NULL; - let mut block_idx = BlockIdx(0); +/// flowgraph.c ref_stack_pop +fn ref_stack_pop(stack: &mut RefStack) -> Ref { + assert!(stack.size > 0); + stack.size -= 1; + stack.refs[stack.size] +} - while blocks[block_idx.idx()].next != BlockIdx::NULL { - debug_assert!(!blocks[block_idx.idx()].cold); - while blocks[block_idx.idx()].next != BlockIdx::NULL - && !blocks[blocks[block_idx.idx()].next.idx()].cold - { - block_idx = blocks[block_idx.idx()].next; - } - if blocks[block_idx.idx()].next == BlockIdx::NULL { - break; - } +/// flowgraph.c ref_stack_swap_top +fn ref_stack_swap_top(stack: &mut RefStack, off: usize) { + assert!(off >= 2 && stack.size >= off); + let top = stack.size - 1; + let other = stack.size - off; + stack.refs.swap(top, other); +} - debug_assert!(!blocks[block_idx.idx()].cold); - debug_assert!(blocks[blocks[block_idx.idx()].next.idx()].cold); +/// flowgraph.c ref_stack_at +fn ref_stack_at(stack: &RefStack, idx: usize) -> Ref { + assert!(idx < stack.size); + stack.refs[idx] +} - let mut block_end = blocks[block_idx.idx()].next; - while blocks[block_end.idx()].next != BlockIdx::NULL - && blocks[blocks[block_end.idx()].next.idx()].cold - { - block_end = blocks[block_end.idx()].next; - } +/// flowgraph.c ref_stack_clear +fn ref_stack_clear(stack: &mut RefStack) { + stack.size = 0; +} - debug_assert!(blocks[block_end.idx()].cold); - debug_assert!( - blocks[block_end.idx()].next == BlockIdx::NULL - || !blocks[blocks[block_end.idx()].next.idx()].cold - ); +/// flowgraph.c optimize_load_fast PUSH_REF +fn push_ref(stack: &mut RefStack, instr: isize, local: isize) -> crate::InternalResult<()> { + ref_stack_push(stack, Ref { instr, local }) +} - if cold_blocks == BlockIdx::NULL { - cold_blocks = blocks[block_idx.idx()].next; - } else { - blocks[cold_blocks_tail.idx()].next = blocks[block_idx.idx()].next; +/// flowgraph.c kill_local +fn kill_local(instr_flags: &mut [u8], refs: &RefStack, local: isize) { + for i in 0..refs.size { + let r = ref_stack_at(refs, i); + if r.local != local { + continue; } - cold_blocks_tail = block_end; - blocks[block_idx.idx()].next = blocks[block_end.idx()].next; - blocks[block_end.idx()].next = BlockIdx::NULL; + debug_assert!(r.instr >= 0); + instr_flags[r.instr as usize] |= LoadFastInstrFlag::SupportKilled as u8; } +} - debug_assert!(blocks[block_idx.idx()].next == BlockIdx::NULL); - blocks[block_idx.idx()].next = cold_blocks; - - if cold_blocks != BlockIdx::NULL { - remove_redundant_nops_and_jumps(blocks)?; +/// flowgraph.c store_local +fn store_local(instr_flags: &mut [u8], refs: &RefStack, local: isize, r: Ref) { + kill_local(instr_flags, refs, local); + if r.instr != DUMMY_INSTR { + instr_flags[r.instr as usize] |= LoadFastInstrFlag::StoredAsLocal as u8; } - Ok(()) } -/// flowgraph.c check_cfg -fn check_cfg(blocks: &[Block]) -> crate::InternalResult<()> { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let block = &blocks[block_idx.idx()]; - for i in 0..block.instruction_used { - let opcode = block.instructions[i].instr; - debug_assert!(!opcode.is_assembler()); - if opcode.is_terminator() && i != block.instruction_used - 1 { - return Err(InternalError::MalformedControlFlowGraph); - } - } - block_idx = block.next; - } - Ok(()) +fn local_as_ref_local(local: usize) -> isize { + local as isize } -/// flowgraph.c jump_thread -fn jump_thread( - blocks: &mut [Block], - block_idx: BlockIdx, - instr_idx: usize, - target: &InstructionInfo, - opcode: AnyInstruction, -) -> crate::InternalResult { - let bi = block_idx.idx(); - debug_assert!(is_jump(&blocks[bi].instructions[instr_idx])); - debug_assert!(is_jump(target)); - debug_assert_eq!(instr_idx + 1, blocks[bi].instruction_used); - debug_assert!(target.target != BlockIdx::NULL); - if blocks[bi].instructions[instr_idx].target != target.target { - set_to_nop(&mut blocks[bi].instructions[instr_idx]); - basicblock_add_jump(blocks, block_idx, opcode, target.target, target)?; - return Ok(true); +/// flowgraph.c load_fast_push_block +fn load_fast_push_block( + worklist: &mut CfgTraversalStack, + blocks: &mut Blocks, + target: BlockIdx, + start_depth: usize, +) { + debug_assert!(target != BlockIdx::NULL); + debug_assert!(blocks[target].start_depth >= 0); + debug_assert_eq!(blocks[target].start_depth as usize, start_depth,); + if !blocks[target].visited { + blocks[target].visited = true; + worklist.push(target); } - Ok(false) } -/// flowgraph.c basicblock_add_jump -fn basicblock_add_jump( - blocks: &mut [Block], - block_idx: BlockIdx, - instr: AnyInstruction, +fn stackdepth_push( + stack: &mut CfgTraversalStack, + blocks: &mut Blocks, target: BlockIdx, - loc_source: &InstructionInfo, + depth: i32, ) -> crate::InternalResult<()> { - let bi = block_idx.idx(); - let last = basicblock_last_instr(&blocks[bi]); - if last.is_some_and(is_jump) { - return Err(InternalError::MalformedControlFlowGraph); + let block_depth = &mut blocks[target].start_depth; + if !(*block_depth < 0 || *block_depth == depth) { + return Err(InternalError::InconsistentStackDepth); + } + if *block_depth < depth && *block_depth < 100 { + debug_assert!(*block_depth < 0); + *block_depth = depth; + stack.push(target); } - debug_assert!(target != BlockIdx::NULL); - let label = blocks[target.idx()].cpython_label; - debug_assert!(is_label(label)); - let arg = instruction_sequence_label_oparg(label); - let block = &mut blocks[bi]; - basicblock_addop( - block, - InstructionInfo { - instr, - arg, - target: BlockIdx::NULL, - location: loc_source.location, - end_location: loc_source.end_location, - except_handler: None, - lineno_override: loc_source.lineno_override, - }, - )?; - let last = basicblock_last_instr_mut(block).expect("missing jump"); - debug_assert!(match (last.instr, instr) { - (AnyInstruction::Real(last), AnyInstruction::Real(opcode)) => - last.as_opcode() == opcode.as_opcode(), - (AnyInstruction::Pseudo(last), AnyInstruction::Pseudo(opcode)) => - last.as_opcode() == opcode.as_opcode(), - _ => false, - }); - last.target = target; Ok(()) } -/// pycore_opcode_utils.h IS_CONDITIONAL_JUMP_OPCODE -fn is_conditional_jump_opcode(instr: &AnyInstruction) -> bool { - matches!( - instr.real().map(Into::into), - Some( - Opcode::PopJumpIfFalse - | Opcode::PopJumpIfTrue - | Opcode::PopJumpIfNone - | Opcode::PopJumpIfNotNone - ) - ) +/// flowgraph.c stack_effects +#[derive(Clone, Copy, Eq, PartialEq)] +struct StackEffects { + net: i32, } -/// flowgraph.c convert_pseudo_conditional_jumps -fn convert_pseudo_conditional_jumps(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut block_idx = BlockIdx(0); - while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx.idx()]; - let mut i = 0; - while i < block.instruction_used { - let instr = block.instructions[i]; - let opcode = instr.instr; - if matches!( - opcode.pseudo(), - Some(PseudoInstruction::JumpIfFalse { .. } | PseudoInstruction::JumpIfTrue { .. }) - ) { - debug_assert_eq!(i, block.instruction_used - 1); - block.instructions[i].instr = - if matches!(opcode.pseudo(), Some(PseudoInstruction::JumpIfFalse { .. })) { - Instruction::PopJumpIfFalse { - delta: Arg::marker(), - } - .into() - } else { - Instruction::PopJumpIfTrue { - delta: Arg::marker(), - } - .into() - }; +/// flowgraph.c get_stack_effects +fn get_stack_effects( + instr: AnyInstruction, + oparg: OpArg, + jump: i32, +) -> crate::InternalResult { + if instr + .real() + .is_some_and(|op| op.as_opcode().deopt().is_some()) + { + return Err(InternalError::InvalidStackEffect); + } + let oparg = u32::from(oparg); + let net = if instr.is_block_push() && jump == 0 { + 0 + } else if jump != 0 { + instr.stack_effect_jump(oparg) + } else { + instr.stack_effect(oparg) + }; + Ok(StackEffects { net }) +} - let location = instr.location; - let end_location = instr.end_location; - let except_handler = instr.except_handler; - let lineno_override = instr.lineno_override; - let copy = InstructionInfo { - instr: Instruction::Copy { i: Arg::marker() }.into(), - arg: OpArg::new(1), - target: BlockIdx::NULL, - location, - end_location, - except_handler, - lineno_override, - }; - basicblock_insert_instruction(block, i, copy)?; - i += 1; +fn vec_try_reserve_exact(vec: &mut Vec, additional: usize) -> crate::InternalResult<()> { + vec.try_reserve_exact(additional) + .map_err(|_| InternalError::MalformedControlFlowGraph) +} - let to_bool = InstructionInfo { - instr: Instruction::ToBool.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, - location, - end_location, - except_handler, - lineno_override, - }; - basicblock_insert_instruction(block, i, to_bool)?; - i += 1; - } - i += 1; - } - block_idx = next; +fn vec_try_resize_to_double_capacity(vec: &mut Vec) -> crate::InternalResult<()> { + let capacity = vec.capacity(); + debug_assert!(capacity > 0); + let len = capacity + .checked_mul(core::mem::size_of::()) + .ok_or(InternalError::MalformedControlFlowGraph)?; + if capacity == 0 || len > usize::MAX / 2 { + return Err(InternalError::MalformedControlFlowGraph); } - Ok(()) + let new_capacity = capacity * 2; + let additional = new_capacity + .checked_sub(vec.len()) + .ok_or(InternalError::MalformedControlFlowGraph)?; + vec_try_reserve_exact(vec, additional) } -/// flowgraph.c normalize_jumps_in_block -fn normalize_jumps_in_block( - blocks: &mut Vec, - block_idx: BlockIdx, -) -> crate::InternalResult<()> { - let idx = block_idx.idx(); - let Some(last_ins) = basicblock_last_instr(&blocks[idx]).copied() else { - return Ok(()); - }; - if !is_conditional_jump_opcode(&last_ins.instr) { - return Ok(()); - } - debug_assert!(!last_ins.instr.is_assembler()); +/// assemble.c write_location_first_byte +fn write_location_first_byte(linetable: &mut Vec, code: u8, length: usize) { + linetable.extend(write_location_entry_start(code, length)); +} - debug_assert!(last_ins.target != BlockIdx::NULL); - let is_forward = !blocks[last_ins.target.idx()].visited; +/// pycore_code.h write_location_entry_start +fn write_location_entry_start(code: u8, length: usize) -> [u8; 1] { + debug_assert!(length > 0 && length <= 8); + debug_assert_eq!(code & 15, code); + [0x80 | (code << 3) | ((length - 1) as u8)] +} - if is_forward { - // Insert NOT_TAKEN after forward conditional jump. - let not_taken = InstructionInfo { - instr: Opcode::NotTaken.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, - location: last_ins.location, - end_location: last_ins.end_location, - except_handler: None, - lineno_override: last_ins.lineno_override, - }; - basicblock_addop(&mut blocks[idx], not_taken)?; - return Ok(()); - } +/// assemble.c write_location_byte +fn write_location_byte(linetable: &mut Vec, value: u8) { + linetable.push(value); +} - let reversed_opcode = match AnyOpcode::from(last_ins.instr).real() { - Some(Opcode::PopJumpIfNotNone) => Opcode::PopJumpIfNone.into(), - Some(Opcode::PopJumpIfNone) => Opcode::PopJumpIfNotNone.into(), - Some(Opcode::PopJumpIfFalse) => Opcode::PopJumpIfTrue.into(), - Some(Opcode::PopJumpIfTrue) => Opcode::PopJumpIfFalse.into(), - _ => unreachable!("conditional jump has reverse opcode"), - }; +/// assemble.c write_location_varint +fn write_location_varint(linetable: &mut Vec, value: u32) { + write_varint(linetable, value); +} - // Transform 'conditional jump T' to 'reversed_jump b_next' followed by - // 'jump_backwards T'. - let loc = last_ins.location; - let end_loc = last_ins.end_location; +/// assemble.c write_location_signed_varint +fn write_location_signed_varint(linetable: &mut Vec, value: i32) { + write_signed_varint(linetable, value); +} - let target = last_ins.target; - let backwards_jump_idx = blocks_new_block(blocks)?; - basicblock_addop( - &mut blocks[backwards_jump_idx.idx()], - InstructionInfo { - instr: Opcode::NotTaken.into(), - arg: OpArg::new(0), - target: BlockIdx::NULL, - location: loc, - end_location: end_loc, - except_handler: None, - lineno_override: last_ins.lineno_override, +/// assemble.c write_location_info_short_form +fn write_location_info_short_form( + linetable: &mut Vec, + length: usize, + column: i32, + end_column: i32, +) { + debug_assert!(length > 0 && length <= 8); + debug_assert!(column < 80); + debug_assert!(end_column >= column); + debug_assert!(end_column - column < 16); + let column_low_bits = column & 7; + let column_group = column >> 3; + let code = PyCodeLocationInfoKind::Short0 as u8 + column_group as u8; + write_location_first_byte(linetable, code, length); + write_location_byte( + linetable, + ((column_low_bits as u8) << 4) | ((end_column - column) as u8), + ); +} + +/// assemble.c write_location_info_oneline_form +fn write_location_info_oneline_form( + linetable: &mut Vec, + length: usize, + line_delta: i32, + column: i32, + end_column: i32, +) { + debug_assert!(length > 0 && length <= 8); + debug_assert!((0..3).contains(&line_delta)); + debug_assert!(column < 128); + debug_assert!(end_column < 128); + let code = PyCodeLocationInfoKind::OneLine0 as u8 + line_delta as u8; + write_location_first_byte(linetable, code, length); + write_location_byte(linetable, column as u8); + write_location_byte(linetable, end_column as u8); +} + +/// assemble.c write_location_info_long_form +fn write_location_info_long_form( + linetable: &mut Vec, + loc: LineTableLocation, + length: usize, + line_delta: i32, +) { + debug_assert!(length > 0 && length <= 8); + write_location_first_byte(linetable, PyCodeLocationInfoKind::Long as u8, length); + write_location_signed_varint(linetable, line_delta); + debug_assert!(loc.end_line >= loc.line); + write_location_varint(linetable, (loc.end_line - loc.line) as u32); + write_location_varint( + linetable, + if loc.col < 0 { 0 } else { (loc.col as u32) + 1 }, + ); + write_location_varint( + linetable, + if loc.end_col < 0 { + 0 + } else { + (loc.end_col as u32) + 1 }, - )?; - basicblock_add_jump( - blocks, - backwards_jump_idx, - PseudoOpcode::Jump.into(), - target, - &last_ins, - )?; - blocks[backwards_jump_idx.idx()].start_depth = blocks[target.idx()].start_depth; - - let old_next = blocks[idx].next; - debug_assert!(old_next != BlockIdx::NULL); - - let last_mut = basicblock_last_instr_mut(&mut blocks[idx]).unwrap(); - last_mut.instr = reversed_opcode; - last_mut.target = old_next; - - blocks[backwards_jump_idx.idx()].cold = blocks[idx].cold; - blocks[backwards_jump_idx.idx()].next = old_next; - blocks[idx].next = backwards_jump_idx; - Ok(()) + ); } -/// flowgraph.c normalize_jumps -fn normalize_jumps(blocks: &mut Vec) -> crate::InternalResult<()> { - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - blocks[current.idx()].visited = false; - current = blocks[current.idx()].next; - } +/// assemble.c write_location_info_none +fn write_location_info_none(linetable: &mut Vec, length: usize) { + write_location_first_byte(linetable, PyCodeLocationInfoKind::None as u8, length); +} - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let idx = current.idx(); - blocks[idx].visited = true; - normalize_jumps_in_block(blocks, current)?; - current = blocks[idx].next; - } - Ok(()) +/// assemble.c write_location_info_no_column +fn write_location_info_no_column(linetable: &mut Vec, length: usize, line_delta: i32) { + write_location_first_byte(linetable, PyCodeLocationInfoKind::NoColumns as u8, length); + write_location_signed_varint(linetable, line_delta); } -/// flowgraph.c basicblock_inline_small_or_no_lineno_blocks -fn basicblock_inline_small_or_no_lineno_blocks( - blocks: &mut [Block], - block_idx: BlockIdx, -) -> crate::InternalResult { - let Some(last) = basicblock_last_instr(&blocks[block_idx.idx()]).copied() else { - return Ok(false); - }; - if !last.instr.is_unconditional_jump() { - return Ok(false); +/// assemble.c write_location_info_entry +fn write_location_info_entry( + linetable: &mut Vec, + loc: LineTableLocation, + length: usize, + prev_line: &mut i32, + debug_ranges: bool, +) -> crate::InternalResult<()> { + const THEORETICAL_MAX_ENTRY_SIZE: usize = 25; + if linetable + .len() + .checked_add(THEORETICAL_MAX_ENTRY_SIZE) + .ok_or(InternalError::MalformedControlFlowGraph)? + >= linetable.capacity() + { + debug_assert!(linetable.capacity() > THEORETICAL_MAX_ENTRY_SIZE); + vec_try_resize_to_double_capacity(linetable)?; } - - let target = last.target; - debug_assert!(target != BlockIdx::NULL); - let small_exit_block = basicblock_exits_scope(&blocks[target.idx()]) - && blocks[target.idx()].instruction_used <= MAX_COPY_SIZE; - let no_lineno_no_fallthrough = basicblock_has_no_lineno(&blocks[target.idx()]) - && !bb_has_fallthrough(&blocks[target.idx()]); - if small_exit_block || no_lineno_no_fallthrough { - debug_assert!(is_jump(&last)); - let removed_jump_opcode = last.instr; - let last = basicblock_last_instr_mut(&mut blocks[block_idx.idx()]) - .expect("non-empty block has last instruction"); - set_to_nop(last); - basicblock_append_block_instructions(blocks, block_idx, target)?; - if no_lineno_no_fallthrough { - let last = basicblock_last_instr_mut(&mut blocks[block_idx.idx()]).unwrap(); - if last.instr.is_unconditional_jump() - && matches!( - removed_jump_opcode.into(), - AnyOpcode::Pseudo(PseudoOpcode::Jump) - ) - { - last.instr = PseudoOpcode::Jump.into(); - } - } - blocks[target.idx()].predecessors -= 1; - return Ok(true); + if loc.line == NO_LOCATION_OVERRIDE { + write_location_info_none(linetable, length); + return Ok(()); } - Ok(false) -} -/// flowgraph.c inline_small_or_no_lineno_blocks -fn inline_small_or_no_lineno_blocks(blocks: &mut [Block]) -> crate::InternalResult { - loop { - let mut changes = false; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let next = blocks[current.idx()].next; - let res = basicblock_inline_small_or_no_lineno_blocks(blocks, current)?; - if res { - changes = true; - } - - current = next; - } - if !changes { - return Ok(changes); - } + let line_delta = loc.line - *prev_line; + let column = loc.col; + let end_column = loc.end_col; + if !debug_ranges + || ((column < 0 || end_column < 0) && (loc.end_line == loc.line || loc.end_line < 0)) + { + write_location_info_no_column(linetable, length, line_delta); + *prev_line = loc.line; + return Ok(()); } -} - -/// flowgraph.c basicblock_remove_redundant_nops -#[allow(clippy::unnecessary_wraps)] -fn basicblock_remove_redundant_nops( - blocks: &mut [Block], - block_idx: BlockIdx, -) -> crate::InternalResult { - let bi = block_idx.idx(); - let mut dest = 0; - let mut prev_lineno = -1i32; - let instr_count = blocks[bi].instruction_used; - - for src in 0..instr_count { - let instr = blocks[bi].instructions[src]; - let lineno = instruction_lineno(&instr); - if matches!(instr.instr.real(), Some(Instruction::Nop)) { - if lineno < 0 { - continue; - } - if prev_lineno == lineno { - continue; - } - if src < instr_count - 1 { - let next_lineno = instruction_lineno(&blocks[bi].instructions[src + 1]); - if next_lineno == lineno { - continue; - } - if next_lineno < 0 { - instr_set_loc( - &mut blocks[bi].instructions[src + 1], - instr.location, - instr.end_location, - instr.lineno_override, - ); - continue; - } - } else { - let next = next_nonempty_block(blocks, blocks[bi].next); - if next != BlockIdx::NULL { - let mut next_loc = no_linetable_location(); - let mut next_i = 0; - while next_i < blocks[next.idx()].instruction_used { - let instr = blocks[next.idx()].instructions[next_i]; - if matches!(instr.instr.real(), Some(Instruction::Nop)) - && instruction_lineno(&instr) < 0 - { - next_i += 1; - continue; - } - next_loc = instruction_linetable_location(&instr); - break; - } - if lineno == next_loc.line { - continue; - } - } - } + if loc.end_line == loc.line { + if line_delta == 0 && column < 80 && end_column - column < 16 && end_column >= column { + write_location_info_short_form(linetable, length, column, end_column); + return Ok(()); } - - if dest != src { - blocks[bi].instructions[dest] = blocks[bi].instructions[src]; + if (0..3).contains(&line_delta) && column < 128 && end_column < 128 { + write_location_info_oneline_form(linetable, length, line_delta, column, end_column); + *prev_line = loc.line; + return Ok(()); } - dest += 1; - prev_lineno = lineno; } - debug_assert!(dest <= instr_count); - let num_removed = instr_count - dest; - blocks[bi].instruction_used = dest; - Ok(num_removed) + write_location_info_long_form(linetable, loc, length, line_delta); + *prev_line = loc.line; + Ok(()) } -/// flowgraph.c remove_redundant_nops -#[allow(clippy::unnecessary_wraps)] -fn remove_redundant_nops(blocks: &mut [Block]) -> crate::InternalResult { - let mut changes = 0; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let next = blocks[current.idx()].next; - let change = basicblock_remove_redundant_nops(blocks, current)?; - changes += change; - current = next; +/// assemble.c assemble_emit_location +fn assemble_emit_location( + linetable: &mut Vec, + loc: LineTableLocation, + mut size: usize, + prev_line: &mut i32, + debug_ranges: bool, +) -> crate::InternalResult<()> { + if size == 0 { + return Ok(()); + } + while size > 8 { + write_location_info_entry(linetable, loc, 8, prev_line, debug_ranges)?; + size -= 8; } - Ok(changes) + write_location_info_entry(linetable, loc, size, prev_line, debug_ranges) } -/// flowgraph.c no_redundant_nops -#[cfg(debug_assertions)] -fn no_redundant_nops(blocks: &mut [Block]) -> bool { - match remove_redundant_nops(blocks) { - Ok(0) => true, - Ok(_) | Err(_) => false, +fn no_linetable_location() -> LineTableLocation { + LineTableLocation { + line: NO_LOCATION_OVERRIDE, + end_line: NO_LOCATION_OVERRIDE, + col: NO_LOCATION_OVERRIDE, + end_col: NO_LOCATION_OVERRIDE, } } -/// flowgraph.c remove_redundant_jumps -fn remove_redundant_jumps(blocks: &mut [Block]) -> crate::InternalResult { - let mut changes = 0; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let block_idx = current.idx(); - let Some(last) = basicblock_last_instr(&blocks[block_idx]).copied() else { - current = blocks[block_idx].next; - continue; - }; - debug_assert!(!last.instr.is_assembler()); - if last.instr.is_unconditional_jump() { - let jump_target = next_nonempty_block(blocks, last.target); - if jump_target == BlockIdx::NULL { - return Err(InternalError::MalformedControlFlowGraph); - } - let next = next_nonempty_block(blocks, blocks[block_idx].next); - if jump_target == next { - changes += 1; - let last = basicblock_last_instr_mut(&mut blocks[block_idx]).unwrap(); - set_to_nop(last); - } - } - current = blocks[block_idx].next; +fn next_linetable_location() -> LineTableLocation { + LineTableLocation { + line: NEXT_LOCATION_OVERRIDE, + end_line: NEXT_LOCATION_OVERRIDE, + col: NEXT_LOCATION_OVERRIDE, + end_col: NEXT_LOCATION_OVERRIDE, } - Ok(changes) } -/// flowgraph.c no_redundant_jumps -#[cfg(debug_assertions)] -fn no_redundant_jumps(blocks: &[Block]) -> bool { - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let block = &blocks[current.idx()]; - if let Some(last) = basicblock_last_instr(block) - && last.instr.is_unconditional_jump() - { - let next = next_nonempty_block(blocks, block.next); - let jump_target = next_nonempty_block(blocks, last.target); - if jump_target == next { - assert!(next != BlockIdx::NULL); - if instruction_lineno(last) - == instruction_lineno(&blocks[next.idx()].instructions[0]) - { - assert_ne!( - instruction_lineno(last), - instruction_lineno(&blocks[next.idx()].instructions[0]), - "redundant jump has same line as fallthrough target" - ); - return false; - } - } - } - current = block.next; +/// assemble.c assemble_emit_exception_table_item +fn assemble_emit_exception_table_item(table: &mut Vec, value: i32, mut msb: u8) { + debug_assert!((msb | 128) == 128); + debug_assert!((0..(1 << 30)).contains(&value)); + let value = value as u32; + const CONTINUATION_BIT: u8 = 64; + if value >= 1 << 24 { + table.push(((value >> 24) as u8) | CONTINUATION_BIT | msb); + msb = 0; } - true + if value >= 1 << 18 { + table.push((((value >> 18) & 0x3f) as u8) | CONTINUATION_BIT | msb); + msb = 0; + } + if value >= 1 << 12 { + table.push((((value >> 12) & 0x3f) as u8) | CONTINUATION_BIT | msb); + msb = 0; + } + if value >= 1 << 6 { + table.push((((value >> 6) & 0x3f) as u8) | CONTINUATION_BIT | msb); + msb = 0; + } + table.push(((value & 0x3f) as u8) | msb); } -fn remove_redundant_nops_and_jumps(blocks: &mut [Block]) -> crate::InternalResult<()> { - loop { - // Convergence is guaranteed because the number of redundant jumps and - // nops only decreases. - let removed_nops = remove_redundant_nops(blocks)?; - let removed_jumps = remove_redundant_jumps(blocks)?; - if removed_nops + removed_jumps == 0 { - break; - } +/// assemble.c assemble_emit_exception_table_entry +fn assemble_emit_exception_table_entry( + table: &mut Vec, + start: i32, + end: i32, + handler_offset: i32, + handler: InstructionSequenceExceptHandlerInfo, +) -> crate::InternalResult<()> { + const MAX_SIZE_OF_ENTRY: usize = 20; + if table + .len() + .checked_add(MAX_SIZE_OF_ENTRY) + .ok_or(InternalError::MalformedControlFlowGraph)? + >= table.capacity() + { + vec_try_resize_to_double_capacity(table)?; + } + let size = end - start; + debug_assert!(end > start); + let target = handler_offset; + let mut depth = handler.start_depth - 1; + if handler.preserve_lasti > 0 { + depth -= 1; } + debug_assert!(depth >= 0); + let depth_lasti = (depth << 1) | handler.preserve_lasti; + assemble_emit_exception_table_item(table, start, 1 << 7); + assemble_emit_exception_table_item(table, size, 0); + assemble_emit_exception_table_item(table, target, 0); + assemble_emit_exception_table_item(table, depth_lasti, 0); Ok(()) } -/// flowgraph.c make_cfg_traversal_stack -fn make_cfg_traversal_stack(blocks: &mut [Block]) -> crate::InternalResult { - debug_assert!(!blocks.is_empty()); - let mut nblocks = 0; - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - blocks[current.idx()].visited = false; - nblocks += 1; - current = blocks[current.idx()].next; +/// assemble.c assemble_exception_table +fn assemble_exception_table( + instrs: &[InstructionSequenceEntry], +) -> crate::InternalResult> { + let mut table = Vec::new(); + vec_try_reserve_exact(&mut table, DEFAULT_LNOTAB_SIZE)?; + let mut handler = InstructionSequenceExceptHandlerInfo { + h_label: NO_EXCEPTION_HANDLER_LABEL, + start_depth: -1, + preserve_lasti: -1, + }; + let mut start = -1; + let mut ioffset = 0i32; + + for i in 0..instrs.len() { + let instr = &instrs[i]; + if instr.except_handler.h_label != handler.h_label { + if handler.h_label >= 0 { + let handler_offset = instrs[handler.h_label as usize].i_offset; + assemble_emit_exception_table_entry( + &mut table, + start, + ioffset, + handler_offset, + handler, + )?; + } + start = ioffset; + handler = instr.except_handler; + } + ioffset += instr.info.instr_size() as i32; + } + + if handler.h_label >= 0 { + let handler_offset = instrs[handler.h_label as usize].i_offset; + assemble_emit_exception_table_entry(&mut table, start, ioffset, handler_offset, handler)?; } - debug_assert!(nblocks > 0); - let mut stack = Vec::new(); - stack - .try_reserve_exact(nblocks) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - stack.resize(nblocks, BlockIdx::NULL); - let stack = CfgTraversalStack { stack, sp: 0 }; - debug_assert_eq!(stack.capacity(), nblocks); - Ok(stack) + + Ok(table.into_boxed_slice()) } -fn blocks_new_block(blocks: &mut Vec) -> crate::InternalResult { - blocks - .try_reserve(1) - .map_err(|_| InternalError::MalformedControlFlowGraph)?; - let block_idx = BlockIdx( - blocks - .len() - .to_u32() - .ok_or(InternalError::MalformedControlFlowGraph)?, - ); - blocks.push(Block::default()); - Ok(block_idx) +/// pycore_opcode_utils.h IS_CONDITIONAL_JUMP_OPCODE +fn is_conditional_jump_opcode(instr: AnyInstruction) -> bool { + matches!( + instr.real().map(Into::into), + Some( + Opcode::PopJumpIfFalse + | Opcode::PopJumpIfTrue + | Opcode::PopJumpIfNone + | Opcode::PopJumpIfNotNone + ) + ) } /// flowgraph.c struct _PyCfgBuilder struct CfgBuilder { - blocks: Vec, + blocks: Blocks, entry: BlockIdx, block_list: BlockIdx, current: BlockIdx, @@ -5888,9 +6112,9 @@ struct CfgBuilder { /// flowgraph.c cfg_builder_new_block fn cfg_builder_new_block(g: &mut CfgBuilder) -> crate::InternalResult { - let block = blocks_new_block(&mut g.blocks)?; - g.blocks[block.idx()].allocation_next = g.block_list; - g.blocks[block.idx()].cpython_label = InstructionSequenceLabel::NO_LABEL; + let block = g.blocks.blocks_new_block()?; + g.blocks[block].allocation_next = g.block_list; + g.blocks[block].cpython_label = InstructionSequenceLabel::NO_LABEL; g.block_list = block; Ok(block) } @@ -5898,7 +6122,7 @@ fn cfg_builder_new_block(g: &mut CfgBuilder) -> crate::InternalResult /// flowgraph.c cfg_builder_use_next_block fn cfg_builder_use_next_block(g: &mut CfgBuilder, block: BlockIdx) -> BlockIdx { debug_assert!(block != BlockIdx::NULL); - g.blocks[g.current.idx()].next = block; + g.blocks[g.current].next = block; g.current = block; block } @@ -5916,7 +6140,7 @@ fn init_cfg_builder(g: &mut CfgBuilder) -> crate::InternalResult<()> { /// flowgraph.c _PyCfgBuilder_New fn cfg_builder_new() -> crate::InternalResult { let mut builder = CfgBuilder { - blocks: Vec::new(), + blocks: Blocks::default(), entry: BlockIdx::NULL, block_list: BlockIdx::NULL, current: BlockIdx::NULL, @@ -5928,8 +6152,8 @@ fn cfg_builder_new() -> crate::InternalResult { /// flowgraph.c cfg_builder_current_block_is_terminated fn cfg_builder_current_block_is_terminated(g: &mut CfgBuilder) -> bool { - let block = &mut g.blocks[g.current.idx()]; - let last = basicblock_last_instr(block).copied(); + let block = &mut g.blocks[g.current]; + let last = block.basicblock_last_instr().copied(); if last.is_some_and(|last| last.instr.is_terminator()) { return true; } @@ -5947,7 +6171,7 @@ fn cfg_builder_current_block_is_terminated(g: &mut CfgBuilder) -> bool { fn cfg_builder_maybe_start_new_block(g: &mut CfgBuilder) -> crate::InternalResult<()> { if cfg_builder_current_block_is_terminated(g) { let block = cfg_builder_new_block(g)?; - g.blocks[block.idx()].cpython_label = g.current_label; + g.blocks[block].cpython_label = g.current_label; g.current_label = InstructionSequenceLabel::NO_LABEL; cfg_builder_use_next_block(g, block); } @@ -5966,17 +6190,17 @@ fn cfg_builder_use_label( /// flowgraph.c _PyCfgBuilder_Addop fn cfg_builder_addop(g: &mut CfgBuilder, info: InstructionInfo) -> crate::InternalResult<()> { cfg_builder_maybe_start_new_block(g)?; - basicblock_addop(&mut g.blocks[g.current.idx()], info) + g.blocks[g.current].basicblock_addop(info) } /// flowgraph.c cfg_builder_check fn cfg_builder_check(g: &CfgBuilder) -> bool { debug_assert!(g.entry != BlockIdx::NULL); - debug_assert!(g.blocks[g.entry.idx()].instruction_used != 0); + debug_assert!(g.blocks[g.entry].instruction_used != 0); let mut block = g.block_list; while block != BlockIdx::NULL { debug_assert!(block.idx() < g.blocks.len()); - let block_ref = &g.blocks[block.idx()]; + let block_ref = &g.blocks[block]; let has_instr_array = block_ref.instruction_allocation > 0; if has_instr_array { debug_assert!(block_ref.instruction_allocation > 0); @@ -6004,7 +6228,7 @@ fn cfg_builder_check_size(g: &CfgBuilder) -> crate::InternalResult<()> { while block != BlockIdx::NULL { debug_assert!(block.idx() < g.blocks.len()); nblocks += 1; - block = g.blocks[block.idx()].allocation_next; + block = g.blocks[block].allocation_next; } debug_assert_eq!(nblocks, g.blocks.len()); if nblocks > usize::MAX / core::mem::size_of::() { @@ -6014,7 +6238,7 @@ fn cfg_builder_check_size(g: &CfgBuilder) -> crate::InternalResult<()> { } /// flowgraph.c translate_jump_labels_to_targets -fn translate_jump_labels_to_targets(blocks: &mut [Block]) -> crate::InternalResult<()> { +fn translate_jump_labels_to_targets(blocks: &mut Blocks) -> crate::InternalResult<()> { let max_label = get_max_label(blocks); let label_count = (max_label + 1) as usize; if label_count > usize::MAX / core::mem::size_of::() { @@ -6026,7 +6250,7 @@ fn translate_jump_labels_to_targets(blocks: &mut [Block]) -> crate::InternalResu let mut block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let block = &blocks[block_idx.idx()]; + let block = &blocks[block_idx]; if is_label(block.cpython_label) { let label_id = block.cpython_label; debug_assert!(label_id.0 <= max_label); @@ -6037,20 +6261,17 @@ fn translate_jump_labels_to_targets(blocks: &mut [Block]) -> crate::InternalResu block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - for i in 0..blocks[block_idx.idx()].instruction_used { - let info = &mut blocks[block_idx.idx()].instructions[i]; + let next = blocks[block_idx].next; + for i in 0..blocks[block_idx].instruction_used { + let info = &mut blocks[block_idx].instructions[i]; debug_assert_eq!(info.target, BlockIdx::NULL); if info.instr.has_target() { let lbl = u32::from(info.arg) as i32; debug_assert!(lbl >= 0 && lbl <= max_label); let target = label_to_block[lbl as usize]; debug_assert!(target != BlockIdx::NULL); - debug_assert_eq!( - blocks[target.idx()].cpython_label, - InstructionSequenceLabel(lbl) - ); info.target = target; + debug_assert_eq!(blocks[target].cpython_label, InstructionSequenceLabel(lbl)); } } block_idx = next; @@ -6061,8 +6282,8 @@ fn translate_jump_labels_to_targets(blocks: &mut [Block]) -> crate::InternalResu /// flowgraph.c _PyCfg_FromInstructionSequence fn cfg_from_instruction_sequence( mut instr_sequence: InstructionSequence, -) -> crate::InternalResult> { - instruction_sequence_apply_label_map(&mut instr_sequence)?; +) -> crate::InternalResult { + instruction_sequence_apply_label_map(&mut instr_sequence); let mut builder = cfg_builder_new()?; for i in 0..instr_sequence.instr_used { @@ -6140,27 +6361,26 @@ fn cfg_from_instruction_sequence( /// flowgraph.c maybe_push fn maybe_push( - blocks: &mut [Block], + blocks: &mut Blocks, worklist: &mut CfgTraversalStack, block: BlockIdx, unsafe_mask: u64, ) { debug_assert!(block != BlockIdx::NULL); - let idx = block.idx(); - let both = blocks[idx].unsafe_locals_mask | unsafe_mask; - if blocks[idx].unsafe_locals_mask != both { - blocks[idx].unsafe_locals_mask = both; - if !blocks[idx].visited { + let both = blocks[block].unsafe_locals_mask | unsafe_mask; + if blocks[block].unsafe_locals_mask != both { + blocks[block].unsafe_locals_mask = both; + if !blocks[block].visited { worklist.push(block); - blocks[idx].visited = true; + blocks[block].visited = true; } } } /// flowgraph.c scan_block_for_locals fn scan_block_for_locals( - blocks: &mut [Block], + blocks: &mut Blocks, block_idx: BlockIdx, worklist: &mut CfgTraversalStack, ) { @@ -6214,13 +6434,13 @@ fn scan_block_for_locals( } let next = blocks[idx].next; - if next != BlockIdx::NULL && bb_has_fallthrough(&blocks[idx]) { + if next != BlockIdx::NULL && blocks[idx].bb_has_fallthrough() { maybe_push(blocks, worklist, next, unsafe_mask); } - let last = basicblock_last_instr(&blocks[idx]).copied(); + let last = blocks[idx].basicblock_last_instr().copied(); if let Some(last) = last - && is_jump(&last) + && last.is_jump() { let target = last.target; debug_assert!(target != BlockIdx::NULL); @@ -6229,7 +6449,7 @@ fn scan_block_for_locals( } /// flowgraph.c fast_scan_many_locals -fn fast_scan_many_locals(blocks: &mut [Block], nlocals: usize) -> crate::InternalResult<()> { +fn fast_scan_many_locals(blocks: &mut Blocks, nlocals: usize) -> crate::InternalResult<()> { debug_assert!(nlocals > LOCAL_UNSAFE_MASK_BITS); let mut states = Vec::new(); states @@ -6240,8 +6460,8 @@ fn fast_scan_many_locals(blocks: &mut [Block], nlocals: usize) -> crate::Interna let mut current = BlockIdx(0); while current != BlockIdx::NULL { blocknum += 1; - for i in 0..blocks[current.idx()].instruction_used { - let info = &mut blocks[current.idx()].instructions[i]; + for i in 0..blocks[current].instruction_used { + let info = &mut blocks[current].instructions[i]; debug_assert!(!matches!(info.instr.real(), Some(Instruction::ExtendedArg))); let arg = u32::from(info.arg) as usize; if arg < LOCAL_UNSAFE_MASK_BITS { @@ -6270,14 +6490,14 @@ fn fast_scan_many_locals(blocks: &mut [Block], nlocals: usize) -> crate::Interna _ => {} } } - current = blocks[current.idx()].next; + current = blocks[current].next; } Ok(()) } /// flowgraph.c add_checks_for_loads_of_uninitialized_variables fn add_checks_for_loads_of_uninitialized_variables( - blocks: &mut [Block], + blocks: &mut Blocks, mut nlocals: usize, nparams: usize, ) -> crate::InternalResult<()> { @@ -6290,7 +6510,7 @@ fn add_checks_for_loads_of_uninitialized_variables( nlocals = LOCAL_UNSAFE_MASK_BITS; } - let mut worklist = make_cfg_traversal_stack(blocks)?; + let mut worklist = blocks.make_cfg_traversal_stack()?; let mut start_mask = 0u64; for i in nparams..nlocals { start_mask |= 1u64 << i; @@ -6300,267 +6520,60 @@ fn add_checks_for_loads_of_uninitialized_variables( let mut current = BlockIdx(0); while current != BlockIdx::NULL { scan_block_for_locals(blocks, current, &mut worklist); - current = blocks[current.idx()].next; + current = blocks[current].next; } while let Some(block_idx) = worklist.pop() { - blocks[block_idx.idx()].visited = false; + blocks[block_idx].visited = false; scan_block_for_locals(blocks, block_idx, &mut worklist); } Ok(()) } /// Follow chain of empty blocks to find first non-empty block. -fn next_nonempty_block(blocks: &[Block], mut idx: BlockIdx) -> BlockIdx { - while idx != BlockIdx::NULL && blocks[idx.idx()].instruction_used == 0 { - idx = blocks[idx.idx()].next; +fn next_nonempty_block(blocks: &Blocks, mut idx: BlockIdx) -> BlockIdx { + while idx != BlockIdx::NULL && blocks[idx].instruction_used == 0 { + idx = blocks[idx].next; } idx } -fn instruction_lineno(instr: &InstructionInfo) -> i32 { - match instr.lineno_override { - Some(LINE_ONLY_LOCATION_OVERRIDE) | None => instr.location.line.get() as i32, - Some(lineno) => lineno, - } -} - -fn instruction_is_no_location(instr: &InstructionInfo) -> bool { - instruction_lineno(instr) == NO_LOCATION_OVERRIDE -} - -/// flowgraph.c basicblock_nofallthrough -fn basicblock_nofallthrough(block: &Block) -> bool { - let last = basicblock_last_instr(block); - last.is_some_and(|last| last.instr.is_scope_exit() || last.instr.is_unconditional_jump()) -} - -/// flowgraph.c BB_NO_FALLTHROUGH -fn bb_no_fallthrough(block: &Block) -> bool { - basicblock_nofallthrough(block) -} - -/// flowgraph.c BB_HAS_FALLTHROUGH -fn bb_has_fallthrough(block: &Block) -> bool { - !bb_no_fallthrough(block) -} - /// flowgraph.c add_checks_for_loads_of_uninitialized_variables uses uint64_t masks. const LOCAL_UNSAFE_MASK_BITS: usize = 64; /// flowgraph.c MAX_COPY_SIZE const MAX_COPY_SIZE: usize = 4; -/// flowgraph.c is_jump -fn is_jump(instr: &InstructionInfo) -> bool { - instr.instr.has_jump() -} - -/// flowgraph.c is_block_push -fn is_block_push(instr: &InstructionInfo) -> bool { - instr.instr.is_block_push() -} - -/// flowgraph.c basicblock_returns -#[cfg(test)] -fn basicblock_returns(block: &Block) -> bool { - let last = basicblock_last_instr(block); - if let Some(last) = last { - matches!(last.instr.real(), Some(Instruction::ReturnValue)) - } else { - false - } -} - -/// flowgraph.c basicblock_exits_scope -fn basicblock_exits_scope(block: &Block) -> bool { - let last = basicblock_last_instr(block); - last.is_some_and(|last| last.instr.is_scope_exit()) -} - -/// flowgraph.c is_exit_or_eval_check_without_lineno -fn is_exit_or_eval_check_without_lineno(block: &Block) -> bool { - if basicblock_exits_scope(block) || basicblock_has_eval_break(block) { - basicblock_has_no_lineno(block) - } else { - false - } -} - -/// flowgraph.c basicblock_has_eval_break -fn basicblock_has_eval_break(block: &Block) -> bool { - let mut i = 0; - while i < block.instruction_used { - if block.instructions[i].instr.has_eval_break() { - return true; - } - i += 1; - } - false -} - -/// flowgraph.c basicblock_has_no_lineno -fn basicblock_has_no_lineno(block: &Block) -> bool { - let mut i = 0; - while i < block.instruction_used { - if instruction_lineno(&block.instructions[i]) >= 0 { - return false; - } - i += 1; - } - true -} - -/// flowgraph.c copy_basicblock -fn copy_basicblock( - blocks: &mut Vec, - block_idx: BlockIdx, -) -> crate::InternalResult { - debug_assert!(bb_no_fallthrough(&blocks[block_idx.idx()])); - let result = blocks_new_block(blocks)?; - basicblock_append_block_instructions(blocks, result, block_idx)?; - Ok(result) -} - /// flowgraph.c get_max_label -fn get_max_label(blocks: &[Block]) -> i32 { +fn get_max_label(blocks: &Blocks) -> i32 { let mut lbl = -1; let mut current = BlockIdx(0); while current != BlockIdx::NULL { - let cpython_label = blocks[current.idx()].cpython_label; + let cpython_label = blocks[current].cpython_label; lbl = lbl.max(cpython_label.0); - current = blocks[current.idx()].next; + current = blocks[current].next; } lbl } -fn duplicate_exits_without_lineno(blocks: &mut Vec) -> crate::InternalResult<()> { - let mut next_lbl = get_max_label(blocks) + 1; - - let entryblock = BlockIdx(0); - let mut b = entryblock; - while b != BlockIdx::NULL { - let Some(last) = basicblock_last_instr(&blocks[b.idx()]).copied() else { - b = blocks[b.idx()].next; - continue; - }; - if is_jump(&last) { - debug_assert!(last.target != BlockIdx::NULL); - let target = next_nonempty_block(blocks, last.target); - debug_assert!(target != BlockIdx::NULL); - if is_exit_or_eval_check_without_lineno(&blocks[target.idx()]) - && blocks[target.idx()].predecessors > 1 - { - let new_target = copy_basicblock(blocks, target)?; - instr_set_location( - &mut blocks[new_target.idx()].instructions[0], - instr_location(&last), - ); - let last_mut = basicblock_last_instr_mut(&mut blocks[b.idx()]).unwrap(); - last_mut.target = new_target; - blocks[target.idx()].predecessors -= 1; - blocks[new_target.idx()].predecessors = 1; - blocks[new_target.idx()].next = blocks[target.idx()].next; - blocks[new_target.idx()].cpython_label = InstructionSequenceLabel(next_lbl); - next_lbl += 1; - blocks[target.idx()].next = new_target; - } - } - b = blocks[b.idx()].next; - } - - b = entryblock; - while b != BlockIdx::NULL { - let next = blocks[b.idx()].next; - if bb_has_fallthrough(&blocks[b.idx()]) - && next != BlockIdx::NULL - && blocks[b.idx()].instruction_used != 0 - && is_exit_or_eval_check_without_lineno(&blocks[next.idx()]) - { - let last = *basicblock_last_instr(&blocks[b.idx()]).expect("block has instructions"); - instr_set_location( - &mut blocks[next.idx()].instructions[0], - instr_location(&last), - ); - } - b = blocks[b.idx()].next; - } - Ok(()) -} - -fn propagate_line_numbers(blocks: &mut [Block]) { - let mut current = BlockIdx(0); - while current != BlockIdx::NULL { - let idx = current.idx(); - let Some(last) = basicblock_last_instr(&blocks[idx]).copied() else { - current = blocks[idx].next; - continue; - }; - - let mut prev_location = no_instruction_location(); - for i in 0..blocks[idx].instruction_used { - if instruction_is_no_location(&blocks[idx].instructions[i]) { - instr_set_location(&mut blocks[idx].instructions[i], prev_location); - } else { - prev_location = instr_location(&blocks[idx].instructions[i]); - } - } - - let next = blocks[idx].next; - if bb_has_fallthrough(&blocks[idx]) { - debug_assert!(next != BlockIdx::NULL); - if next != BlockIdx::NULL - && blocks[next.idx()].predecessors == 1 - && blocks[next.idx()].instruction_used != 0 - && instruction_is_no_location(&blocks[next.idx()].instructions[0]) - { - instr_set_location(&mut blocks[next.idx()].instructions[0], prev_location); - } - } - - if is_jump(&last) { - let target = last.target; - debug_assert!(target != BlockIdx::NULL); - if blocks[target.idx()].predecessors == 1 { - let instr = basicblock_raw_first_instr_mut(&mut blocks[target.idx()]); - if instruction_is_no_location(instr) { - instr_set_location(instr, prev_location); - } - } - } - current = blocks[current.idx()].next; - } -} - -fn resolve_line_numbers( - blocks: &mut Vec, - _firstlineno: OneIndexed, -) -> crate::InternalResult<()> { - duplicate_exits_without_lineno(blocks)?; - propagate_line_numbers(blocks); - Ok(()) -} - /// flowgraph.c make_except_stack -#[allow(clippy::unnecessary_wraps)] -fn make_except_stack() -> crate::InternalResult { +fn make_except_stack() -> CfgExceptStack { let handlers = [BlockIdx::NULL; CO_MAXBLOCKS + 2]; debug_assert_eq!(handlers[0], BlockIdx::NULL); - Ok(CfgExceptStack { handlers, depth: 0 }) + CfgExceptStack { handlers, depth: 0 } } /// flowgraph.c copy_except_stack -#[allow(clippy::unnecessary_wraps)] -fn copy_except_stack(stack: &CfgExceptStack) -> crate::InternalResult { +fn copy_except_stack(stack: &CfgExceptStack) -> CfgExceptStack { debug_assert!(stack.depth <= CO_MAXBLOCKS + 1); - Ok(CfgExceptStack { + CfgExceptStack { handlers: stack.handlers, depth: stack.depth, - }) + } } /// flowgraph.c except_stack_top -fn except_stack_top(stack: &CfgExceptStack, blocks: &[Block]) -> Option { +fn except_stack_top(stack: &CfgExceptStack, blocks: &Blocks) -> Option { debug_assert!(stack.depth <= CO_MAXBLOCKS + 1); let handler_block = stack.handlers[stack.depth]; if handler_block == BlockIdx::NULL { @@ -6568,7 +6581,7 @@ fn except_stack_top(stack: &CfgExceptStack, blocks: &[Block]) -> Option Option Option { - debug_assert!(is_block_push(&setup)); + debug_assert!(setup.is_block_push()); let instr = setup.instr; let target = setup.target; debug_assert!(target != BlockIdx::NULL); @@ -6586,7 +6599,7 @@ fn push_except_block( instr.pseudo(), Some(PseudoInstruction::SetupWith { .. } | PseudoInstruction::SetupCleanup { .. }) ) { - blocks[target.idx()].preserve_lasti = true; + blocks[target].preserve_lasti = true; } debug_assert!(stack.depth <= CO_MAXBLOCKS); stack.depth += 1; @@ -6596,19 +6609,19 @@ fn push_except_block( } /// flowgraph.c pop_except_block -fn pop_except_block(stack: &mut CfgExceptStack, blocks: &[Block]) -> Option { +fn pop_except_block(stack: &mut CfgExceptStack, blocks: &Blocks) -> Option { debug_assert!(stack.depth > 0); stack.depth -= 1; debug_assert!(stack.depth <= CO_MAXBLOCKS); except_stack_top(stack, blocks) } -pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalResult<()> { - let mut todo = make_cfg_traversal_stack(blocks)?; +pub(crate) fn label_exception_targets(blocks: &mut Blocks) -> crate::InternalResult<()> { + let mut todo = blocks.make_cfg_traversal_stack()?; todo.push(BlockIdx(0)); blocks[0].visited = true; - blocks[0].except_stack = Some(make_except_stack()?); + blocks[0].except_stack = Some(make_except_stack()); while let Some(block_idx) = todo.pop() { let bi = block_idx.idx(); @@ -6630,14 +6643,14 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe let target = info.target; let arg = info.arg; - if is_block_push(&info) { + if info.is_block_push() { debug_assert!(target != BlockIdx::NULL); - if !blocks[target.idx()].visited { - blocks[target.idx()].except_stack = Some(copy_except_stack( + if !blocks[target].visited { + blocks[target].except_stack = Some(copy_except_stack( stack.as_ref().expect("active exception stack"), - )?); + )); todo.push(target); - blocks[target.idx()].visited = true; + blocks[target].visited = true; } handler = push_except_block( stack.as_mut().expect("active exception stack"), @@ -6646,8 +6659,8 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe ); } else if instr.is_pop_block() { handler = pop_except_block(stack.as_mut().expect("active exception stack"), blocks); - set_to_nop(&mut blocks[bi].instructions[i]); - } else if is_jump(&blocks[bi].instructions[i]) { + blocks[bi].instructions[i].set_to_nop(); + } else if blocks[bi].instructions[i].is_jump() { blocks[bi].instructions[i].except_handler = handler; debug_assert_eq!(i, instr_count - 1); @@ -6655,20 +6668,20 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe // when this block can also fall through, otherwise transfer it // to the jump target. debug_assert!(target != BlockIdx::NULL); - if !blocks[target.idx()].visited { - if bb_has_fallthrough(&blocks[bi]) { - blocks[target.idx()].except_stack = Some(copy_except_stack( + if !blocks[target].visited { + if blocks[bi].bb_has_fallthrough() { + blocks[target].except_stack = Some(copy_except_stack( stack.as_ref().expect("active exception stack"), - )?); + )); } else { - blocks[target.idx()].except_stack = stack.take(); + blocks[target].except_stack = stack.take(); stack_transferred = true; todo.push(target); - blocks[target.idx()].visited = true; + blocks[target].visited = true; break; } todo.push(target); - blocks[target.idx()].visited = true; + blocks[target].visited = true; } } else if matches!(instr.real(), Some(Instruction::YieldValue { .. })) { blocks[bi].instructions[i].except_handler = handler; @@ -6691,12 +6704,12 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe } let next = blocks[bi].next; - if !stack_transferred && bb_has_fallthrough(&blocks[bi]) { + if !stack_transferred && blocks[bi].bb_has_fallthrough() { debug_assert!(next != BlockIdx::NULL); - if next != BlockIdx::NULL && !blocks[next.idx()].visited { - blocks[next.idx()].except_stack = stack.take(); + if next != BlockIdx::NULL && !blocks[next].visited { + blocks[next].except_stack = stack.take(); todo.push(next); - blocks[next.idx()].visited = true; + blocks[next].visited = true; } } } @@ -6704,7 +6717,7 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe { let mut block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let block = &blocks[block_idx.idx()]; + let block = &blocks[block_idx]; debug_assert!(block.except_stack.is_none()); block_idx = block.next; } @@ -6714,15 +6727,15 @@ pub(crate) fn label_exception_targets(blocks: &mut [Block]) -> crate::InternalRe /// Convert remaining pseudo ops to real instructions or NOP. /// flowgraph.c convert_pseudo_ops -pub(crate) fn convert_pseudo_ops(blocks: &mut [Block]) -> crate::InternalResult<()> { +pub(crate) fn convert_pseudo_ops(blocks: &mut Blocks) -> crate::InternalResult<()> { let mut block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx.idx()]; + let next = blocks[block_idx].next; + let block = &mut blocks[block_idx]; for i in 0..block.instruction_used { let info = &mut block.instructions[i]; - if is_block_push(info) { - set_to_nop(info); + if info.is_block_push() { + info.set_to_nop(); } else if matches!( info.instr.pseudo(), Some(PseudoInstruction::LoadClosure { .. }) @@ -6740,21 +6753,17 @@ pub(crate) fn convert_pseudo_ops(blocks: &mut [Block]) -> crate::InternalResult< PseudoOpcode::StoreFastMaybeNull, Opcode::StoreFast )); - info.instr = Instruction::StoreFast { - var_num: Arg::marker(), - } - .into(); + info.instr = Opcode::StoreFast.into(); } } block_idx = next; } // CPython flowgraph.c::convert_pseudo_ops() finishes by calling // remove_redundant_nops_and_jumps(). - remove_redundant_nops_and_jumps(blocks) + blocks.remove_redundant_nops_and_jumps() } /// flowgraph.c build_cellfixedoffsets -#[allow(clippy::needless_range_loop)] pub(crate) fn build_cellfixedoffsets( metadata: &CodeUnitMetadata, ) -> crate::InternalResult> { @@ -6765,27 +6774,28 @@ pub(crate) fn build_cellfixedoffsets( let mut fixed = Vec::new(); vec_try_reserve_exact(&mut fixed, noffsets)?; fixed.resize(noffsets, 0); - for i in 0..noffsets { - fixed[i] = (nlocals + i) as i32; + + for (i, item) in fixed.iter_mut().enumerate().take(noffsets) { + *item = (nlocals + i) as i32; } - for oldindex in 0..ncellvars { + + for (oldindex, cell) in fixed.iter_mut().enumerate().take(ncellvars) { let varname = metadata .cellvars .get_index(oldindex) .expect("cellvar index is in range"); if let Some(varindex) = metadata.varnames.get_index_of(varname) { let argoffset = varindex as i32; - fixed[oldindex] = argoffset; + *cell = argoffset; } } Ok(fixed) } /// flowgraph.c fix_cell_offsets -#[allow(clippy::needless_range_loop)] pub(crate) fn fix_cell_offsets( metadata: &CodeUnitMetadata, - blocks: &mut [Block], + blocks: &mut Blocks, cellfixedoffsets: &mut [i32], ) -> usize { let nlocals = metadata.varnames.len(); @@ -6795,9 +6805,9 @@ pub(crate) fn fix_cell_offsets( debug_assert_eq!(cellfixedoffsets.len(), noffsets); let mut numdropped = 0usize; - for i in 0..noffsets { - if cellfixedoffsets[i] == (i + nlocals) as i32 { - cellfixedoffsets[i] -= numdropped as i32; + for (i, cell) in cellfixedoffsets.iter_mut().enumerate().take(noffsets) { + if *cell == (i + nlocals) as i32 { + *cell -= numdropped as i32; } else { numdropped += 1; } @@ -6805,8 +6815,8 @@ pub(crate) fn fix_cell_offsets( let mut block_idx = BlockIdx(0); while block_idx != BlockIdx::NULL { - let next = blocks[block_idx.idx()].next; - let block = &mut blocks[block_idx.idx()]; + let next = blocks[block_idx].next; + let block = &mut blocks[block_idx]; for i in 0..block.instruction_used { let inst = &mut block.instructions[i]; debug_assert!( @@ -6840,6 +6850,58 @@ pub(crate) fn fix_cell_offsets( #[cfg(test)] mod tests { use super::*; + use rustpython_compiler_core::bytecode::Arg; + + fn int_const(value: i32) -> ConstantData { + ConstantData::Integer { + value: BigInt::from(value), + } + } + + fn nan_const() -> ConstantData { + ConstantData::Float { value: f64::NAN } + } + + #[test] + fn constant_pool_frozenset_key_ignores_order_and_duplicates_like_cpython() { + let mut pool = ConstantPool::default(); + let (first, inserted) = pool.insert_full(ConstantData::Frozenset { + elements: vec![int_const(1), int_const(2)], + }); + assert_eq!(first, 0); + assert!(inserted); + + let (second, inserted) = pool.insert_full(ConstantData::Frozenset { + elements: vec![int_const(2), int_const(1), int_const(1)], + }); + assert_eq!( + second, first, + "CPython _PyCode_ConstantKey uses frozenset item keys, not insertion order" + ); + assert!(!inserted); + assert!(matches!( + &pool.constants[first], + ConstantData::Frozenset { elements } if elements.len() == 2 + )); + } + + #[test] + fn constant_pool_frozenset_key_preserves_nan_duplicates_like_cpython() { + let mut pool = ConstantPool::default(); + let (idx, inserted) = pool.insert_full(ConstantData::Frozenset { + elements: vec![nan_const(), nan_const()], + }); + + assert_eq!(idx, 0); + assert!(inserted); + assert!(matches!( + &pool.constants[idx], + ConstantData::Frozenset { elements } + if elements.iter().filter(|constant| { + matches!(constant, ConstantData::Float { value } if value.is_nan()) + }).count() == 2 + )); + } fn test_location(line: u32) -> SourceLocation { SourceLocation { @@ -6874,8 +6936,17 @@ mod tests { instr } + fn test_true_cond_jump(target: BlockIdx, line: u32) -> InstructionInfo { + let mut instr = test_instr(Instruction::Nop, line); + instr.instr = PseudoOpcode::JumpIfTrue.into(); + instr.target = target; + instr + } + fn test_block_push(block: &mut Block, info: InstructionInfo) { - let off = basicblock_next_instr(block).expect("test block instruction slot"); + let off = block + .basicblock_next_instr() + .expect("test block instruction slot"); block.instructions[off] = info; } @@ -6884,7 +6955,7 @@ mod tests { flags: CodeFlags::empty(), source_path: "source_path".to_owned(), private: None, - blocks: vec![block], + blocks: Blocks::from([block]), current_block: BlockIdx::new(0), instr_sequence: instruction_sequence_new(), instr_sequence_label_map: InstructionSequenceLabelMap::new(), @@ -6958,19 +7029,16 @@ mod tests { #[test] fn except_stack_tracks_cpython_depth_and_handler_slots() { - let mut stack = make_except_stack().unwrap(); + let mut stack = make_except_stack(); assert_eq!(stack.depth, 0); assert_eq!(stack.handlers.len(), CO_MAXBLOCKS + 2); assert_eq!(stack.handlers[0], BlockIdx::NULL); - let mut blocks = vec![Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default()]); assert!(except_stack_top(&stack, &blocks).is_none()); let setup = InstructionInfo { - instr: PseudoInstruction::SetupWith { - delta: Arg::marker(), - } - .into(), + instr: PseudoOpcode::SetupWith.into(), arg: OpArg::new(0), target: BlockIdx::new(1), location: SourceLocation::default(), @@ -6985,7 +7053,7 @@ mod tests { assert!(handler.preserve_lasti); assert!(blocks[1].preserve_lasti); - let copy = copy_except_stack(&stack).unwrap(); + let copy = copy_except_stack(&stack); assert_eq!(copy.depth, stack.depth); assert_eq!(copy.handlers, stack.handlers); @@ -7027,12 +7095,12 @@ mod tests { #[test] fn cfg_traversal_stack_resets_visited_and_allocates_for_blocks() { - let mut blocks = vec![Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default()]); blocks[0].next = BlockIdx::new(1); blocks[0].visited = true; blocks[1].visited = true; - let mut stack = make_cfg_traversal_stack(&mut blocks).unwrap(); + let mut stack = blocks.make_cfg_traversal_stack().unwrap(); assert!(!blocks[0].visited); assert!(!blocks[1].visited); assert!(stack.capacity() >= 2); @@ -7128,9 +7196,10 @@ mod tests { let mut stale = test_instr(Instruction::Nop, 11); stale.except_handler = Some(handler); test_block_push(&mut block, stale); - basicblock_clear(&mut block); + block.basicblock_clear(); - basicblock_addop(&mut block, test_instr(Instruction::PopTop, 12)) + block + .basicblock_addop(test_instr(Instruction::PopTop, 12)) .expect("basicblock_addop succeeds"); // CPython `basicblock_addop()` writes opcode/oparg/target/location into @@ -7144,14 +7213,16 @@ mod tests { fn basicblock_next_instr_tracks_cpython_c_array_allocation() { let mut block = Block::default(); for i in 0..15 { - basicblock_addop(&mut block, test_instr(Instruction::PopTop, 10 + i)) + block + .basicblock_addop(test_instr(Instruction::PopTop, 10 + i)) .expect("basicblock_addop succeeds"); } assert_eq!(block.instruction_allocation, DEFAULT_BLOCK_SIZE); // CPython calls `_Py_CArray_EnsureCapacity(b_iused + 1)`, so the 16th // instruction expands a 16-slot array to 32 before returning offset 15. - basicblock_addop(&mut block, test_instr(Instruction::PopTop, 25)) + block + .basicblock_addop(test_instr(Instruction::PopTop, 25)) .expect("basicblock_addop succeeds"); assert_eq!(block.instruction_allocation, DEFAULT_BLOCK_SIZE * 2); } @@ -7169,7 +7240,8 @@ mod tests { test_block_push(&mut block, stale); block.instruction_used = 1; - basicblock_insert_instruction(&mut block, 0, test_instr(Instruction::PopTop, 23)) + block + .basicblock_insert_instruction(0, test_instr(Instruction::PopTop, 23)) .expect("basicblock_insert_instruction succeeds"); // CPython `basicblock_insert_instruction()` also obtains a slot with @@ -7190,8 +7262,9 @@ mod tests { stale.except_handler = Some(handler); test_block_push(&mut block, stale); - basicblock_clear(&mut block); - basicblock_addop(&mut block, test_instr(Instruction::Nop, 32)) + block.basicblock_clear(); + block + .basicblock_addop(test_instr(Instruction::Nop, 32)) .expect("basicblock_addop succeeds"); // CPython `remove_unreachable()` sets `b_iused = 0` without clearing the @@ -7213,9 +7286,10 @@ mod tests { test_block_push(&mut block, stale); } - basicblock_clear(&mut block); + block.basicblock_clear(); for i in 0..3 { - basicblock_addop(&mut block, test_instr(Instruction::PopTop, 38 + i)) + block + .basicblock_addop(test_instr(Instruction::PopTop, 38 + i)) .expect("basicblock_addop succeeds"); } @@ -7241,14 +7315,15 @@ mod tests { handler_block: BlockIdx::new(5), preserve_lasti: false, }; - let mut blocks = vec![Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default()]); let mut stale = test_instr(Instruction::Nop, 41); stale.except_handler = Some(handler); test_block_push(&mut blocks[0], stale); - basicblock_clear(&mut blocks[0]); + blocks[0].basicblock_clear(); test_block_push(&mut blocks[1], test_instr(Instruction::PopTop, 42)); - basicblock_append_block_instructions(&mut blocks, BlockIdx::new(0), BlockIdx::new(1)) + blocks + .basicblock_append_block_instructions(BlockIdx::new(0), BlockIdx::new(1)) .expect("basicblock_append_block_instructions succeeds"); // CPython `basicblock_append_instructions()` obtains a slot with @@ -7261,16 +7336,17 @@ mod tests { #[test] fn instr_set_op0_nop_preserves_cpython_stale_target() { let mut info = test_jump(BlockIdx::new(1), 50); - set_to_nop(&mut info); + info.set_to_nop(); assert_eq!(info.target, BlockIdx::new(1)); - let mut blocks = vec![Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default()]); test_block_push(&mut blocks[0], info); blocks[0].next = BlockIdx::new(1); let mut instr_sequence = instruction_sequence_new(); - cfg_to_instruction_sequence(&mut blocks, &mut instr_sequence) + blocks + .cfg_to_instruction_sequence(&mut instr_sequence) .expect("non-target NOP should ignore stale CPython i_target"); } @@ -7280,23 +7356,18 @@ mod tests { fn cfg_to_instruction_sequence_requires_target_for_target_opcodes() { let mut block = Block::default(); test_block_push(&mut block, test_jump(BlockIdx::NULL, 51)); - let mut blocks = vec![block]; + let mut blocks = Blocks::from([block]); let mut instr_sequence = instruction_sequence_new(); - let _ = cfg_to_instruction_sequence(&mut blocks, &mut instr_sequence); + let _ = blocks.cfg_to_instruction_sequence(&mut instr_sequence); } #[test] fn static_swaps_respect_cpython_no_location_line_boundary() { let mut block = Block::default(); - let mut swap = test_instr(Instruction::Swap { i: Arg::marker() }, 60); + let mut swap = test_instr(Opcode::Swap.into(), 60); swap.arg = OpArg::new(2); - let mut store = test_instr( - Instruction::StoreFast { - var_num: Arg::marker(), - }, - 60, - ); + let mut store = test_instr(Opcode::StoreFast.into(), 60); store.arg = OpArg::new(0); let mut pop = test_instr(Instruction::PopTop, 60); pop.lineno_override = Some(NO_LOCATION_OVERRIDE); @@ -7304,7 +7375,9 @@ mod tests { test_block_push(&mut block, info); } - apply_static_swaps_block(&mut block).expect("apply_static_swaps_block succeeds"); + block + .apply_static_swaps_block() + .expect("apply_static_swaps_block succeeds"); // CPython `next_swappable_instruction()` compares `i_loc.lineno` // directly, so a following NO_LOCATION swaperand does not match the @@ -7323,14 +7396,9 @@ mod tests { )); let mut block = Block::default(); - let mut swap = test_instr(Instruction::Swap { i: Arg::marker() }, 70); + let mut swap = test_instr(Opcode::Swap.into(), 70); swap.arg = OpArg::new(2); - let mut store = test_instr( - Instruction::StoreFast { - var_num: Arg::marker(), - }, - 70, - ); + let mut store = test_instr(Opcode::StoreFast.into(), 70); store.arg = OpArg::new(0); store.lineno_override = Some(NO_LOCATION_OVERRIDE); let pop = test_instr(Instruction::PopTop, 71); @@ -7338,37 +7406,31 @@ mod tests { test_block_push(&mut block, info); } - apply_static_swaps_block(&mut block).expect("apply_static_swaps_block succeeds"); + block + .apply_static_swaps_block() + .expect("apply_static_swaps_block succeeds"); // Conversely, when the first swaperand has NO_LOCATION, CPython passes // `-1` as the line filter and does not enforce a boundary. assert!(matches!( - block.instructions[0].instr.real(), - Some(Instruction::Nop) + block.instructions[0].instr.real_opcode(), + Some(Opcode::Nop) )); assert!(matches!( - block.instructions[1].instr.real(), - Some(Instruction::PopTop) + block.instructions[1].instr.real_opcode(), + Some(Opcode::PopTop) )); assert!(matches!( - block.instructions[2].instr.real(), - Some(Instruction::StoreFast { .. }) + block.instructions[2].instr.real_opcode(), + Some(Opcode::StoreFast) )); } #[test] fn optimize_load_const_tracks_cpython_copy_of_load_const() { let mut block = Block::default(); - test_block_push( - &mut block, - test_instr( - Instruction::LoadConst { - consti: Arg::marker(), - }, - 80, - ), - ); - let mut copy = test_instr(Instruction::Copy { i: Arg::marker() }, 80); + test_block_push(&mut block, test_instr(Opcode::LoadConst.into(), 80)); + let mut copy = test_instr(Opcode::Copy.into(), 80); copy.arg = OpArg::new(1); test_block_push(&mut block, copy); test_block_push(&mut block, test_instr(Instruction::ToBool, 80)); @@ -7407,25 +7469,65 @@ mod tests { } #[test] - fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() { + fn optimize_load_const_pseudo_opcode_breaks_effective_load_const() { let mut block = Block::default(); test_block_push( &mut block, test_instr( - Instruction::LoadFast { - var_num: Arg::marker(), + Instruction::LoadConst { + consti: Arg::marker(), }, - 10, + 90, ), ); + test_block_push(&mut block, test_true_cond_jump(BlockIdx::new(0), 90)); + let mut copy = test_instr(Instruction::Copy { i: Arg::marker() }, 90); + copy.arg = OpArg::new(1); + test_block_push(&mut block, copy); + test_block_push(&mut block, test_instr(Instruction::ToBool, 90)); + + let mut code = test_code_info(block); + let (const_idx, _) = code.metadata.consts.insert_full(ConstantData::Tuple { + elements: vec![ConstantData::Integer { + value: BigInt::from(1), + }], + }); + code.blocks[0].instructions[0].arg = OpArg::new(const_idx as u32); + + optimize_load_const(&mut code.metadata, &mut code.blocks) + .expect("optimize_load_const succeeds"); + + // `basicblock_optimize_load_const()` assigns the current + // pseudo opcode to its effective opcode slot, so the following COPY 1 + // is not treated as a copy of the earlier LOAD_CONST. + assert!(matches!( + code.blocks[0].instructions[1].instr.pseudo(), + Some(PseudoInstruction::Jump { .. }) + )); + assert!(matches!( + code.blocks[0].instructions[2].instr.real(), + Some(Instruction::Copy { .. }) + )); + assert!(matches!( + code.blocks[0].instructions[3].instr.real(), + Some(Instruction::ToBool) + )); + } + + #[test] + fn optimize_load_fast_records_no_input_opcode_ref_at_cpython_produced_index() { + let mut block = Block::default(); + test_block_push(&mut block, test_instr(Opcode::LoadFast.into(), 10)); test_block_push(&mut block, test_instr(Instruction::GetLen, 10)); - let mut swap = test_instr(Instruction::Swap { i: Arg::marker() }, 10); + let mut swap = test_instr(Opcode::Swap.into(), 10); swap.arg = OpArg::new(2); test_block_push(&mut block, swap); test_block_push(&mut block, test_instr(Instruction::PopTop, 10)); let mut code = test_code_info(block); - optimize_load_fast(&mut code.blocks).expect("optimize_load_fast succeeds"); + code.blocks + .optimize_load_fast() + .expect("optimize_load_fast succeeds"); // CPython `optimize_load_fast()` shadows the outer instruction index in // the produced-value loop for GET_LEN, so the produced ref is recorded @@ -7467,12 +7569,7 @@ mod tests { let mut mortal = test_instr(Instruction::Nop, 90); mortal.instr = Opcode::LoadConstMortal.into(); mortal.arg = OpArg::new(right as u32); - let mut build = test_instr( - Instruction::BuildTuple { - count: Arg::marker(), - }, - 90, - ); + let mut build = test_instr(Opcode::BuildTuple.into(), 90); build.arg = OpArg::new(2); let mut block = Block::default(); for info in [immortal, mortal, build] { @@ -7506,10 +7603,28 @@ mod tests { )); } + #[test] + fn empty_tuple_repeat_folds_negative_count_like_cpython() { + let folded = const_folding_safe_multiply( + &ConstantData::Tuple { + elements: Vec::new(), + }, + &ConstantData::Integer { + value: BigInt::from(-1), + }, + ) + .expect("CPython skips repeat-count checks for empty tuples"); + + assert!(matches!( + folded, + ConstantData::Tuple { elements } if elements.is_empty() + )); + } + #[test] fn resolve_line_numbers_duplicates_exit_blocks_like_cpython() { let exit = BlockIdx::new(2); - let mut blocks = vec![Block::default(), Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]); blocks[0].cpython_label = InstructionSequenceLabel::from_index(0); blocks[1].cpython_label = InstructionSequenceLabel::from_index(1); blocks[2].cpython_label = InstructionSequenceLabel::from_index(2); @@ -7520,23 +7635,24 @@ mod tests { test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30)); blocks[2].instructions[0].lineno_override = Some(NO_LOCATION_OVERRIDE); - remove_unreachable(&mut blocks).expect("remove_unreachable succeeds"); - resolve_line_numbers(&mut blocks, OneIndexed::MIN).expect("resolve_line_numbers succeeds"); + blocks + .remove_unreachable() + .expect("remove_unreachable succeeds"); + blocks + .resolve_line_numbers(OneIndexed::MIN) + .expect("resolve_line_numbers succeeds"); // CPython `duplicate_exits_without_lineno()` copies a shared exit block // reached by jumps so each copy can inherit its sole predecessor's line. let duplicate = blocks[0].instructions[0].target; assert_ne!(duplicate, exit); assert_eq!( - blocks[duplicate.idx()].cpython_label, + blocks[duplicate].cpython_label, InstructionSequenceLabel::from_index(3) ); - assert_eq!( - instruction_lineno(&blocks[duplicate.idx()].instructions[0]), - 10 - ); + assert_eq!(blocks[duplicate].instructions[0].instruction_lineno(), 10); assert_eq!(blocks[1].instructions[0].target, exit); - assert_eq!(instruction_lineno(&blocks[exit.idx()].instructions[0]), 20); + assert_eq!(blocks[exit].instructions[0].instruction_lineno(), 20); } #[test] @@ -7547,10 +7663,12 @@ mod tests { block.instructions[1].lineno_override = Some(NEXT_LOCATION_OVERRIDE); test_block_push(&mut block, test_instr(Instruction::ReturnValue, 30)); block.instructions[2].lineno_override = Some(NO_LOCATION_OVERRIDE); - let mut blocks = vec![block]; + let mut blocks = Blocks::from([block]); - remove_unreachable(&mut blocks).expect("remove_unreachable succeeds"); - propagate_line_numbers(&mut blocks); + blocks + .remove_unreachable() + .expect("remove_unreachable succeeds"); + blocks.propagate_line_numbers(); // CPython `propagate_line_numbers()` only copies over NO_LOCATION // (`lineno == NO_LOCATION`). `NEXT_LOCATION` (`lineno == -2`) becomes the @@ -7568,22 +7686,24 @@ mod tests { #[test] fn propagate_line_numbers_updates_empty_jump_target_raw_slot_like_cpython() { - let mut blocks = vec![Block::default(), Block::default(), Block::default()]; + let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]); blocks[0].next = BlockIdx::new(2); test_block_push(&mut blocks[0], test_cond_jump(BlockIdx::new(1), 10)); test_block_push(&mut blocks[1], test_instr(Instruction::Nop, 20)); blocks[1].instructions[0].lineno_override = Some(NO_LOCATION_OVERRIDE); - basicblock_clear(&mut blocks[1]); + blocks[1].basicblock_clear(); test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30)); - remove_unreachable(&mut blocks).expect("remove_unreachable succeeds"); - propagate_line_numbers(&mut blocks); + blocks + .remove_unreachable() + .expect("remove_unreachable succeeds"); + blocks.propagate_line_numbers(); // CPython `propagate_line_numbers()` directly reads `target->b_instr[0]` // for jump targets without checking `b_iused`. If // `remove_redundant_nops()` emptied the target, that writes the stale // backing slot rather than an active instruction. - assert_eq!(instruction_lineno(&blocks[1].instructions[0]), 10); + assert_eq!(blocks[1].instructions[0].instruction_lineno(), 10); } #[test] @@ -7594,20 +7714,20 @@ mod tests { // CPython `basicblock_has_no_lineno()` treats every negative lineno as // no line number, including `NEXT_LOCATION` (`lineno == -2`). - assert!(basicblock_has_no_lineno(&block)); + assert!(block.basicblock_has_no_lineno()); test_block_push(&mut block, test_instr(Instruction::PopTop, 11)); - assert!(!basicblock_has_no_lineno(&block)); + assert!(!block.basicblock_has_no_lineno()); } #[test] fn jump_threading_rechecks_new_jump_like_cpython() { - let mut blocks = vec![ + let mut blocks = Blocks::from([ Block::default(), Block::default(), Block::default(), Block::default(), - ]; + ]); for (i, block) in blocks.iter_mut().enumerate() { block.cpython_label = InstructionSequenceLabel::from_index(i as i32); } @@ -7620,12 +7740,13 @@ mod tests { test_block_push(&mut blocks[3], test_instr(Instruction::ReturnValue, 40)); let mut metadata = test_code_info(Block::default()).metadata; - optimize_basic_block(&mut blocks, &mut metadata, BlockIdx::new(0)) + blocks + .optimize_basic_block(&mut metadata, BlockIdx::new(0)) .expect("valid jump chain"); // CPython `optimize_basic_block()` continues after `jump_thread()`, so // the appended jump is immediately checked against the next jump target. - let threaded = basicblock_last_instr(&blocks[0]).expect("threaded jump"); + let threaded = blocks[0].basicblock_last_instr().expect("threaded jump"); assert!(matches!( threaded.instr.pseudo(), Some(PseudoInstruction::Jump { .. }) @@ -7633,4 +7754,51 @@ mod tests { assert_eq!(threaded.target, BlockIdx::new(3)); assert_eq!(u32::from(threaded.arg), 3); } + + #[test] + fn same_direction_pseudo_conditional_jump_thread_false_keeps_target() { + let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]); + for (i, block) in blocks.iter_mut().enumerate() { + block.cpython_label = InstructionSequenceLabel::from_index(i as i32); + } + blocks[0].next = BlockIdx::new(1); + blocks[1].next = BlockIdx::new(2); + test_block_push(&mut blocks[0], test_cond_jump(BlockIdx::new(1), 10)); + test_block_push(&mut blocks[1], test_cond_jump(BlockIdx::new(1), 20)); + test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30)); + + let mut metadata = test_code_info(Block::default()).metadata; + blocks + .optimize_basic_block(&mut metadata, BlockIdx::new(0)) + .expect("valid conditional jump chain"); + + // Only rewrite JUMP_IF_FALSE -> JUMP_IF_TRUE through + // target->b_next. For same-direction jumps, a failed jump_thread() + // leaves the original target unchanged. + assert_eq!(blocks[0].instructions[0].target, BlockIdx::new(1)); + assert!(matches!( + blocks[0].instructions[0].instr.pseudo(), + Some(PseudoInstruction::JumpIfFalse { .. }) + )); + } + + #[test] + fn opposite_direction_pseudo_conditional_uses_target_fallthrough() { + let mut blocks = Blocks::from([Block::default(), Block::default(), Block::default()]); + for (i, block) in blocks.iter_mut().enumerate() { + block.cpython_label = InstructionSequenceLabel::from_index(i as i32); + } + blocks[0].next = BlockIdx::new(1); + blocks[1].next = BlockIdx::new(2); + test_block_push(&mut blocks[0], test_cond_jump(BlockIdx::new(1), 10)); + test_block_push(&mut blocks[1], test_true_cond_jump(BlockIdx::new(2), 20)); + test_block_push(&mut blocks[2], test_instr(Instruction::ReturnValue, 30)); + + let mut metadata = test_code_info(Block::default()).metadata; + blocks + .optimize_basic_block(&mut metadata, BlockIdx::new(0)) + .expect("valid conditional jump chain"); + + assert_eq!(blocks[0].instructions[0].target, BlockIdx::new(2)); + } } diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index b598ab7e933..a7349a5762f 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -8,13 +8,15 @@ extern crate log; extern crate alloc; +use rustpython_compiler_core::bytecode::ConstantData; + type IndexMap = indexmap::IndexMap; type IndexSet = indexmap::IndexSet; pub mod compile; pub mod error; pub mod ir; -mod preprocess; +pub mod preprocess; mod string_parser; pub mod symboltable; mod unparse; @@ -24,6 +26,73 @@ use ruff_python_ast as ast; pub(crate) use compile::InternalResult; +#[cfg(test)] +pub(crate) fn constant_data_to_ast_constant_value(value: ConstantData) -> ast::ConstantValue { + match value { + ConstantData::None => ast::ConstantValue::None, + ConstantData::Boolean { value } => ast::ConstantValue::Boolean(value), + ConstantData::Str { value } => ast::ConstantValue::Str(value.to_string().into_boxed_str()), + ConstantData::Bytes { value } => ast::ConstantValue::Bytes(value.into_boxed_slice()), + ConstantData::Integer { value } => ast::ConstantValue::Integer(value.to_string().into()), + ConstantData::Tuple { elements } => ast::ConstantValue::Tuple( + elements + .into_iter() + .map(constant_data_to_ast_constant_value) + .collect(), + ), + ConstantData::Frozenset { elements } => ast::ConstantValue::Frozenset( + elements + .into_iter() + .map(constant_data_to_ast_constant_value) + .collect(), + ), + ConstantData::Float { value } => ast::ConstantValue::Float(value), + ConstantData::Complex { value } => ast::ConstantValue::Complex { + real: value.re, + imag: value.im, + }, + ConstantData::Ellipsis => ast::ConstantValue::Ellipsis, + ConstantData::Code { .. } | ConstantData::Slice { .. } => { + unreachable!("ast.Constant values cannot contain code objects or slices") + } + } +} + +pub(crate) fn ast_constant_value_to_constant_data(value: ast::ConstantValue) -> ConstantData { + match value { + ast::ConstantValue::None => ConstantData::None, + ast::ConstantValue::Boolean(value) => ConstantData::Boolean { value }, + ast::ConstantValue::Str(value) => ConstantData::Str { + value: value.to_string().into(), + }, + ast::ConstantValue::Bytes(value) => ConstantData::Bytes { + value: value.into_vec(), + }, + ast::ConstantValue::Integer(value) => ConstantData::Integer { + value: value + .parse() + .expect("RustPython ast.Constant integer values are decimal integers"), + }, + ast::ConstantValue::Tuple(elements) => ConstantData::Tuple { + elements: elements + .into_iter() + .map(ast_constant_value_to_constant_data) + .collect(), + }, + ast::ConstantValue::Frozenset(elements) => ConstantData::Frozenset { + elements: elements + .into_iter() + .map(ast_constant_value_to_constant_data) + .collect(), + }, + ast::ConstantValue::Float(value) => ConstantData::Float { value }, + ast::ConstantValue::Complex { real, imag } => ConstantData::Complex { + value: num_complex::Complex::new(real, imag), + }, + ast::ConstantValue::Ellipsis => ConstantData::Ellipsis, + } +} + pub trait ToPythonName { /// Returns a short name for the node suitable for use in error messages. fn python_name(&self) -> &'static str; @@ -48,6 +117,19 @@ impl ToPythonName for ast::Expr { } Self::EllipsisLiteral(_) => "ellipsis", Self::NoneLiteral(_) => "None", + Self::Constant(expr) => match &expr.value { + ast::ConstantValue::None => "None", + ast::ConstantValue::Boolean(true) => "True", + ast::ConstantValue::Boolean(false) => "False", + ast::ConstantValue::Ellipsis => "ellipsis", + ast::ConstantValue::Tuple(_) => "tuple", + ast::ConstantValue::Frozenset(_) => "literal", + ast::ConstantValue::Str(_) + | ast::ConstantValue::Bytes(_) + | ast::ConstantValue::Integer(_) + | ast::ConstantValue::Float(_) + | ast::ConstantValue::Complex { .. } => "literal", + }, Self::NumberLiteral(_) | Self::BytesLiteral(_) | Self::StringLiteral(_) => "literal", Self::Tuple(_) => "tuple", Self::List { .. } => "list", @@ -65,7 +147,7 @@ impl ToPythonName for ast::Expr { Self::Lambda { .. } => "lambda", Self::If { .. } => "conditional expression", Self::Named { .. } => "named expression", - Self::IpyEscapeCommand(_) => todo!(), + Self::IpyEscapeCommand(_) => "expression", } } } diff --git a/crates/codegen/src/preprocess.rs b/crates/codegen/src/preprocess.rs index ae2e65bf3fe..084b72c87f7 100644 --- a/crates/codegen/src/preprocess.rs +++ b/crates/codegen/src/preprocess.rs @@ -9,28 +9,414 @@ use ruff_python_ast::{ }; use ruff_text_size::{Ranged, TextRange}; +use crate::compile::FutureFeature; +use rustpython_compiler_core::bytecode; + const MAXDIGITS: usize = 3; const F_LJUST: u8 = 1; -pub(crate) fn preprocess_mod(module: &mut ast::Mod) { - let preprocessor = AstPreprocessor; +/// ast_preprocess.c ControlFlowInFinallyContext +#[derive(Clone, Copy)] +struct ControlFlowInFinallyContext { + in_finally: bool, + in_funcdef: bool, + in_loop: bool, +} + +/// ast_preprocess.c before_return +fn before_return( + 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( + 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( + body: &[ast::Stmt], + contexts: &mut Vec, + 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( + body: &[ast::Stmt], + contexts: &mut Vec, + 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( + stmt: &ast::Stmt, + contexts: &mut Vec, + 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( + module: &ast::Mod, + mut warn: impl FnMut(TextRange, String) -> Result<(), E>, +) -> Result<(), E> { + let mut contexts = Vec::new(); match module { - ast::Mod::Module(module) => preprocessor.visit_body(&mut module.body), + ast::Mod::Module(module) => { + visit_body_for_control_flow_in_finally(&module.body, &mut contexts, &mut warn)?; + } + ast::Mod::Expression(_) => {} + } + Ok(()) +} + +pub fn has_future_annotations(module: &ast::Mod) -> bool { + future_features(module).contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS) +} + +pub fn future_features(module: &ast::Mod) -> bytecode::CodeFlags { + checked_future_features(module).unwrap_or_else(|err| err.features) +} + +pub struct FutureFeatureError { + pub features: bytecode::CodeFlags, + pub range: TextRange, + pub kind: FutureFeatureErrorKind, +} + +pub enum FutureFeatureErrorKind { + InvalidFeature(String), + InvalidBraces, +} + +pub fn checked_future_features( + module: &ast::Mod, +) -> Result { + 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 { + 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 { + let future_feature = + alias + .name + .as_str() + .try_into() + .map_err(|name| FutureFeatureError { + features: future_features, + range: alias.range, + kind: FutureFeatureErrorKind::InvalidFeature(name), + })?; + + match future_feature { + FutureFeature::Braces => { + return Err(FutureFeatureError { + features: future_features, + range: alias.range, + kind: FutureFeatureErrorKind::InvalidBraces, + }); + } + FutureFeature::Annotations => { + future_features.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS) + } + FutureFeature::BarryAsFLUFL => { + // We do not support Barry-as-BDFL parser mode yet. This is a nop for now. + } + FutureFeature::AbsoluteImport + | FutureFeature::Division + | FutureFeature::GeneratorStop + | FutureFeature::Generators + | FutureFeature::NestedScopes + | FutureFeature::PrintFunction + | FutureFeature::UnicodeLiterals + | FutureFeature::WithStatement => { + // Python 3 features. They are already implemented by default. + } + } + } + } + _ => return Ok(future_features), + } + } + Ok(future_features) +} + +pub fn preprocess_statements( + body: &mut [ast::Stmt], + optimize: u8, + future_annotations: bool, + syntax_check_only: bool, +) { + let preprocessor = AstPreprocessor { + optimize, + future_annotations, + constant_folding: !syntax_check_only, + }; + for stmt in body { + preprocessor.visit_stmt(stmt); + } +} + +pub fn preprocess_mod( + module: &mut ast::Mod, + optimize: u8, + future_annotations: bool, + syntax_check_only: bool, +) { + let preprocessor = AstPreprocessor { + optimize, + future_annotations, + constant_folding: !syntax_check_only, + }; + match module { + ast::Mod::Module(module) => preprocessor.visit_astfold_body(&mut module.body), ast::Mod::Expression(expr) => preprocessor.visit_expr(&mut expr.body), } } -struct AstPreprocessor; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct AstPreprocessor { + optimize: u8, + future_annotations: bool, + constant_folding: bool, +} + +impl AstPreprocessor { + fn visit_astfold_body(self, body: &mut ast::Suite) { + let mut docstring = body_starts_with_docstring(body); + if docstring && self.optimize >= 2 { + remove_docstring_from_body(body); + docstring = false; + } + + for stmt in body.iter_mut() { + self.visit_stmt(stmt); + } + + if !docstring && body_starts_with_docstring(body) { + wrap_first_docstring_as_fstring(body); + } + } +} impl Transformer for AstPreprocessor { + fn visit_stmt(&self, stmt: &mut ast::Stmt) { + match stmt { + ast::Stmt::FunctionDef(function) => { + if let Some(type_params) = &mut function.type_params { + self.visit_type_params(type_params); + } + self.visit_parameters(&mut function.parameters); + self.visit_astfold_body(&mut function.body); + for decorator in &mut function.decorator_list { + self.visit_decorator(decorator); + } + if let Some(returns) = &mut function.returns { + self.visit_annotation(returns); + } + } + ast::Stmt::ClassDef(class) => { + if let Some(type_params) = &mut class.type_params { + self.visit_type_params(type_params); + } + if let Some(arguments) = &mut class.arguments { + self.visit_arguments(arguments); + } + self.visit_astfold_body(&mut class.body); + for decorator in &mut class.decorator_list { + self.visit_decorator(decorator); + } + } + _ => transformer::walk_stmt(self, stmt), + } + } + + fn visit_annotation(&self, expr: &mut Expr) { + if !self.future_annotations { + transformer::walk_annotation(self, expr); + } + } + + fn visit_pattern(&self, pattern: &mut ast::Pattern) { + transformer::walk_pattern(self, pattern); + if !self.constant_folding { + return; + } + match pattern { + ast::Pattern::MatchValue(value) => fold_match_value_constant_expr(&mut value.value), + ast::Pattern::MatchMapping(mapping) => { + for key in &mut mapping.keys { + fold_match_value_constant_expr(key); + } + } + _ => {} + } + } + fn visit_expr(&self, expr: &mut Expr) { transformer::walk_expr(self, expr); - if let Some(optimized) = optimize_format(expr) { - *expr = optimized; + 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 { + 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 { let Expr::BinOp(binop) = expr else { return None; @@ -38,9 +424,7 @@ fn optimize_format(expr: &Expr) -> Option { if !matches!(binop.op, Operator::Mod) { return None; } - let Expr::StringLiteral(format) = binop.left.as_ref() else { - return None; - }; + let (format, _) = string_literal_expr_value(&binop.left)?; let Expr::Tuple(tuple) = binop.right.as_ref() else { return None; }; @@ -52,7 +436,7 @@ fn optimize_format(expr: &Expr) -> Option { return None; } - let elements = parse_format(format.value.to_str(), &tuple.elts)?; + let elements = parse_format(format, &tuple.elts)?; Some(Expr::FString(ExprFString { node_index: binop.node_index.clone(), range: binop.range, @@ -62,6 +446,8 @@ fn optimize_format(expr: &Expr) -> Option { elements: InterpolatedStringElements::from(elements), flags: FStringFlags::empty(), }), + runtime_joined_str: None, + runtime_values: None, })) } @@ -163,6 +549,9 @@ fn parse_format_arg(chars: &[char], pos: &mut usize, arg: Expr) -> Option InterpolatedStringLiteralElement { value: value.into_boxed_str(), } } + +fn remove_docstring_from_body(body: &mut ast::Suite) { + if let Some(range) = take_docstring(body) { + if !body.is_empty() { + return; + } + let start = range.start(); + let pass_range = TextRange::new(start, start + ruff_text_size::TextSize::from(4)); + body.push(ast::Stmt::Pass(ast::StmtPass { + node_index: Default::default(), + range: pass_range, + })); + } +} + +fn take_docstring(body: &mut ast::Suite) -> Option { + 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 { + 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 { + 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 + ); + } +} diff --git a/crates/codegen/src/string_parser.rs b/crates/codegen/src/string_parser.rs index ee6c08a5639..622488a2177 100644 --- a/crates/codegen/src/string_parser.rs +++ b/crates/codegen/src/string_parser.rs @@ -13,6 +13,7 @@ use rustpython_wtf8::{CodePoint, Wtf8, Wtf8Buf}; // use ruff_python_parser::{LexicalError, LexicalErrorType}; type LexicalError = Infallible; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] enum EscapedChar { Literal(CodePoint), Escape(char), @@ -113,7 +114,7 @@ impl StringParser { let name_and_ending = self.skip_bytes(close_idx + 1); let name = &name_and_ending[..name_and_ending.len() - 1]; - unicode_names2::character(name).ok_or_else(|| unreachable!()) + rustpython_unicode::lookup_character(name).ok_or_else(|| unreachable!()) } /// Parse an escaped character, returning the new character. diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index cd9ee4f0a41..a771e19d36f 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -17,6 +17,9 @@ use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; use rustpython_compiler_core::{PositionEncoding, SourceFile, SourceLocation}; +const DEFAULT_RECURSION_LIMIT: usize = 1000; +const RECURSION_ERROR: &str = "maximum recursion depth exceeded during compilation"; + /// Captures all symbols in the current scope, and has a list of sub-scopes in this scope. #[derive(Clone)] pub struct SymbolTable { @@ -42,6 +45,20 @@ pub struct SymbolTable { /// AST nodes. pub sub_tables: Vec, + /// Annotation scopes registered in st_blocks but not added + /// to ste_children, e.g. future-annotation function signatures. + pub hidden_annotation_blocks: Vec, + + /// Cursor pointing to the next hidden annotation block to consume. + pub next_hidden_annotation_block: usize, + + /// Inlined comprehension scopes removed from ste_children but + /// can still find through st_blocks keyed by the comprehension expression. + pub inlined_comprehension_blocks: Vec, + + /// Cursor pointing to the next inlined comprehension block to consume. + pub next_inlined_comprehension_block: usize, + /// Cursor pointing to the next sub-table to consume during compilation. pub next_sub_table: usize, @@ -63,6 +80,19 @@ pub struct SymbolTable { /// Whether this scope contains await or async comprehension machinery. pub is_coroutine: bool, + /// Whether this scope contains a return statement with a value. + pub returns_value: bool, + + /// Whether this block visited at least one annotation expression. + pub annotations_used: bool, + + /// Optional description of the current type-variable evaluator context. + pub scope_info: Option<&'static str>, + + /// Whether this annotation block is currently visiting an unevaluated + /// function-local annotation. + pub in_unevaluated_annotation: bool, + /// Whether this comprehension scope should be inlined (PEP 709) /// True for list/set/dict comprehensions in non-generator expressions pub comp_inlined: bool, @@ -73,7 +103,7 @@ pub struct SymbolTable { /// True only for deferred function/class/module annotation scopes that /// should resolve outer names as if they were siblings of the owning - /// function body, matching CPython's PEP 649 lookup rules. + /// function body, matching PEP 649 lookup rules. pub skip_enclosing_function_scope: bool, /// PEP 649: Whether this scope has conditional annotations @@ -99,6 +129,10 @@ impl SymbolTable { is_method: false, symbols: IndexMap::default(), sub_tables: vec![], + hidden_annotation_blocks: vec![], + next_hidden_annotation_block: 0, + inlined_comprehension_blocks: vec![], + next_inlined_comprehension_block: 0, next_sub_table: 0, varnames: Vec::new(), needs_class_closure: false, @@ -106,6 +140,10 @@ impl SymbolTable { can_see_class_scope: false, is_generator: false, is_coroutine: false, + returns_value: false, + annotations_used: false, + scope_info: None, + in_unevaluated_annotation: false, comp_inlined: false, annotation_block: None, skip_enclosing_function_scope: false, @@ -115,11 +153,39 @@ impl SymbolTable { } } + fn add_format_parameter(&mut self) { + let name = ".format"; + let symbol = self + .symbols + .entry(name.to_owned()) + .or_insert_with(|| Symbol::new(name)); + symbol + .flags + .insert(SymbolFlags::DEF_PARAM | SymbolFlags::USE); + if !self.varnames.iter().any(|varname| varname == name) { + self.varnames.push(name.to_owned()); + } + } + pub fn scan_program( program: &ast::ModModule, source_file: SourceFile, + ) -> SymbolTableResult { + Self::scan_program_with_options(program, source_file, false, false, DEFAULT_RECURSION_LIMIT) + } + + pub fn scan_program_with_options( + program: &ast::ModModule, + source_file: SourceFile, + allow_top_level_await: bool, + future_annotations: bool, + recursion_limit: usize, ) -> SymbolTableResult { let mut builder = SymbolTableBuilder::new(source_file); + builder.allow_top_level_await = allow_top_level_await; + builder.recursion_limit = recursion_limit; + builder.future_annotations = future_annotations + || SymbolTableBuilder::future_annotations_from_module_body(program.body.as_ref()); builder.scan_statements(program.body.as_ref())?; builder.finish() } @@ -127,8 +193,21 @@ impl SymbolTable { pub fn scan_expr( expr: &ast::ModExpression, source_file: SourceFile, + ) -> SymbolTableResult { + Self::scan_expr_with_options(expr, source_file, false, false, DEFAULT_RECURSION_LIMIT) + } + + pub fn scan_expr_with_options( + expr: &ast::ModExpression, + source_file: SourceFile, + allow_top_level_await: bool, + future_annotations: bool, + recursion_limit: usize, ) -> SymbolTableResult { let mut builder = SymbolTableBuilder::new(source_file); + builder.allow_top_level_await = allow_top_level_await; + builder.recursion_limit = recursion_limit; + builder.future_annotations = future_annotations; builder.scan_expression(expr.body.as_ref(), ExpressionContext::Load)?; builder.finish() } @@ -150,6 +229,8 @@ pub enum CompilerScope { TypeParams, /// PEP 649: Annotation scope for deferred evaluation Annotation, + TypeAlias, + TypeVariable, } impl fmt::Display for CompilerScope { @@ -163,11 +244,8 @@ impl fmt::Display for CompilerScope { Self::Comprehension => write!(f, "comprehension"), Self::TypeParams => write!(f, "type parameter"), Self::Annotation => write!(f, "annotation"), - // TODO missing types from the C implementation - // if self._table.type == _symtable.TYPE_TYPE_VAR_BOUND: - // return "TypeVar bound" - // if self._table.type == _symtable.TYPE_TYPE_ALIAS: - // return "type alias" + Self::TypeAlias => write!(f, "type alias"), + Self::TypeVariable => write!(f, "TypeVar bound"), } } } @@ -184,21 +262,38 @@ pub enum SymbolScope { Cell, } +impl SymbolScope { + /// Returns the [`i32`] representation of this symbol scope. + /// + /// # See also + /// [CPython's definition](https://github.com/python/cpython/blob/v3.14.6/Include/internal/pycore_symtable.h#L180-L184) + #[must_use] + pub const fn as_i32(&self) -> i32 { + match self { + Self::Unknown => 0, + Self::Local => 1, + Self::GlobalExplicit => 2, + Self::GlobalImplicit => 3, + Self::Free => 4, + Self::Cell => 5, + } + } +} + +impl From for i32 { + fn from(scope: SymbolScope) -> Self { + scope.as_i32() + } +} + bitflags! { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct SymbolFlags: u16 { - const REFERENCED = 0x001; // USE - const ASSIGNED = 0x002; // DEF_LOCAL - const PARAMETER = 0x004; // DEF_PARAM - const ANNOTATED = 0x008; // DEF_ANNOT - const IMPORTED = 0x010; // DEF_IMPORT - const NONLOCAL = 0x020; // DEF_NONLOCAL - // indicates if the symbol gets a value assigned by a named expression in a comprehension - // this is required to correct the scope in the analysis. - const ASSIGNED_IN_COMPREHENSION = 0x040; - // indicates that the symbol is used a bound iterator variable. We distinguish this case - // from normal assignment to detect disallowed re-assignment to iterator variables. - const ITER = 0x080; + const DEF_GLOBAL = 1; + const DEF_LOCAL = 2; + const DEF_PARAM = 2 << 1; + const DEF_NONLOCAL = 2 << 2; + const USE = 2 << 3; /// indicates that the symbol is a free variable in a class method from the scope that the /// class is defined in, e.g.: /// ```python @@ -207,12 +302,18 @@ bitflags! { /// def method(self): /// return x // is_free_class /// ``` - const FREE_CLASS = 0x100; // DEF_FREE_CLASS - const GLOBAL = 0x200; // DEF_GLOBAL - const COMP_ITER = 0x400; // DEF_COMP_ITER - const COMP_CELL = 0x800; // DEF_COMP_CELL - const TYPE_PARAM = 0x1000; // DEF_TYPE_PARAM - const BOUND = Self::ASSIGNED.bits() | Self::PARAMETER.bits() | Self::IMPORTED.bits() | Self::ITER.bits() | Self::TYPE_PARAM.bits(); + const DEF_FREE_CLASS = 2 << 5; + const DEF_IMPORT = 2 << 6; + const DEF_ANNOT = 2 << 7; + const DEF_COMP_ITER = 2 << 8; + const DEF_TYPE_PARAM = 2 << 9; + const DEF_COMP_CELL = 2 << 10; + const DEF_BOUND = ( + Self::DEF_LOCAL.bits() + | Self::DEF_PARAM.bits() + | Self::DEF_IMPORT.bits() + | Self::DEF_TYPE_PARAM.bits() + ); } } @@ -223,6 +324,7 @@ pub struct Symbol { pub name: String, pub scope: SymbolScope, pub flags: SymbolFlags, + pub location: Option, } impl Symbol { @@ -232,6 +334,7 @@ impl Symbol { // table, scope: SymbolScope::Unknown, flags: SymbolFlags::empty(), + location: None, } } @@ -250,7 +353,7 @@ impl Symbol { #[must_use] pub const fn is_bound(&self) -> bool { - self.flags.intersects(SymbolFlags::BOUND) + self.flags.intersects(SymbolFlags::DEF_BOUND) } } @@ -263,9 +366,14 @@ pub struct SymbolTableError { impl SymbolTableError { #[must_use] pub fn into_codegen_error(self, source_path: String) -> CodegenError { + let error = if self.error == RECURSION_ERROR { + CodegenErrorType::RecursionError + } else { + CodegenErrorType::SyntaxError(self.error) + }; CodegenError { location: self.location, - error: CodegenErrorType::SyntaxError(self.error), + error, source_path, } } @@ -296,7 +404,7 @@ fn analyze_symbol_table(symbol_table: &mut SymbolTable) -> SymbolTableResult { } /* Drop __class__ and __classdict__ from free variables in class scope - and set the appropriate flags. Equivalent to CPython's drop_class_free(). + and set the appropriate flags. Equivalent to drop_class_free(). See: https://github.com/python/cpython/blob/main/Python/symtable.c#L884 This function removes __class__ and __classdict__ from the @@ -315,20 +423,6 @@ fn drop_class_free(symbol_table: &mut SymbolTable, newfree: &mut IndexSet, parent_type: CompilerScope, ) -> IndexSet { - let mut removed_class_implicit = IndexSet::default(); + let mut removed_class_implicits = IndexSet::default(); for (name, sub_symbol) in &comp.symbols { // Skip the .0 parameter - if sub_symbol.flags.contains(SymbolFlags::PARAMETER) { + if sub_symbol.flags.contains(SymbolFlags::DEF_PARAM) { continue; } // Track inlined cells if sub_symbol.scope == SymbolScope::Cell - || sub_symbol.flags.contains(SymbolFlags::COMP_CELL) + || sub_symbol.flags.contains(SymbolFlags::DEF_COMP_CELL) { inlined_cells.insert(name.clone()); } - // Handle __class__ in ClassBlock + // __class__, __classdict__ and __conditional_annotations__ are never + // allowed to be free through a class scope. let scope = if sub_symbol.scope == SymbolScope::Free && parent_type == CompilerScope::Class && matches!( name.as_str(), "__class__" | "__classdict__" | "__conditional_annotations__" ) { - comp_free.swap_remove(name); - removed_class_implicit.insert(name.clone()); + let is_free_in_child = comp.sub_tables.iter().any(|child| { + child + .symbols + .get(name) + .is_some_and(|s| s.scope == SymbolScope::Free) + }); + if !is_free_in_child { + comp_free.swap_remove(name); + } + removed_class_implicits.insert(name.clone()); SymbolScope::GlobalImplicit } else { sub_symbol.scope @@ -389,14 +492,14 @@ fn inline_comprehension( } } else { // Name doesn't exist in parent, copy the comprehension binding. - // This matches CPython's inline_comprehension(): newly introduced + // Matches inline_comprehension(): newly introduced // comprehension locals stay locals in the parent scope. let mut symbol = sub_symbol.clone(); symbol.scope = scope; parent_symbols.insert(name.clone(), symbol); } } - removed_class_implicit + removed_class_implicits } type SymbolMap = IndexMap; @@ -447,12 +550,6 @@ mod stack { pub(super) fn iter_mut(&mut self) -> impl DoubleEndedIterator + '_ { self.as_mut().iter_mut().map(|x| &mut **x) } - // pub fn top(&self) -> Option<&T> { - // self.as_ref().last().copied() - // } - // pub fn top_mut(&mut self) -> Option<&mut T> { - // self.as_mut().last_mut().map(|x| &mut **x) - // } pub(super) fn len(&self) -> usize { self.v.len() } @@ -524,32 +621,27 @@ impl SymbolTableAnalyzer { symbol_table.typ, symbol_table.skip_enclosing_function_scope, ); + let class_scope_entry = if is_class { + class_symbols_clone.as_ref() + } else { + class_entry + }; self.tables.with_append(&mut info, |list| { let inner_scope = unsafe { &mut *(list as *mut _ as *mut Self) }; for sub_table in sub_tables.iter_mut() { - let child_class_entry = if sub_table.can_see_class_scope { - if is_class { - class_symbols_clone.as_ref() - } else { - class_entry - } - } else { - None - }; + let child_class_entry = sub_table + .can_see_class_scope + .then_some(class_scope_entry) + .flatten(); let child_free = inner_scope.analyze_symbol_table(sub_table, child_class_entry)?; child_frees.push((child_free, sub_table.comp_inlined)); } // PEP 649: Analyze annotation block if present if let Some(annotation_table) = annotation_block { - let ann_class_entry = if annotation_table.can_see_class_scope { - if is_class { - class_symbols_clone.as_ref() - } else { - class_entry - } - } else { - None - }; + let ann_class_entry = annotation_table + .can_see_class_scope + .then_some(class_scope_entry) + .flatten(); let child_free = inner_scope.analyze_symbol_table(annotation_table, ann_class_entry)?; annotation_free = Some(child_free); @@ -590,11 +682,31 @@ impl SymbolTableAnalyzer { newfree.extend(ann_free); } + let mut inlined_blocks = Vec::new(); + let mut idx = 0; + while idx < symbol_table.sub_tables.len() { + if symbol_table.sub_tables[idx].comp_inlined { + let comp = symbol_table.sub_tables.remove(idx); + let nested_inlined_blocks = comp.inlined_comprehension_blocks.clone(); + let children = comp.sub_tables.clone(); + let inserted = children.len(); + inlined_blocks.push(comp); + inlined_blocks.extend(nested_inlined_blocks); + symbol_table.sub_tables.splice(idx..idx, children); + idx += inserted; + } else { + idx += 1; + } + } + symbol_table + .inlined_comprehension_blocks + .extend(inlined_blocks); + let sub_tables = &*symbol_table.sub_tables; for symbol in symbol_table.symbols.values_mut() { if inlined_cells.contains(&symbol.name) { - symbol.flags.insert(SymbolFlags::COMP_CELL); + symbol.flags.insert(SymbolFlags::DEF_COMP_CELL); } } @@ -609,7 +721,7 @@ impl SymbolTableAnalyzer { class_entry, )?; - // CPython analyze_cells(): once a function-like scope owns a + // analyze_cells(): once a function-like scope owns a // child-requested name as a cell, that name is no longer free in // the enclosing scope. if function_like_scope && symbol.scope == SymbolScope::Cell { @@ -617,12 +729,14 @@ impl SymbolTableAnalyzer { } // Collect free variables from this scope - if symbol.scope == SymbolScope::Free || symbol.flags.contains(SymbolFlags::FREE_CLASS) { + if symbol.scope == SymbolScope::Free + || symbol.flags.contains(SymbolFlags::DEF_FREE_CLASS) + { newfree.insert(symbol.name.clone()); } } - // PEP 709 / CPython symtable.c: + // PEP 709 / symtable.c: // - only promote LOCAL -> CELL in function-like scopes, where // analyze_cells() runs. Module and class scopes keep their normal // scope and rely on DEF_COMP_CELL for comprehension-only cells. @@ -640,25 +754,17 @@ impl SymbolTableAnalyzer { drop_class_free(symbol_table, &mut newfree); } - // CPython update_symbols(..., classflag): after class implicit frees + // update_symbols(..., classflag): after class implicit frees // are dropped, a class block, or an annotation/type-params block that // can see a class scope, records existing child-free names with // DEF_FREE_CLASS. This preserves the current scope's own lookup kind // (for example GLOBAL_IMPLICIT via __classdict__) while still making // the name available as a closure cell for nested children such as // generator expressions. - if symbol_table.typ == CompilerScope::Class { + if symbol_table.typ == CompilerScope::Class || symbol_table.can_see_class_scope { for name in &newfree { if let Some(symbol) = symbol_table.symbols.get_mut(name) { - symbol.flags.insert(SymbolFlags::FREE_CLASS); - } - } - } else if symbol_table.can_see_class_scope { - for name in &newfree { - if let Some(symbol) = symbol_table.symbols.get_mut(name) - && !symbol.is_local() - { - symbol.flags.insert(SymbolFlags::FREE_CLASS); + symbol.flags.insert(SymbolFlags::DEF_FREE_CLASS); } } } @@ -674,118 +780,98 @@ impl SymbolTableAnalyzer { sub_tables: &[SymbolTable], class_entry: Option<&SymbolMap>, ) -> SymbolTableResult { - if symbol - .flags - .contains(SymbolFlags::ASSIGNED_IN_COMPREHENSION) - && st_typ == CompilerScope::Comprehension - { - // propagate symbol to next higher level that can hold it, - // i.e., function or module. Comprehension is skipped and - // Class is not allowed and detected as error. - //symbol.scope = SymbolScope::Nonlocal; - self.analyze_symbol_comprehension(symbol, 0)? - } else { - match symbol.scope { - SymbolScope::Free => { - if !self.tables.as_ref().is_empty() { - let scope_depth = self.tables.as_ref().len(); - // check if the name is already defined in any outer scope - if scope_depth < 2 - || self.found_in_outer_scope( - &symbol.name, - st_typ, - skip_enclosing_function_scope, - ) != Some(SymbolScope::Free) - { - return Err(SymbolTableError { - error: format!("no binding for nonlocal '{}' found", symbol.name), - // TODO: accurate location info, somehow - location: None, - }); - } - // Check if the nonlocal binding refers to a type parameter - if symbol.flags.contains(SymbolFlags::NONLOCAL) { - for (symbols, _typ, _skip) in self.tables.iter().rev() { - if let Some(sym) = symbols.get(&symbol.name) { - if sym.flags.contains(SymbolFlags::TYPE_PARAM) { - return Err(SymbolTableError { - error: format!( - "nonlocal binding not allowed for type parameter '{}'", - symbol.name - ), - location: None, - }); - } - if sym.is_bound() { - break; - } + match symbol.scope { + SymbolScope::Free => { + if !self.tables.as_ref().is_empty() { + let scope_depth = self.tables.as_ref().len(); + // check if the name is already defined in any outer scope + if scope_depth < 2 + || self.found_in_outer_scope( + &symbol.name, + st_typ, + skip_enclosing_function_scope, + ) != Some(SymbolScope::Free) + { + return Err(SymbolTableError { + error: format!("no binding for nonlocal '{}' found", symbol.name), + location: symbol.location, + }); + } + // Check if the nonlocal binding refers to a type parameter + if symbol.flags.contains(SymbolFlags::DEF_NONLOCAL) { + for (symbols, _typ, _skip) in self.tables.iter().rev() { + if let Some(sym) = symbols.get(&symbol.name) { + if sym.flags.contains(SymbolFlags::DEF_TYPE_PARAM) { + return Err(SymbolTableError { + error: format!( + "nonlocal binding not allowed for type parameter '{}'", + symbol.name + ), + location: symbol.location, + }); + } + if sym.is_bound() { + break; } } } - } else { - return Err(SymbolTableError { - error: format!( - "nonlocal {} defined at place without an enclosing scope", - symbol.name - ), - // TODO: accurate location info, somehow - location: None, - }); } + } else { + return Err(SymbolTableError { + error: format!( + "nonlocal {} defined at place without an enclosing scope", + symbol.name + ), + location: symbol.location, + }); } - SymbolScope::GlobalExplicit | SymbolScope::GlobalImplicit => { - // TODO: add more checks for globals? - } - SymbolScope::Local | SymbolScope::Cell => { - // all is well - } - SymbolScope::Unknown => { - // Try hard to figure out what the scope of this symbol is. - let scope = if symbol.is_bound() { - if symbol.flags.contains(SymbolFlags::COMP_CELL) - && matches!(st_typ, CompilerScope::Module | CompilerScope::Class) - { - // CPython keeps comprehension-only cells in - // module/class scopes as normal local/name - // bindings and uses DEF_COMP_CELL to allocate the - // synthetic cell slot. The spliced comp child - // should not force the outer name itself to CELL. - SymbolScope::Local - } else { - self.found_in_inner_scope(sub_tables, &symbol.name, st_typ) - .unwrap_or(SymbolScope::Local) - } - } else if let Some(scope) = class_entry - .and_then(|class_symbols| class_symbols.get(&symbol.name)) - .and_then(|class_sym| { - if class_sym.flags.contains(SymbolFlags::GLOBAL) { - Some(SymbolScope::GlobalExplicit) - } else if class_sym.is_bound() && class_sym.scope != SymbolScope::Free { - // If name is bound in enclosing class, use GlobalImplicit - // so it can be accessed via __classdict__ - Some(SymbolScope::GlobalImplicit) - } else { - None - } - }) + } + SymbolScope::GlobalExplicit | SymbolScope::GlobalImplicit => {} + SymbolScope::Local | SymbolScope::Cell => {} + SymbolScope::Unknown => { + // Try hard to figure out what the scope of this symbol is. + let scope = if symbol.is_bound() { + if symbol.flags.contains(SymbolFlags::DEF_COMP_CELL) + && matches!(st_typ, CompilerScope::Module | CompilerScope::Class) { - scope - } else if let Some(scope) = self.found_in_outer_scope( - &symbol.name, - st_typ, - skip_enclosing_function_scope, - ) { - // If found in enclosing scope (function/TypeParams), use that - scope - } else if self.tables.is_empty() { - // Don't make assumptions when we don't know. - SymbolScope::Unknown + // CPython keeps comprehension-only cells in + // module/class scopes as normal local/name + // bindings and uses DEF_COMP_CELL to allocate the + // synthetic cell slot. The spliced comp child + // should not force the outer name itself to CELL. + SymbolScope::Local } else { - // If there are scopes above we assume global. - SymbolScope::GlobalImplicit - }; - symbol.scope = scope; - } + self.found_in_inner_scope(sub_tables, &symbol.name, st_typ) + .unwrap_or(SymbolScope::Local) + } + } else if let Some(scope) = class_entry + .and_then(|class_symbols| class_symbols.get(&symbol.name)) + .and_then(|class_sym| { + if class_sym.flags.contains(SymbolFlags::DEF_GLOBAL) { + Some(SymbolScope::GlobalExplicit) + } else if class_sym.is_bound() && class_sym.scope != SymbolScope::Free { + // If name is bound in enclosing class, use GlobalImplicit + // so it can be accessed via __classdict__ + Some(SymbolScope::GlobalImplicit) + } else { + None + } + }) + { + scope + } else if let Some(scope) = + self.found_in_outer_scope(&symbol.name, st_typ, skip_enclosing_function_scope) + { + // If found in enclosing scope (function/TypeParams), use that + scope + } else if self.tables.is_empty() { + // Don't make assumptions when we don't know. + SymbolScope::Unknown + } else { + // If there are scopes above we assume global. + SymbolScope::GlobalImplicit + }; + symbol.scope = scope; } } Ok(()) @@ -862,10 +948,10 @@ impl SymbolTableAnalyzer { for (table, typ, _skip) in self.tables.iter_mut().rev().take(decl_depth) { if let CompilerScope::Class = typ { if let Some(free_class) = table.get_mut(name) { - free_class.flags.insert(SymbolFlags::FREE_CLASS) + free_class.flags.insert(SymbolFlags::DEF_FREE_CLASS) } else { let mut symbol = Symbol::new(name); - symbol.flags.insert(SymbolFlags::FREE_CLASS); + symbol.flags.insert(SymbolFlags::DEF_FREE_CLASS); symbol.scope = SymbolScope::Free; table.insert(name.to_owned(), symbol); } @@ -904,7 +990,7 @@ impl SymbolTableAnalyzer { } let sym = st.symbols.get(name)?; if sym.scope == SymbolScope::Free - || (sym.flags.contains(SymbolFlags::FREE_CLASS) + || (sym.flags.contains(SymbolFlags::DEF_FREE_CLASS) && !matches!(st_typ, CompilerScope::Module)) { if st_typ == CompilerScope::Class && name != "__class__" { @@ -921,110 +1007,6 @@ impl SymbolTableAnalyzer { } }) } - - // Implements the symbol analysis and scope extension for names - // assigned by a named expression in a comprehension. See: - // https://github.com/python/cpython/blob/7b78e7f9fd77bb3280ee39fb74b86772a7d46a70/Python/symtable.c#L1435 - fn analyze_symbol_comprehension( - &mut self, - symbol: &mut Symbol, - parent_offset: usize, - ) -> SymbolTableResult { - // when this is called, we expect to be in the direct parent scope of the scope that contains 'symbol' - let last = self.tables.iter_mut().rev().nth(parent_offset).unwrap(); - let symbols = &mut last.0; - let table_type = last.1; - - // it is not allowed to use an iterator variable as assignee in a named expression - if symbol.flags.contains(SymbolFlags::ITER) { - return Err(SymbolTableError { - error: format!( - "assignment expression cannot rebind comprehension iteration variable {}", - symbol.name - ), - // TODO: accurate location info, somehow - location: None, - }); - } - - match table_type { - CompilerScope::Module => { - symbol.scope = SymbolScope::GlobalImplicit; - } - CompilerScope::Class => { - // named expressions are forbidden in comprehensions on class scope - return Err(SymbolTableError { - error: "assignment expression within a comprehension cannot be used in a class body".to_string(), - // TODO: accurate location info, somehow - location: None, - }); - } - CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda => { - if let Some(parent_symbol) = symbols.get_mut(&symbol.name) { - if let SymbolScope::Unknown = parent_symbol.scope { - // this information is new, as the assignment is done in inner scope - parent_symbol.flags.insert(SymbolFlags::ASSIGNED); - } - - symbol.scope = if parent_symbol.is_global() { - parent_symbol.scope - } else { - SymbolScope::Free - }; - } else { - let mut cloned_sym = symbol.clone(); - cloned_sym.scope = SymbolScope::Cell; - last.0.insert(cloned_sym.name.to_owned(), cloned_sym); - } - } - CompilerScope::Comprehension => { - // TODO check for conflicts - requires more context information about variables - match symbols.get_mut(&symbol.name) { - Some(parent_symbol) => { - // check if assignee is an iterator in top scope - if parent_symbol.flags.contains(SymbolFlags::ITER) { - return Err(SymbolTableError { - error: format!( - "assignment expression cannot rebind comprehension iteration variable {}", - symbol.name - ), - location: None, - }); - } - - // we synthesize the assignment to the symbol from inner scope - parent_symbol.flags.insert(SymbolFlags::ASSIGNED); // more checks are required - } - None => { - // extend the scope of the inner symbol - // as we are in a nested comprehension, we expect that the symbol is needed - // outside, too, and set it therefore to non-local scope. I.e., we expect to - // find a definition on a higher level - let mut cloned_sym = symbol.clone(); - cloned_sym.scope = SymbolScope::Free; - last.0.insert(cloned_sym.name.to_owned(), cloned_sym); - } - } - - self.analyze_symbol_comprehension(symbol, parent_offset + 1)?; - } - CompilerScope::TypeParams => { - // Named expression in comprehension cannot be used in type params - return Err(SymbolTableError { - error: "assignment expression within a comprehension cannot be used within the definition of a generic".to_string(), - location: None, - }); - } - CompilerScope::Annotation => { - // Named expression is not allowed in annotation scope - return Err(SymbolTableError { - error: "named expression cannot be used within an annotation".to_string(), - location: None, - }); - } - } - Ok(()) - } } #[derive(Clone, Copy, Debug)] @@ -1037,7 +1019,6 @@ enum SymbolUsage { AnnotationAssigned, Parameter, AnnotationParameter, - AssignedNamedExprInComprehension, Iter, TypeParam, } @@ -1047,6 +1028,7 @@ struct SymbolTableBuilder { // Scope stack. tables: Vec, future_annotations: bool, + allow_top_level_await: bool, source_file: SourceFile, // Current scope's varnames being collected (temporary storage) current_varnames: Vec, @@ -1054,19 +1036,14 @@ struct SymbolTableBuilder { varnames_stack: Vec>, // Track if we're inside an iterable definition expression (for nested comprehensions) in_iter_def_exp: bool, - // Track if we're inside an annotation (yield/await/named expr not allowed) - in_annotation: bool, - // CPython's ste_in_unevaluated_annotation: function-local AnnAssign - // annotations are not executed and do not contribute name bindings. - in_unevaluated_annotation: bool, - // Track if we're inside a type alias (yield/await/named expr not allowed) - in_type_alias: bool, - // Track if we're scanning an inner loop iteration target (not the first generator) - in_comp_inner_loop_target: bool, - // Scope info for error messages (e.g., "a TypeVar bound") - scope_info: Option<&'static str>, + // yield/yield from inside comprehension scopes is rejected with a + // message that names the comprehension kind. + comprehension_yield_context: Option<&'static str>, // PEP 649: Track if we're inside a conditional block (if/for/while/etc.) in_conditional_block: bool, + // Mirrors symtable ENTER_RECURSIVE guards during compilation. + recursion_depth: usize, + recursion_limit: usize, } /// Enum to indicate in what mode an expression @@ -1088,16 +1065,15 @@ impl SymbolTableBuilder { class_name: None, tables: vec![], future_annotations: false, + allow_top_level_await: false, source_file, current_varnames: Vec::new(), varnames_stack: Vec::new(), in_iter_def_exp: false, - in_annotation: false, - in_unevaluated_annotation: false, - in_type_alias: false, - in_comp_inner_loop_target: false, - scope_info: None, + comprehension_yield_context: None, in_conditional_block: false, + recursion_depth: 0, + recursion_limit: DEFAULT_RECURSION_LIMIT, }; this.enter_scope("top", CompilerScope::Module, 0); this @@ -1111,10 +1087,42 @@ impl SymbolTableBuilder { | CompilerScope::Lambda | CompilerScope::Comprehension | CompilerScope::Annotation + | CompilerScope::TypeAlias + | CompilerScope::TypeVariable | CompilerScope::TypeParams ) } + fn future_annotations_from_module_body(body: &[ast::Stmt]) -> bool { + let mut statements = body.iter(); + if let Some(ast::Stmt::Expr(ast::StmtExpr { value, .. })) = statements.clone().next() + && is_docstring_expr(value) + { + 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__") => + { + if names + .iter() + .any(|future| future.name.as_str() == "annotations") + { + return true; + } + } + _ => return false, + } + } + false + } + fn finish(mut self) -> Result { assert_eq!(self.tables.len(), 1); let mut symbol_table = self.tables.pop().unwrap(); @@ -1159,7 +1167,7 @@ impl SymbolTableBuilder { fn enter_type_param_block( &mut self, name: &str, - line_number: u32, + range: TextRange, for_class: bool, has_defaults: bool, has_kwdefaults: bool, @@ -1170,7 +1178,11 @@ impl SymbolTableBuilder { .last() .is_some_and(|t| t.typ == CompilerScope::Class); - self.enter_scope(name, CompilerScope::TypeParams, line_number); + self.enter_scope( + name, + CompilerScope::TypeParams, + self.line_index_start(range), + ); // Set properties on the newly created type param scope if let Some(table) = self.tables.last_mut() { @@ -1184,19 +1196,22 @@ impl SymbolTableBuilder { // Add __classdict__ as a USE symbol in type param scope if in class if in_class { - self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?; + self.register_name("__classdict__", SymbolUsage::Used, range)?; } - // Register .type_params as a SET symbol (it will be converted to cell variable later) - self.register_name(".type_params", SymbolUsage::Assigned, TextRange::default())?; if for_class { - self.register_name(".generic_base", SymbolUsage::Assigned, TextRange::default())?; + // It gets set when we create the type params tuple and used when + // we build up the bases. + self.register_name(".type_params", SymbolUsage::Assigned, range)?; + self.register_name(".type_params", SymbolUsage::Used, range)?; + self.register_name(".generic_base", SymbolUsage::Assigned, range)?; + self.register_name(".generic_base", SymbolUsage::Used, range)?; } if has_defaults { - self.register_name(".defaults", SymbolUsage::Parameter, TextRange::default())?; + self.register_name(".defaults", SymbolUsage::Parameter, range)?; } if has_kwdefaults { - self.register_name(".kwdefaults", SymbolUsage::Parameter, TextRange::default())?; + self.register_name(".kwdefaults", SymbolUsage::Parameter, range)?; } Ok(()) @@ -1212,9 +1227,22 @@ impl SymbolTableBuilder { self.current_varnames = self.varnames_stack.pop().unwrap_or_default(); } + /// Pop symbol table without adding it to the parent children list. + fn discard_scope(&mut self) -> SymbolTable { + let mut table = self.tables.pop().unwrap(); + table.varnames = core::mem::take(&mut self.current_varnames); + self.current_varnames = self.varnames_stack.pop().unwrap_or_default(); + table + } + /// Enter annotation scope (PEP 649) /// Creates or reuses the annotation block for the current scope - fn enter_annotation_scope(&mut self, line_number: u32) { + fn enter_annotation_scope( + &mut self, + line_number: u32, + include_classdict_with_future: bool, + include_conditional_annotations: bool, + ) { let current = self.tables.last_mut().unwrap(); let can_see_class_scope = current.typ == CompilerScope::Class || current.can_see_class_scope; @@ -1232,8 +1260,7 @@ impl SymbolTableBuilder { // Annotation scope in class can see class scope annotation_table.can_see_class_scope = can_see_class_scope; annotation_table.skip_enclosing_function_scope = true; - // Add 'format' parameter - annotation_table.varnames.push("format".to_owned()); + annotation_table.add_format_parameter(); current.annotation_block = Some(Box::new(annotation_table)); } @@ -1245,10 +1272,10 @@ impl SymbolTableBuilder { .push(core::mem::take(&mut self.current_varnames)); self.current_varnames = self.tables.last().unwrap().varnames.clone(); - if can_see_class_scope && !self.future_annotations { + if can_see_class_scope && (include_classdict_with_future || !self.future_annotations) { self.add_classdict_freevar(); // Also add __conditional_annotations__ as free var if parent has conditional annotations - if has_conditional { + if include_conditional_annotations && has_conditional { self.add_conditional_annotations_freevar(); } } @@ -1277,7 +1304,7 @@ impl SymbolTableBuilder { symbol.scope = SymbolScope::Free; symbol .flags - .insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS); + .insert(SymbolFlags::USE | SymbolFlags::DEF_FREE_CLASS); } fn add_conditional_annotations_freevar(&mut self) { @@ -1290,18 +1317,13 @@ impl SymbolTableBuilder { symbol.scope = SymbolScope::Free; symbol .flags - .insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS); + .insert(SymbolFlags::USE | SymbolFlags::DEF_FREE_CLASS); } /// Walk up the scope chain to determine if we're inside an async function. /// Annotation and TypeParams scopes act as async barriers (always non-async). /// Comprehension scopes are transparent (inherit parent's async context). fn is_in_async_context(&self) -> bool { - // Annotations are evaluated in a non-async scope even when - // the enclosing function is async. - if self.in_annotation { - return false; - } for table in self.tables.iter().rev() { match table.typ { CompilerScope::AsyncFunction => return true, @@ -1310,6 +1332,8 @@ impl SymbolTableBuilder { | CompilerScope::Class | CompilerScope::Module | CompilerScope::Annotation + | CompilerScope::TypeAlias + | CompilerScope::TypeVariable | CompilerScope::TypeParams => return false, // Comprehension inherits parent's async context CompilerScope::Comprehension => continue, @@ -1318,6 +1342,14 @@ impl SymbolTableBuilder { false } + fn allows_top_level_await(&self) -> bool { + self.allow_top_level_await + && self + .tables + .last() + .is_some_and(|table| table.typ == CompilerScope::Module) + } + fn line_index_start(&self, range: TextRange) -> u32 { self.source_file .to_source_code() @@ -1340,12 +1372,6 @@ impl SymbolTableBuilder { } fn scan_parameter(&mut self, parameter: &ast::Parameter) -> SymbolTableResult { - self.check_name( - parameter.name.as_str(), - ExpressionContext::Store, - parameter.name.range, - )?; - let usage = if parameter.annotation.is_some() { SymbolUsage::AnnotationParameter } else { @@ -1371,15 +1397,85 @@ impl SymbolTableBuilder { self.register_ident(¶meter.name, usage) } - fn scan_annotation(&mut self, annotation: &ast::Expr) -> SymbolTableResult { - self.scan_annotation_inner(annotation, false) - } - /// Scan an annotation from an AnnAssign statement (can be conditional) fn scan_ann_assign_annotation(&mut self, annotation: &ast::Expr) -> SymbolTableResult { self.scan_annotation_inner(annotation, true) } + fn scan_function_annotations( + &mut self, + parameters: &ast::Parameters, + returns: Option<&ast::Expr>, + line_number: u32, + ) -> SymbolTableResult { + let current = self.tables.last().unwrap(); + let can_see_class_scope = + current.typ == CompilerScope::Class || current.can_see_class_scope; + self.enter_scope("__annotate__", CompilerScope::Annotation, line_number); + self.tables.last_mut().unwrap().can_see_class_scope = can_see_class_scope; + self.tables.last_mut().unwrap().add_format_parameter(); + if can_see_class_scope { + self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?; + } + + let was_in_unevaluated_annotation = self.tables.last().unwrap().in_unevaluated_annotation; + self.tables.last_mut().unwrap().in_unevaluated_annotation = false; + + let result = (|| { + for annotation in parameters + .posonlyargs + .iter() + .chain(parameters.args.iter()) + .filter_map(|arg| arg.parameter.annotation.as_ref()) + { + self.tables.last_mut().unwrap().annotations_used = true; + self.scan_expression(annotation, ExpressionContext::Load)?; + } + if let Some(annotation) = parameters + .vararg + .as_ref() + .and_then(|arg| arg.annotation.as_ref()) + { + self.tables.last_mut().unwrap().annotations_used = true; + self.scan_expression(annotation, ExpressionContext::Load)?; + } + if let Some(annotation) = parameters + .kwarg + .as_ref() + .and_then(|arg| arg.annotation.as_ref()) + { + self.tables.last_mut().unwrap().annotations_used = true; + self.scan_expression(annotation, ExpressionContext::Load)?; + } + for annotation in parameters + .kwonlyargs + .iter() + .filter_map(|arg| arg.parameter.annotation.as_ref()) + { + self.tables.last_mut().unwrap().annotations_used = true; + self.scan_expression(annotation, ExpressionContext::Load)?; + } + if let Some(annotation) = returns { + self.tables.last_mut().unwrap().annotations_used = true; + self.scan_expression(annotation, ExpressionContext::Load)?; + } + Ok(()) + })(); + + self.tables.last_mut().unwrap().in_unevaluated_annotation = was_in_unevaluated_annotation; + if self.future_annotations { + let annotation_block = self.discard_scope(); + self.tables + .last_mut() + .unwrap() + .hidden_annotation_blocks + .push(annotation_block); + } else { + self.leave_scope(); + } + result + } + fn scan_annotation_inner( &mut self, annotation: &ast::Expr, @@ -1407,11 +1503,6 @@ impl SymbolTableBuilder { } if should_register_conditional_annotations { - self.register_name( - "__conditional_annotations__", - SymbolUsage::Assigned, - annotation.range(), - )?; self.register_name( "__conditional_annotations__", SymbolUsage::Used, @@ -1421,25 +1512,14 @@ impl SymbolTableBuilder { // Create annotation scope for deferred evaluation let line_number = self.line_index_start(annotation.range()); - self.enter_annotation_scope(line_number); - - if self.future_annotations { - // PEP 563: annotations are stringified at compile time - // Don't scan expression - symbols would fail to resolve - // Just create the annotation_block structure - self.leave_annotation_scope(); - return Ok(()); - } + self.enter_annotation_scope(line_number, false, true); // PEP 649: scan expression for symbol references // Class annotations are evaluated in class locals (not module globals) - let was_in_annotation = self.in_annotation; - let was_in_unevaluated_annotation = self.in_unevaluated_annotation; - self.in_annotation = true; - self.in_unevaluated_annotation = is_unevaluated; + let was_in_unevaluated_annotation = self.tables.last().unwrap().in_unevaluated_annotation; + self.tables.last_mut().unwrap().in_unevaluated_annotation = is_unevaluated; let result = self.scan_expression(annotation, ExpressionContext::Load); - self.in_annotation = was_in_annotation; - self.in_unevaluated_annotation = was_in_unevaluated_annotation; + self.tables.last_mut().unwrap().in_unevaluated_annotation = was_in_unevaluated_annotation; self.leave_annotation_scope(); @@ -1447,468 +1527,518 @@ impl SymbolTableBuilder { } fn scan_statement(&mut self, statement: &ast::Stmt) -> SymbolTableResult { - use ast::*; - if let Stmt::ImportFrom(StmtImportFrom { module, names, .. }) = &statement - && module.as_ref().map(|id| id.as_str()) == Some("__future__") - { - self.future_annotations = - self.future_annotations || names.iter().any(|future| &future.name == "annotations"); + if self.recursion_depth >= self.recursion_limit { + return Err(SymbolTableError { + error: RECURSION_ERROR.to_owned(), + location: None, + }); } - - match &statement { - Stmt::Global(StmtGlobal { names, .. }) => { - for name in names { - self.register_ident(name, SymbolUsage::Global)?; + self.recursion_depth += 1; + let result = (|| { + use ast::*; + match &statement { + Stmt::Global(StmtGlobal { names, .. }) => { + for name in names { + self.register_name(name.as_str(), SymbolUsage::Global, statement.range())?; + } } - } - Stmt::Nonlocal(StmtNonlocal { names, .. }) => { - for name in names { - self.register_ident(name, SymbolUsage::Nonlocal)?; + Stmt::Nonlocal(StmtNonlocal { names, .. }) => { + for name in names { + self.register_name( + name.as_str(), + SymbolUsage::Nonlocal, + statement.range(), + )?; + } } - } - Stmt::FunctionDef(StmtFunctionDef { - name, - body, - parameters, - decorator_list, - type_params, - returns, - range, - is_async, - .. - }) => { - self.scan_decorators(decorator_list, ExpressionContext::Load)?; - self.register_ident(name, SymbolUsage::Assigned)?; - - // Save the parent's annotation_block before scanning function annotations, - // so function annotations don't interfere with parent scope annotations. - // This applies to both class scope (methods) and module scope (top-level functions). - let parent_scope_typ = self.tables.last().map(|t| t.typ); - let should_save_annotation_block = matches!( - parent_scope_typ, - Some( - CompilerScope::Class - | CompilerScope::Module - | CompilerScope::Function - | CompilerScope::AsyncFunction - ) - ); - let saved_annotation_block = if should_save_annotation_block { - self.tables.last_mut().unwrap().annotation_block.take() - } else { - None - }; + Stmt::FunctionDef(StmtFunctionDef { + name, + body, + parameters, + decorator_list, + type_params, + returns, + range, + is_async, + .. + }) => { + self.register_name(name.as_str(), SymbolUsage::Assigned, *range)?; - // For generic functions, scan defaults before entering type_param_block - // (defaults are evaluated in the enclosing scope, not the type param scope) - let has_type_params = type_params.is_some(); - if has_type_params { self.scan_parameter_defaults(parameters)?; - } + self.scan_decorators(decorator_list, ExpressionContext::Load)?; - // For generic functions, enter type_param block FIRST so that - // annotation scopes are nested inside and can see type parameters. - if let Some(type_params) = type_params { - self.enter_type_param_block( - &format!("", name.as_str()), - self.line_index_start(type_params.range), - false, - true, - Self::has_kwonlydefaults(parameters), + // For generic functions, enter type_param block FIRST so that + // annotation scopes are nested inside and can see type parameters. + if let Some(type_params) = type_params { + self.enter_type_param_block( + name.as_str(), + *range, + false, + Self::has_positional_defaults(parameters), + Self::has_kwonlydefaults(parameters), + )?; + self.scan_type_params(type_params)?; + } + self.enter_scope_with_parameters( + name.as_str(), + parameters, + self.line_index_start(*range), + returns.as_deref(), + if *is_async { + CompilerScope::AsyncFunction + } else { + CompilerScope::Function + }, + true, // skip_defaults: already scanned above + false, )?; - self.scan_type_params(type_params)?; - } - let has_return_annotation = if let Some(expression) = returns { - self.scan_annotation(expression)?; - true - } else { - false - }; - self.enter_scope_with_parameters( - name.as_str(), - parameters, - self.line_index_start(*range), - has_return_annotation, if *is_async { - CompilerScope::AsyncFunction - } else { - CompilerScope::Function - }, - has_type_params, // skip_defaults: already scanned above - )?; - if *is_async { - self.tables.last_mut().unwrap().is_coroutine = true; - } - self.scan_statements(body)?; - self.leave_scope(); - if type_params.is_some() { + self.tables.last_mut().unwrap().is_coroutine = true; + } + self.scan_statements(body)?; self.leave_scope(); + if type_params.is_some() { + self.leave_scope(); + } } + Stmt::ClassDef(StmtClassDef { + name, + body, + arguments, + decorator_list, + type_params, + range, + .. + }) => { + let prev_class = self.class_name.clone(); + self.register_name(name.as_str(), SymbolUsage::Assigned, *range)?; + self.scan_decorators(decorator_list, ExpressionContext::Load)?; - // Restore parent's annotation_block after processing the function - if let Some(block) = saved_annotation_block { - self.tables.last_mut().unwrap().annotation_block = Some(block); - } - } - Stmt::ClassDef(StmtClassDef { - name, - body, - arguments, - decorator_list, - type_params, - range, - node_index: _, - }) => { - // Save class_name for the entire ClassDef processing - let prev_class = self.class_name.take(); - if let Some(type_params) = type_params { - self.enter_type_param_block( - &format!("", name.as_str()), - self.line_index_start(type_params.range), - true, // for_class: enable selective mangling - false, - false, - )?; - // Set class_name for mangling in type param scope + if let Some(type_params) = type_params { + self.enter_type_param_block( + name.as_str(), + *range, + true, // for_class: enable selective mangling + false, + false, + )?; + // Set class_name for mangling in type param scope + self.class_name = Some(name.to_string()); + self.scan_type_params(type_params)?; + } + + if type_params.is_none() { + self.class_name.clone_from(&prev_class); + } + + if let Some(arguments) = arguments { + self.scan_expressions(&arguments.args, ExpressionContext::Load)?; + for keyword in &arguments.keywords { + if let Some(arg) = &keyword.arg { + self.check_name( + arg.as_str(), + ExpressionContext::Store, + keyword.range, + )?; + } + } + for keyword in &arguments.keywords { + self.scan_expression(&keyword.value, ExpressionContext::Load)?; + } + } + + self.enter_scope( + name.as_str(), + CompilerScope::Class, + self.line_index_start(*range), + ); + // Reset in_conditional_block for new class scope + let saved_in_conditional = self.in_conditional_block; + self.in_conditional_block = false; self.class_name = Some(name.to_string()); - self.scan_type_params(type_params)?; - } - self.enter_scope( - name.as_str(), - CompilerScope::Class, - self.line_index_start(*range), - ); - // Reset in_conditional_block for new class scope - let saved_in_conditional = self.in_conditional_block; - self.in_conditional_block = false; - self.class_name = Some(name.to_string()); - self.register_name("__module__", SymbolUsage::Assigned, *range)?; - self.register_name("__qualname__", SymbolUsage::Assigned, *range)?; - self.register_name("__doc__", SymbolUsage::Assigned, *range)?; - self.register_name("__class__", SymbolUsage::Assigned, *range)?; - if type_params.is_some() { - self.register_name(".type_params", SymbolUsage::Used, *range)?; - self.register_name("__type_params__", SymbolUsage::Assigned, *range)?; - } - self.scan_statements(body)?; - self.leave_scope(); - self.in_conditional_block = saved_in_conditional; - // For non-generic classes, restore class_name before base scanning. - // Bases are evaluated in the enclosing scope, not the class scope. - // For generic classes, bases are scanned within the type_param scope - // where class_name is already correctly set. - if type_params.is_none() { - self.class_name = prev_class.clone(); - } - if let Some(arguments) = arguments { - self.scan_expressions(&arguments.args, ExpressionContext::Load)?; - for keyword in &arguments.keywords { - self.scan_expression(&keyword.value, ExpressionContext::Load)?; + if type_params.is_some() { + self.register_name(".type_params", SymbolUsage::Used, *range)?; + self.register_name("__type_params__", SymbolUsage::Assigned, *range)?; } - } - if type_params.is_some() { + self.scan_statements(body)?; self.leave_scope(); + self.in_conditional_block = saved_in_conditional; + if type_params.is_some() { + self.leave_scope(); + } + // Restore class_name after all ClassDef processing + self.class_name = prev_class; } - // Restore class_name after all ClassDef processing - self.class_name = prev_class; - self.scan_decorators(decorator_list, ExpressionContext::Load)?; - self.register_ident(name, SymbolUsage::Assigned)?; - } - Stmt::Expr(StmtExpr { value, .. }) => { - self.scan_expression(value, ExpressionContext::Load)? - } - Stmt::If(StmtIf { - test, - body, - elif_else_clauses, - .. - }) => { - self.scan_expression(test, ExpressionContext::Load)?; - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - self.scan_statements(body)?; - for elif in elif_else_clauses { - if let Some(test) = &elif.test { - self.scan_expression(test, ExpressionContext::Load)?; - } - self.scan_statements(&elif.body)?; - } - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::For(StmtFor { - target, - iter, - body, - orelse, - .. - }) => { - self.scan_expression(target, ExpressionContext::Store)?; - self.scan_expression(iter, ExpressionContext::Load)?; - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - self.scan_statements(body)?; - self.scan_statements(orelse)?; - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::While(StmtWhile { - test, body, orelse, .. - }) => { - self.scan_expression(test, ExpressionContext::Load)?; - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - self.scan_statements(body)?; - self.scan_statements(orelse)?; - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::Break(_) | Stmt::Continue(_) | Stmt::Pass(_) => { - // No symbols here. - } - Stmt::Import(StmtImport { names, .. }) - | Stmt::ImportFrom(StmtImportFrom { names, .. }) => { - for name in names { - if let Some(alias) = &name.asname { - // `import my_module as my_alias` - self.check_name(alias.as_str(), ExpressionContext::Store, alias.range)?; - self.register_ident(alias, SymbolUsage::Imported)?; - } else if name.name.as_str() == "*" { - // Star imports are only allowed at module level - if self.tables.last().unwrap().typ != CompilerScope::Module { - return Err(SymbolTableError { - error: "'import *' only allowed at module level".to_string(), - location: Some(self.source_file.to_source_code().source_location( - name.name.range.start(), - PositionEncoding::Utf8, - )), - }); + Stmt::Expr(StmtExpr { value, .. }) => { + self.scan_expression(value, ExpressionContext::Load)? + } + Stmt::If(StmtIf { + test, + body, + elif_else_clauses, + .. + }) => { + self.scan_expression(test, ExpressionContext::Load)?; + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + self.scan_statements(body)?; + for elif in elif_else_clauses { + if let Some(test) = &elif.test { + self.scan_expression(test, ExpressionContext::Load)?; } - // Don't register star imports as symbols - } else { - // `import module` or `from x import name` - let imported_name = name.name.split('.').next().unwrap(); - self.check_name(imported_name, ExpressionContext::Store, name.name.range)?; - self.register_name(imported_name, SymbolUsage::Imported, name.name.range)?; + self.scan_statements(&elif.body)?; } + self.in_conditional_block = saved_in_conditional_block; + } + Stmt::For(StmtFor { + target, + iter, + body, + orelse, + is_async, + .. + }) => { + if *is_async && self.allows_top_level_await() { + self.tables.last_mut().unwrap().is_coroutine = true; + } + if *is_async && !self.tables.last().unwrap().is_coroutine { + return Err(SymbolTableError { + error: "'async for' outside async function".to_owned(), + location: Some(self.source_file.to_source_code().source_location( + statement.range().start(), + PositionEncoding::Utf8, + )), + }); + } + self.scan_expression(target, ExpressionContext::Store)?; + self.scan_expression(iter, ExpressionContext::Load)?; + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + self.scan_statements(body)?; + self.scan_statements(orelse)?; + self.in_conditional_block = saved_in_conditional_block; } - } - Stmt::Return(StmtReturn { value, .. }) => { - if let Some(expression) = value { - self.scan_expression(expression, ExpressionContext::Load)?; + Stmt::While(StmtWhile { + test, body, orelse, .. + }) => { + self.scan_expression(test, ExpressionContext::Load)?; + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + self.scan_statements(body)?; + self.scan_statements(orelse)?; + self.in_conditional_block = saved_in_conditional_block; + } + Stmt::Break(_) | Stmt::Continue(_) | Stmt::Pass(_) => { + // No symbols here. + } + Stmt::Import(StmtImport { names, .. }) + | Stmt::ImportFrom(StmtImportFrom { names, .. }) => { + for name in names { + if let Some(alias) = &name.asname { + // `import my_module as my_alias` + self.register_name( + alias.as_str(), + SymbolUsage::Imported, + name.name.range, + )?; + } else if name.name.as_str() == "*" { + // Star imports are only allowed at module level + if self.tables.last().unwrap().typ != CompilerScope::Module { + return Err(SymbolTableError { + error: "import * only allowed at module level".to_string(), + location: Some( + self.source_file.to_source_code().source_location( + name.name.range.start(), + PositionEncoding::Utf8, + ), + ), + }); + } + // Don't register star imports as symbols + } else { + // `import module` or `from x import name` + let imported_name = name.name.split('.').next().unwrap(); + self.check_name( + imported_name, + ExpressionContext::Store, + name.name.range, + )?; + self.register_name( + imported_name, + SymbolUsage::Imported, + name.name.range, + )?; + } + } } - } - Stmt::Assert(StmtAssert { test, msg, .. }) => { - self.scan_expression(test, ExpressionContext::Load)?; - if let Some(expression) = msg { - self.scan_expression(expression, ExpressionContext::Load)?; + Stmt::Return(StmtReturn { value, .. }) => { + if let Some(expression) = value { + self.scan_expression(expression, ExpressionContext::Load)?; + self.tables.last_mut().unwrap().returns_value = true; + } } - } - Stmt::Delete(StmtDelete { targets, .. }) => { - self.scan_expressions(targets, ExpressionContext::Delete)?; - } - Stmt::Assign(StmtAssign { targets, value, .. }) => { - self.scan_expressions(targets, ExpressionContext::Store)?; - self.scan_expression(value, ExpressionContext::Load)?; - } - Stmt::AugAssign(StmtAugAssign { target, value, .. }) => { - self.scan_expression(target, ExpressionContext::Store)?; - self.scan_expression(value, ExpressionContext::Load)?; - } - Stmt::AnnAssign(StmtAnnAssign { - target, - annotation, - value, - simple, - range, - node_index: _, - }) => { - // https://github.com/python/cpython/blob/main/Python/symtable.c#L1233 - match &**target { - Expr::Name(ast::ExprName { id, .. }) => { - let id_str = id.as_str(); - - if *simple { - self.check_name(id_str, ExpressionContext::Store, *range)?; - - self.register_name(id_str, SymbolUsage::AnnotationAssigned, *range)?; - // PEP 649: Register annotate function in module/class scope - let current_scope = self.tables.last().map(|t| t.typ); - match current_scope { - Some(CompilerScope::Module) => { - self.register_name( - "__annotate__", - SymbolUsage::Assigned, - *range, - )?; + Stmt::Assert(StmtAssert { test, msg, .. }) => { + self.scan_expression(test, ExpressionContext::Load)?; + if let Some(expression) = msg { + self.scan_expression(expression, ExpressionContext::Load)?; + } + } + Stmt::Delete(StmtDelete { targets, .. }) => { + self.scan_expressions(targets, ExpressionContext::Delete)?; + } + Stmt::Assign(StmtAssign { targets, value, .. }) => { + self.scan_expressions(targets, ExpressionContext::Store)?; + self.scan_expression(value, ExpressionContext::Load)?; + } + Stmt::AugAssign(StmtAugAssign { target, value, .. }) => { + self.scan_expression(target, ExpressionContext::Store)?; + self.scan_expression(value, ExpressionContext::Load)?; + } + Stmt::AnnAssign(StmtAnnAssign { + target, + annotation, + value, + simple, + range, + .. + }) => { + self.tables.last_mut().unwrap().annotations_used = true; + // https://github.com/python/cpython/blob/main/Python/symtable.c#L1233 + match &**target { + Expr::Name(ast::ExprName { + id, + range: target_range, + .. + }) => { + let id_str = id.as_str(); + + if *simple { + let existing_flags = self.tables.last().and_then(|table| { + let name = maybe_mangle_name( + self.class_name.as_deref(), + table.mangled_names.as_ref(), + id_str, + ); + table.symbols.get(name.as_ref()).map(|symbol| symbol.flags) + }); + if self + .tables + .last() + .is_some_and(|table| table.typ != CompilerScope::Module) + && let Some(flags) = existing_flags + && flags.intersects( + SymbolFlags::DEF_GLOBAL | SymbolFlags::DEF_NONLOCAL, + ) + { + let usage = if flags.contains(SymbolFlags::DEF_GLOBAL) { + "global" + } else { + "nonlocal" + }; + return Err(SymbolTableError { + error: format!( + "annotated name '{id_str}' can't be {usage}" + ), + location: Some( + self.source_file.to_source_code().source_location( + range.start(), + PositionEncoding::Utf8, + ), + ), + }); } - Some(CompilerScope::Class) => { - self.register_name( - "__annotate_func__", - SymbolUsage::Assigned, - *range, - )?; + + self.register_name( + id_str, + SymbolUsage::AnnotationAssigned, + *target_range, + )?; + // PEP 649: Register annotate function in module/class scope + let current_scope = self.tables.last().map(|t| t.typ); + match current_scope { + Some(CompilerScope::Module) => { + self.register_name( + "__annotate__", + SymbolUsage::Assigned, + *range, + )?; + } + Some(CompilerScope::Class) => { + self.register_name( + "__annotate_func__", + SymbolUsage::Assigned, + *range, + )?; + } + _ => {} } - _ => {} + } else if value.is_some() { + self.register_name(id_str, SymbolUsage::Assigned, *target_range)?; } - } else if value.is_some() { - self.check_name(id_str, ExpressionContext::Store, *range)?; - self.register_name(id_str, SymbolUsage::Assigned, *range)?; + } + _ => { + self.scan_expression(target, ExpressionContext::Store)?; } } - _ => { - self.scan_expression(target, ExpressionContext::Store)?; + self.scan_ann_assign_annotation(annotation)?; + if let Some(value) = value { + self.scan_expression(value, ExpressionContext::Load)?; } } - self.scan_ann_assign_annotation(annotation)?; - if let Some(value) = value { - self.scan_expression(value, ExpressionContext::Load)?; - } - } - Stmt::With(StmtWith { items, body, .. }) => { - for item in items { - self.scan_expression(&item.context_expr, ExpressionContext::Load)?; - if let Some(expression) = &item.optional_vars { - self.scan_expression(expression, ExpressionContext::Store)?; - } - } - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - self.scan_statements(body)?; - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::Try(StmtTry { - body, - handlers, - orelse, - finalbody, - .. - }) => { - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - self.scan_statements(body)?; - // Preserve source-order symbol analysis so `global`/`nonlocal` - // semantics match CPython, but reorder child scope storage to - // match the codegen order for plain try/except/else. - let body_subtables_len = self.tables.last().unwrap().sub_tables.len(); - for handler in handlers { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { - type_, - name, - body, - .. - }) = &handler; - if let Some(expression) = type_ { - self.scan_expression(expression, ExpressionContext::Load)?; + Stmt::With(StmtWith { + items, + body, + is_async, + .. + }) => { + if *is_async && self.allows_top_level_await() { + self.tables.last_mut().unwrap().is_coroutine = true; } - if let Some(name) = name { - self.register_ident(name, SymbolUsage::Assigned)?; + if *is_async && !self.tables.last().unwrap().is_coroutine { + return Err(SymbolTableError { + error: "'async with' outside async function".to_owned(), + location: Some(self.source_file.to_source_code().source_location( + statement.range().start(), + PositionEncoding::Utf8, + )), + }); + } + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + for item in items { + self.scan_expression(&item.context_expr, ExpressionContext::Load)?; + if let Some(expression) = &item.optional_vars { + self.scan_expression(expression, ExpressionContext::Store)?; + } } self.scan_statements(body)?; - } - if finalbody.is_empty() { - let handler_subtables = self - .tables - .last_mut() - .unwrap() - .sub_tables - .split_off(body_subtables_len); - self.scan_statements(orelse)?; - self.tables - .last_mut() - .unwrap() - .sub_tables - .extend(handler_subtables); - } else { + self.in_conditional_block = saved_in_conditional_block; + } + Stmt::Try(StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) => { + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + self.scan_statements(body)?; + for handler in handlers { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_, + name, + body, + .. + }) = &handler; + if let Some(expression) = type_ { + self.scan_expression(expression, ExpressionContext::Load)?; + } + if let Some(name) = name { + self.register_name( + name.as_str(), + SymbolUsage::Assigned, + handler.range(), + )?; + } + self.scan_statements(body)?; + } self.scan_statements(orelse)?; + self.scan_statements(finalbody)?; + self.in_conditional_block = saved_in_conditional_block; + } + Stmt::Match(StmtMatch { subject, cases, .. }) => { + self.scan_expression(subject, ExpressionContext::Load)?; + // PEP 649: Track conditional block for annotations + let saved_in_conditional_block = self.in_conditional_block; + self.in_conditional_block = true; + for case in cases { + self.scan_pattern(&case.pattern)?; + if let Some(guard) = &case.guard { + self.scan_expression(guard, ExpressionContext::Load)?; + } + self.scan_statements(&case.body)?; + } + self.in_conditional_block = saved_in_conditional_block; } - self.scan_statements(finalbody)?; - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::Match(StmtMatch { subject, cases, .. }) => { - self.scan_expression(subject, ExpressionContext::Load)?; - // PEP 649: Track conditional block for annotations - let saved_in_conditional_block = self.in_conditional_block; - self.in_conditional_block = true; - for case in cases { - self.scan_pattern(&case.pattern)?; - if let Some(guard) = &case.guard { - self.scan_expression(guard, ExpressionContext::Load)?; - } - self.scan_statements(&case.body)?; - } - self.in_conditional_block = saved_in_conditional_block; - } - Stmt::Raise(StmtRaise { exc, cause, .. }) => { - if let Some(expression) = exc { - self.scan_expression(expression, ExpressionContext::Load)?; + Stmt::Raise(StmtRaise { exc, cause, .. }) => { + if let Some(expression) = exc { + self.scan_expression(expression, ExpressionContext::Load)?; + if let Some(expression) = cause { + self.scan_expression(expression, ExpressionContext::Load)?; + } + } } - if let Some(expression) = cause { - self.scan_expression(expression, ExpressionContext::Load)?; + Stmt::TypeAlias(StmtTypeAlias { + name, + value, + type_params, + range, + .. + }) => { + let Some(name_expr) = name.as_name_expr() else { + return Err(SymbolTableError { + error: "type alias expects name".to_owned(), + location: Some( + self.source_file + .to_source_code() + .source_location(name.range().start(), PositionEncoding::Utf8), + ), + }); + }; + let alias_name = name_expr.id.to_string(); + self.scan_expression(name, ExpressionContext::Store)?; + // Check before entering any sub-scopes + let in_class = self + .tables + .last() + .is_some_and(|t| t.typ == CompilerScope::Class); + let is_generic = type_params.is_some(); + if let Some(type_params) = type_params { + self.enter_type_param_block(&alias_name, *range, false, false, false)?; + self.scan_type_params(type_params)?; + } + // Value scope for lazy evaluation + self.enter_scope( + &alias_name, + CompilerScope::TypeAlias, + self.line_index_start(*range), + ); + // Evaluator takes a format parameter + self.register_name(".format", SymbolUsage::Parameter, *range)?; + self.register_name(".format", SymbolUsage::Used, *range)?; + if in_class { + if let Some(table) = self.tables.last_mut() { + table.can_see_class_scope = true; + } + self.register_name("__classdict__", SymbolUsage::Used, value.range())?; + } + self.scan_expression(value, ExpressionContext::Load)?; + self.leave_scope(); + if is_generic { + self.leave_scope(); + } } - } - Stmt::TypeAlias(StmtTypeAlias { - name, - value, - type_params, - .. - }) => { - let Some(name_expr) = name.as_name_expr() else { + Stmt::IpyEscapeCommand(stmt) => { return Err(SymbolTableError { - error: "type alias expects name".to_owned(), + error: "invalid syntax".to_owned(), location: Some( self.source_file .to_source_code() - .source_location(name.range().start(), PositionEncoding::Utf8), + .source_location(stmt.range.start(), PositionEncoding::Utf8), ), }); - }; - let alias_name = name_expr.id.to_string(); - let was_in_type_alias = self.in_type_alias; - self.in_type_alias = true; - // Check before entering any sub-scopes - let in_class = self - .tables - .last() - .is_some_and(|t| t.typ == CompilerScope::Class); - let is_generic = type_params.is_some(); - if let Some(type_params) = type_params { - self.enter_type_param_block( - &format!(""), - self.line_index_start(type_params.range), - false, - false, - false, - )?; - self.scan_type_params(type_params)?; - } - // Value scope for lazy evaluation - self.enter_scope( - &alias_name, - CompilerScope::Annotation, - self.line_index_start(value.range()), - ); - // Evaluator takes a format parameter - self.register_name(".format", SymbolUsage::Parameter, TextRange::default())?; - if in_class { - if let Some(table) = self.tables.last_mut() { - table.can_see_class_scope = true; - } - self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?; } - self.scan_expression(value, ExpressionContext::Load)?; - self.leave_scope(); - if is_generic { - self.leave_scope(); - } - self.in_type_alias = was_in_type_alias; - self.scan_expression(name, ExpressionContext::Store)?; } - Stmt::IpyEscapeCommand(_) => todo!(), - } - Ok(()) + Ok(()) + })(); + self.recursion_depth -= 1; + result } fn scan_decorators( @@ -1938,389 +2068,459 @@ impl SymbolTableBuilder { expression: &ast::Expr, context: ExpressionContext, ) -> SymbolTableResult { - use ast::*; - - // Check for expressions not allowed in certain contexts - // (type parameters, annotations, type aliases, TypeVar bounds/defaults) - if let Some(keyword) = match expression { - Expr::Yield(_) | Expr::YieldFrom(_) => Some("yield"), - Expr::Await(_) => Some("await"), - Expr::Named(_) => Some("named"), - _ => None, - } { - // Determine the context name for the error message - // scope_info takes precedence (e.g., "a TypeVar bound") - let context_name = if let Some(scope_info) = self.scope_info { - Some(scope_info) - } else if let Some(table) = self.tables.last() - && table.typ == CompilerScope::TypeParams - { - Some("a type parameter") - } else if self.in_annotation { - Some("an annotation") - } else if self.in_type_alias { - Some("a type alias") - } else { - None - }; - - if let Some(context_name) = context_name { - return Err(SymbolTableError { - error: format!("{keyword} expression cannot be used within {context_name}"), - location: Some( - self.source_file - .to_source_code() - .source_location(expression.range().start(), PositionEncoding::Utf8), - ), - }); - } + if self.recursion_depth >= self.recursion_limit { + return Err(SymbolTableError { + error: RECURSION_ERROR.to_owned(), + location: None, + }); } + self.recursion_depth += 1; + let result = (|| { + use ast::*; + + if expression.is_constant_expr() { + return Ok(()); + } + + // Check for expressions not allowed in certain contexts + // (type parameters, annotations, type aliases, TypeVar bounds/defaults) + if let Some(keyword) = match expression { + Expr::Yield(_) | Expr::YieldFrom(_) => Some("yield"), + Expr::Await(_) => Some("await"), + Expr::Named(_) => Some("named"), + _ => None, + } { + // Determine the context name for the error message from the + // current symbol table entry, matching ste_type checks. + let current_is_comprehension = self + .tables + .last() + .is_some_and(|table| table.typ == CompilerScope::Comprehension); + let context_name = if keyword == "named" && current_is_comprehension { + None + } else if let Some(table) = self.tables.last() { + match table.typ { + CompilerScope::Annotation => Some("an annotation"), + CompilerScope::TypeVariable => table.scope_info, + CompilerScope::TypeAlias => Some("a type alias"), + CompilerScope::TypeParams => Some("the definition of a generic"), + _ => None, + } + } else { + None + }; - match expression { - Expr::BinOp(ExprBinOp { - left, - right, - range: _, - .. - }) => { - self.scan_expression(left, context)?; - self.scan_expression(right, context)?; - } - Expr::BoolOp(ExprBoolOp { - values, range: _, .. - }) => { - self.scan_expressions(values, context)?; - } - Expr::Compare(ExprCompare { - left, - comparators, - range: _, - .. - }) => { - self.scan_expression(left, context)?; - self.scan_expressions(comparators, context)?; - } - Expr::Subscript(ExprSubscript { - value, - slice, - range: _, - .. - }) => { - self.scan_expression(value, ExpressionContext::Load)?; - self.scan_expression(slice, ExpressionContext::Load)?; - } - Expr::Attribute(ExprAttribute { - value, attr, range, .. - }) => { - self.check_name(attr.as_str(), context, *range)?; - self.scan_expression(value, ExpressionContext::Load)?; + if let Some(context_name) = context_name { + return Err(SymbolTableError { + error: format!("{keyword} expression cannot be used within {context_name}"), + location: Some( + self.source_file.to_source_code().source_location( + expression.range().start(), + PositionEncoding::Utf8, + ), + ), + }); + } } - Expr::Dict(ExprDict { - items, - node_index: _, - range: _, - }) => { - for item in items { - if let Some(key) = &item.key { - self.scan_expression(key, context)?; + + match expression { + Expr::BinOp(ExprBinOp { left, right, .. }) => { + self.scan_expression(left, context)?; + self.scan_expression(right, context)?; + } + Expr::BoolOp(ExprBoolOp { values, .. }) => { + self.scan_expressions(values, context)?; + } + Expr::Compare(ExprCompare { + left, comparators, .. + }) => { + self.scan_expression(left, context)?; + self.scan_expressions(comparators, context)?; + } + Expr::Subscript(ExprSubscript { value, slice, .. }) => { + self.scan_expression(value, ExpressionContext::Load)?; + self.scan_expression(slice, ExpressionContext::Load)?; + } + Expr::Attribute(ExprAttribute { + value, attr, range, .. + }) => { + self.check_name(attr.as_str(), context, *range)?; + self.scan_expression(value, ExpressionContext::Load)?; + } + Expr::Dict(ExprDict { items, .. }) => { + for item in items { + if let Some(key) = &item.key { + self.scan_expression(key, context)?; + } + } + for item in items { + self.scan_expression(&item.value, context)?; } - self.scan_expression(&item.value, context)?; } - } - Expr::Await(ExprAwait { - value, - node_index: _, - range: _, - }) => { - self.scan_expression(value, context)?; - self.tables.last_mut().unwrap().is_coroutine = true; - } - Expr::Yield(ExprYield { - value, - node_index: _, - range: _, - }) => { - self.tables.last_mut().unwrap().is_generator = true; - if let Some(expression) = value { - self.scan_expression(expression, context)?; + Expr::Await(ExprAwait { value, .. }) => { + let current_scope = self.tables.last().unwrap().typ; + if !self.allows_top_level_await() + && !Self::is_function_like_scope(current_scope) + { + return Err(SymbolTableError { + error: "'await' outside function".to_owned(), + location: Some(self.source_file.to_source_code().source_location( + expression.range().start(), + PositionEncoding::Utf8, + )), + }); + } + if current_scope != CompilerScope::AsyncFunction + && current_scope != CompilerScope::Comprehension + && !self.allows_top_level_await() + { + return Err(SymbolTableError { + error: "'await' outside async function".to_owned(), + location: Some(self.source_file.to_source_code().source_location( + expression.range().start(), + PositionEncoding::Utf8, + )), + }); + } + self.scan_expression(value, context)?; + self.tables.last_mut().unwrap().is_coroutine = true; } - } - Expr::YieldFrom(ExprYieldFrom { - value, - node_index: _, - range: _, - }) => { - self.tables.last_mut().unwrap().is_generator = true; - self.scan_expression(value, context)?; - } - Expr::UnaryOp(ExprUnaryOp { - operand, range: _, .. - }) => { - self.scan_expression(operand, context)?; - } - Expr::Starred(ExprStarred { - value, range: _, .. - }) => { - self.scan_expression(value, context)?; - } - Expr::Tuple(ExprTuple { elts, range: _, .. }) - | Expr::Set(ExprSet { elts, range: _, .. }) - | Expr::List(ExprList { elts, range: _, .. }) => { - self.scan_expressions(elts, context)?; - } - Expr::Slice(ExprSlice { - lower, - upper, - step, - node_index: _, - range: _, - }) => { - if let Some(lower) = lower { - self.scan_expression(lower, context)?; - } - if let Some(upper) = upper { - self.scan_expression(upper, context)?; - } - if let Some(step) = step { - self.scan_expression(step, context)?; + Expr::Yield(ExprYield { value, .. }) => { + if let Some(expression) = value { + self.scan_expression(expression, context)?; + } + self.tables.last_mut().unwrap().is_generator = true; + if let Some(context_name) = self.comprehension_yield_context + && self + .tables + .last() + .is_some_and(|table| table.typ == CompilerScope::Comprehension) + { + return Err(SymbolTableError { + error: format!("'yield' inside {context_name}"), + location: Some(self.source_file.to_source_code().source_location( + expression.range().start(), + PositionEncoding::Utf8, + )), + }); + } } - } - Expr::Generator(ExprGenerator { - elt, - generators, - range, - .. - }) => { - let was_in_iter_def_exp = self.in_iter_def_exp; - if context == ExpressionContext::IterDefinitionExp { - self.in_iter_def_exp = true; - } - // Generator expression - is_generator = true - self.scan_comprehension("", elt, None, generators, *range, true)?; - self.in_iter_def_exp = was_in_iter_def_exp; - } - Expr::ListComp(ExprListComp { - elt, - generators, - range, - node_index: _, - }) => { - let was_in_iter_def_exp = self.in_iter_def_exp; - if context == ExpressionContext::IterDefinitionExp { - self.in_iter_def_exp = true; - } - // List comprehension - is_generator = false (can be inlined) - self.scan_comprehension("", elt, None, generators, *range, false)?; - self.in_iter_def_exp = was_in_iter_def_exp; - } - Expr::SetComp(ExprSetComp { - elt, - generators, - range, - node_index: _, - }) => { - let was_in_iter_def_exp = self.in_iter_def_exp; - if context == ExpressionContext::IterDefinitionExp { - self.in_iter_def_exp = true; - } - // Set comprehension - is_generator = false (can be inlined) - self.scan_comprehension("", elt, None, generators, *range, false)?; - self.in_iter_def_exp = was_in_iter_def_exp; - } - Expr::DictComp(ExprDictComp { - key, - value, - generators, - range, - node_index: _, - }) => { - let was_in_iter_def_exp = self.in_iter_def_exp; - if context == ExpressionContext::IterDefinitionExp { - self.in_iter_def_exp = true; - } - // Dict comprehension - is_generator = false (can be inlined) - self.scan_comprehension("", key, Some(value), generators, *range, false)?; - self.in_iter_def_exp = was_in_iter_def_exp; - } - Expr::Call(ExprCall { - func, - arguments, - node_index: _, - range: _, - }) => { - match context { - ExpressionContext::IterDefinitionExp => { - self.scan_expression(func, ExpressionContext::IterDefinitionExp)?; + Expr::YieldFrom(ExprYieldFrom { value, .. }) => { + self.scan_expression(value, context)?; + self.tables.last_mut().unwrap().is_generator = true; + if let Some(context_name) = self.comprehension_yield_context + && self + .tables + .last() + .is_some_and(|table| table.typ == CompilerScope::Comprehension) + { + return Err(SymbolTableError { + error: format!("'yield' inside {context_name}"), + location: Some(self.source_file.to_source_code().source_location( + expression.range().start(), + PositionEncoding::Utf8, + )), + }); } - _ => { - self.scan_expression(func, ExpressionContext::Load)?; + } + Expr::UnaryOp(ExprUnaryOp { operand, .. }) => { + self.scan_expression(operand, context)?; + } + Expr::Starred(ExprStarred { value, .. }) => { + self.scan_expression(value, context)?; + } + Expr::Tuple(ExprTuple { elts, .. }) + | Expr::Set(ExprSet { elts, .. }) + | Expr::List(ExprList { elts, .. }) => { + self.scan_expressions(elts, context)?; + } + Expr::Slice(ExprSlice { + lower, upper, step, .. + }) => { + if let Some(lower) = lower { + self.scan_expression(lower, context)?; + } + if let Some(upper) = upper { + self.scan_expression(upper, context)?; + } + if let Some(step) = step { + self.scan_expression(step, context)?; } } - - self.scan_expressions(&arguments.args, ExpressionContext::Load)?; - for keyword in &arguments.keywords { - if let Some(arg) = &keyword.arg { - self.check_name(arg.as_str(), ExpressionContext::Store, keyword.range)?; + Expr::Generator(ExprGenerator { + elt, + generators, + range, + .. + }) => { + let was_in_iter_def_exp = self.in_iter_def_exp; + if context == ExpressionContext::IterDefinitionExp { + self.in_iter_def_exp = true; + } + // Generator expression - is_generator = true + self.scan_comprehension("", elt, None, generators, *range, true)?; + self.in_iter_def_exp = was_in_iter_def_exp; + } + Expr::ListComp(ExprListComp { + elt, + generators, + range, + .. + }) => { + let was_in_iter_def_exp = self.in_iter_def_exp; + if context == ExpressionContext::IterDefinitionExp { + self.in_iter_def_exp = true; } - self.scan_expression(&keyword.value, ExpressionContext::Load)?; + // List comprehension - is_generator = false (can be inlined) + self.scan_comprehension("", elt, None, generators, *range, false)?; + self.in_iter_def_exp = was_in_iter_def_exp; + } + Expr::SetComp(ExprSetComp { + elt, + generators, + range, + .. + }) => { + let was_in_iter_def_exp = self.in_iter_def_exp; + if context == ExpressionContext::IterDefinitionExp { + self.in_iter_def_exp = true; + } + // Set comprehension - is_generator = false (can be inlined) + self.scan_comprehension("", elt, None, generators, *range, false)?; + self.in_iter_def_exp = was_in_iter_def_exp; + } + Expr::DictComp(ExprDictComp { + key, + value, + generators, + range, + .. + }) => { + let was_in_iter_def_exp = self.in_iter_def_exp; + if context == ExpressionContext::IterDefinitionExp { + self.in_iter_def_exp = true; + } + // Dict comprehension - is_generator = false (can be inlined) + let key = key.as_ref(); + self.scan_comprehension( + "", + key, + Some(value), + generators, + *range, + false, + )?; + self.in_iter_def_exp = was_in_iter_def_exp; } - } - Expr::Name(ExprName { id, range, .. }) => { - let id = id.as_str(); - - self.check_name(id, context, *range)?; - - if !self.in_unevaluated_annotation { - // Determine the contextual usage of this symbol: + Expr::Call(ExprCall { + func, arguments, .. + }) => { match context { - ExpressionContext::Delete => { - self.register_name(id, SymbolUsage::Assigned, *range)?; - self.register_name(id, SymbolUsage::Used, *range)?; + ExpressionContext::IterDefinitionExp => { + self.scan_expression(func, ExpressionContext::IterDefinitionExp)?; } - ExpressionContext::Load | ExpressionContext::IterDefinitionExp => { - self.register_name(id, SymbolUsage::Used, *range)?; + _ => { + self.scan_expression(func, ExpressionContext::Load)?; } - ExpressionContext::Store => { - self.register_name(id, SymbolUsage::Assigned, *range)?; - } - ExpressionContext::Iter => { - self.register_name(id, SymbolUsage::Iter, *range)?; + } + + self.scan_expressions(&arguments.args, ExpressionContext::Load)?; + for keyword in &arguments.keywords { + if let Some(arg) = &keyword.arg { + self.check_name(arg.as_str(), ExpressionContext::Store, keyword.range)?; } } - // Interesting stuff about the __class__ variable: - // https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object - if context == ExpressionContext::Load - && matches!( - self.tables.last().unwrap().typ, - CompilerScope::Function | CompilerScope::AsyncFunction - ) - && id == "super" + for keyword in &arguments.keywords { + self.scan_expression(&keyword.value, ExpressionContext::Load)?; + } + } + Expr::Name(ExprName { id, range, .. }) => { + let id = id.as_str(); + + self.check_name(id, context, *range)?; + + if !self + .tables + .last() + .is_some_and(|table| table.in_unevaluated_annotation) { - self.register_name("__class__", SymbolUsage::Used, *range)?; + // Determine the contextual usage of this symbol: + match context { + ExpressionContext::Delete => { + self.register_name(id, SymbolUsage::Assigned, *range)?; + } + ExpressionContext::Load | ExpressionContext::IterDefinitionExp => { + self.register_name(id, SymbolUsage::Used, *range)?; + } + ExpressionContext::Store => { + self.register_name(id, SymbolUsage::Assigned, *range)?; + } + ExpressionContext::Iter => { + self.register_name(id, SymbolUsage::Iter, *range)?; + } + } + // Interesting stuff about the __class__ variable: + // https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object + if context == ExpressionContext::Load + && Self::is_function_like_scope(self.tables.last().unwrap().typ) + && id == "super" + { + self.register_name("__class__", SymbolUsage::Used, *range)?; + } } } - } - Expr::Lambda(ExprLambda { - body, - parameters, - node_index: _, - range: _, - }) => { - if let Some(parameters) = parameters { - self.enter_scope_with_parameters( - "lambda", - parameters, - self.line_index_start(expression.range()), - false, // lambdas have no return annotation - CompilerScope::Lambda, - false, // don't skip defaults - )?; - } else { - self.enter_scope( - "lambda", - CompilerScope::Lambda, - self.line_index_start(expression.range()), - ); + Expr::Lambda(ExprLambda { + body, parameters, .. + }) => { + let was_in_iter_def_exp = self.in_iter_def_exp; + if let Some(parameters) = parameters { + if was_in_iter_def_exp { + self.scan_parameter_defaults(parameters)?; + } + self.enter_scope_with_parameters( + "lambda", + parameters, + self.line_index_start(expression.range()), + None, // lambdas have no return annotation + CompilerScope::Lambda, + was_in_iter_def_exp, + false, + )?; + } else { + self.enter_scope( + "lambda", + CompilerScope::Lambda, + self.line_index_start(expression.range()), + ); + } + self.scan_expression(body, ExpressionContext::Load)?; + self.in_iter_def_exp = was_in_iter_def_exp; + self.leave_scope(); } - match context { - ExpressionContext::IterDefinitionExp => { - self.scan_expression(body, ExpressionContext::IterDefinitionExp)?; + Expr::FString(fstring) => { + if let Some(joined_str) = &fstring.runtime_joined_str { + for expr in joined_str { + self.scan_expression(expr, ExpressionContext::Load)?; + } + return Ok(()); } - _ => { - self.scan_expression(body, ExpressionContext::Load)?; + for expr in fstring + .value + .elements() + .filter_map(|x| x.as_interpolation()) + { + self.scan_expression(&expr.expression, ExpressionContext::Load)?; + if let Some(format_spec) = &expr.runtime_formatted_value_format_spec { + self.scan_expression(format_spec, ExpressionContext::Load)?; + } else if let Some(format_spec) = &expr.format_spec { + for element in format_spec.elements.interpolations() { + self.scan_expression(&element.expression, ExpressionContext::Load)? + } + } } } - self.leave_scope(); - } - Expr::FString(ExprFString { value, .. }) => { - for expr in value.elements().filter_map(|x| x.as_interpolation()) { - self.scan_expression(&expr.expression, ExpressionContext::Load)?; - if let Some(format_spec) = &expr.format_spec { - for element in format_spec.elements.interpolations() { - self.scan_expression(&element.expression, ExpressionContext::Load)? + Expr::TString(tstring) => { + if let Some(template_str) = &tstring.runtime_template_str { + for expr in template_str { + self.scan_expression(expr, ExpressionContext::Load)?; } + return Ok(()); } - } - } - Expr::TString(tstring) => { - // Scan t-string interpolation expressions (similar to f-strings) - for expr in tstring - .value - .elements() - .filter_map(|x| x.as_interpolation()) - { - self.scan_expression(&expr.expression, ExpressionContext::Load)?; - if let Some(format_spec) = &expr.format_spec { - for element in format_spec.elements.interpolations() { - self.scan_expression(&element.expression, ExpressionContext::Load)? + // Scan t-string interpolation expressions (similar to f-strings) + for expr in tstring + .value + .elements() + .filter_map(|x| x.as_interpolation()) + { + self.scan_expression(&expr.expression, ExpressionContext::Load)?; + if expr.runtime_str.is_some() { + if let Some(format_spec) = &expr.runtime_interpolation_format_spec { + self.scan_expression(format_spec, ExpressionContext::Load)?; + } + } else if let Some(format_spec) = &expr.format_spec { + for element in format_spec.elements.interpolations() { + self.scan_expression(&element.expression, ExpressionContext::Load)? + } } } } - } - // Constants - Expr::StringLiteral(_) - | Expr::BytesLiteral(_) - | Expr::NumberLiteral(_) - | Expr::BooleanLiteral(_) - | Expr::NoneLiteral(_) - | Expr::EllipsisLiteral(_) => {} - Expr::IpyEscapeCommand(_) => todo!(), - Expr::If(ExprIf { - test, - body, - orelse, - node_index: _, - range: _, - }) => { - self.scan_expression(test, ExpressionContext::Load)?; - self.scan_expression(body, ExpressionContext::Load)?; - self.scan_expression(orelse, ExpressionContext::Load)?; - } - - Expr::Named(ExprNamed { - target, - value, - range, - node_index: _, - }) => { - // named expressions are not allowed in the definition of - // comprehension iterator definitions (including nested comprehensions) - if context == ExpressionContext::IterDefinitionExp || self.in_iter_def_exp { + // Constants + Expr::StringLiteral(_) + | Expr::BytesLiteral(_) + | Expr::NumberLiteral(_) + | Expr::Constant(_) + | Expr::BooleanLiteral(_) + | Expr::NoneLiteral(_) + | Expr::EllipsisLiteral(_) => {} + Expr::IpyEscapeCommand(expr) => { return Err(SymbolTableError { - error: "assignment expression cannot be used in a comprehension iterable expression".to_string(), - location: Some(self.source_file.to_source_code().source_location(target.range().start(), PositionEncoding::Utf8)), - }); + error: "invalid syntax".to_owned(), + location: Some( + self.source_file + .to_source_code() + .source_location(expr.range.start(), PositionEncoding::Utf8), + ), + }); + } + Expr::If(ExprIf { + test, body, orelse, .. + }) => { + self.scan_expression(test, ExpressionContext::Load)?; + self.scan_expression(body, ExpressionContext::Load)?; + self.scan_expression(orelse, ExpressionContext::Load)?; } - self.scan_expression(value, ExpressionContext::Load)?; + Expr::Named(ExprNamed { + target, + value, + range, + .. + }) => { + // named expressions are not allowed in the definition of + // comprehension iterator definitions (including nested comprehensions) + if context == ExpressionContext::IterDefinitionExp || self.in_iter_def_exp { + return Err(SymbolTableError { + error: + "assignment expression cannot be used in a comprehension iterable expression" + .to_string(), + location: Some( + self.source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8), + ), + }); + } + + let named_target = if let Expr::Name(ExprName { + id, + range: target_range, + .. + }) = &**target + { + let id = id.as_str(); + self.check_name(id, ExpressionContext::Store, *target_range)?; + let table = self.tables.last().unwrap(); + if table.typ == CompilerScope::Comprehension { + self.extend_namedexpr_scope(id, *target_range)?; + } + Some((id, *target_range)) + } else { + None + }; + + self.scan_expression(value, ExpressionContext::Load)?; - // special handling for assigned identifier in named expressions - // that are used in comprehensions. This required to correctly - // propagate the scope of the named assigned named and not to - // propagate inner names. - if let Expr::Name(ExprName { id, .. }) = &**target { - let id = id.as_str(); - self.check_name(id, ExpressionContext::Store, *range)?; - let table = self.tables.last().unwrap(); - if table.typ == CompilerScope::Comprehension { - self.extend_namedexpr_scope(id, *range)?; - self.register_name( - id, - SymbolUsage::AssignedNamedExprInComprehension, - *range, - )?; + if let Some((id, target_range)) = named_target { + self.register_name(id, SymbolUsage::Assigned, target_range)?; } else { - // omit one recursion. When the handling of an store changes for - // Identifiers this needs adapted - more forward safe would be - // calling scan_expression directly. - self.register_name(id, SymbolUsage::Assigned, *range)?; + self.scan_expression(target, ExpressionContext::Store)?; } - } else { - self.scan_expression(target, ExpressionContext::Store)?; } } - } - Ok(()) + Ok(()) + })(); + self.recursion_depth -= 1; + result } fn scan_comprehension( @@ -2332,26 +2532,15 @@ impl SymbolTableBuilder { range: TextRange, is_generator: bool, ) -> SymbolTableResult { - // Check for async comprehension outside async function - // (list/set/dict comprehensions only, not generator expressions) - let has_async_gen = generators.iter().any(|g| g.is_async); - if has_async_gen && !is_generator && !self.is_in_async_context() { - return Err(SymbolTableError { - error: "asynchronous comprehension outside of an asynchronous function".to_owned(), - location: Some( - self.source_file - .to_source_code() - .source_location(range.start(), PositionEncoding::Utf8), - ), - }); - } - assert!(!generators.is_empty()); let outermost = &generators[0]; // CPython evaluates the outermost iterator in the enclosing scope // before entering the comprehension scope. + let was_in_iter_def_exp = self.in_iter_def_exp; + self.in_iter_def_exp = true; self.scan_expression(&outermost.iter, ExpressionContext::IterDefinitionExp)?; + self.in_iter_def_exp = was_in_iter_def_exp; // Comprehensions are compiled as functions, so create a scope for them: self.enter_scope( @@ -2359,14 +2548,12 @@ impl SymbolTableBuilder { CompilerScope::Comprehension, self.line_index_start(range), ); - // Generator expressions need the is_generator flag - self.tables.last_mut().unwrap().is_generator = is_generator; - if generators.iter().any(|generator| generator.is_async) { + if outermost.is_async { self.tables.last_mut().unwrap().is_coroutine = true; } // PEP 709: Mark non-generator comprehensions for inlining. - // CPython's symtable marks all non-generator comprehensions for + // symtable marks all non-generator comprehensions for // inlining, except scopes nested under a parent that can see class // scope (for example annotation scopes inside classes). if !is_generator { @@ -2380,31 +2567,63 @@ impl SymbolTableBuilder { // Register the passed argument to the generator function as the name ".0" self.register_name(".0", SymbolUsage::Parameter, range)?; + let saved_comprehension_yield_context = self.comprehension_yield_context; + self.comprehension_yield_context = Some(match scope_name { + "" => "list comprehension", + "" => "set comprehension", + "" => "dict comprehension", + "" => "generator expression", + _ => "comprehension", + }); + self.scan_expression(&outermost.target, ExpressionContext::Iter)?; for if_expr in &outermost.ifs { self.scan_expression(if_expr, ExpressionContext::Load)?; } for generator in &generators[1..] { - self.in_comp_inner_loop_target = true; self.scan_expression(&generator.target, ExpressionContext::Iter)?; - self.in_comp_inner_loop_target = false; + let was_in_iter_def_exp = self.in_iter_def_exp; + self.in_iter_def_exp = true; self.scan_expression(&generator.iter, ExpressionContext::IterDefinitionExp)?; + self.in_iter_def_exp = was_in_iter_def_exp; for if_expr in &generator.ifs { self.scan_expression(if_expr, ExpressionContext::Load)?; } + if generator.is_async { + self.tables.last_mut().unwrap().is_coroutine = true; + } } if let Some(elt2) = elt2 { self.scan_expression(elt2, ExpressionContext::Load)?; } self.scan_expression(elt1, ExpressionContext::Load)?; + self.tables.last_mut().unwrap().is_generator = is_generator; + self.comprehension_yield_context = saved_comprehension_yield_context; - // CPython symtable_handle_comprehension(): non-generator async + // symtable_handle_comprehension(): non-generator async // comprehensions propagate ste_coroutine to the enclosing scope after // the comprehension block is exited. let propagate_coroutine = self.tables.last().unwrap().is_coroutine && !is_generator; self.leave_scope(); + if propagate_coroutine + && self + .tables + .last() + .is_none_or(|table| table.typ != CompilerScope::Comprehension) + && !self.is_in_async_context() + && !self.allows_top_level_await() + { + return Err(SymbolTableError { + error: "asynchronous comprehension outside of an asynchronous function".to_owned(), + location: Some( + self.source_file + .to_source_code() + .source_location(range.start(), PositionEncoding::Utf8), + ), + }); + } if propagate_coroutine { self.tables.last_mut().unwrap().is_coroutine = true; } @@ -2420,142 +2639,148 @@ impl SymbolTableBuilder { scope_name: &str, scope_info: &'static str, ) -> SymbolTableResult { - // Bounds/defaults are compiled as annotation scopes in CPython. + // Bounds/defaults are compiled as annotation scopes. let in_class = self.tables.last().is_some_and(|t| t.can_see_class_scope); let line_number = self.line_index_start(expr.range()); - self.enter_scope(scope_name, CompilerScope::Annotation, line_number); + self.enter_scope(scope_name, CompilerScope::TypeVariable, line_number); // Evaluator takes a format parameter - self.register_name(".format", SymbolUsage::Parameter, TextRange::default())?; + self.register_name(".format", SymbolUsage::Parameter, expr.range())?; + self.register_name(".format", SymbolUsage::Used, expr.range())?; if in_class { if let Some(table) = self.tables.last_mut() { table.can_see_class_scope = true; } - self.register_name("__classdict__", SymbolUsage::Used, TextRange::default())?; + self.register_name("__classdict__", SymbolUsage::Used, expr.range())?; } - // Set scope_info for better error messages - let old_scope_info = self.scope_info; - self.scope_info = Some(scope_info); + self.tables.last_mut().unwrap().scope_info = Some(scope_info); // Scan the expression in this new scope let result = self.scan_expression(expr, ExpressionContext::Load); - // Restore scope_info and exit the scope - self.scope_info = old_scope_info; self.leave_scope(); result } fn scan_type_params(&mut self, type_params: &ast::TypeParams) -> SymbolTableResult { - // Check for duplicate type parameter names - let mut seen_names: IndexSet<&str> = IndexSet::default(); - // Check for non-default type parameter after default type parameter - let mut default_seen = false; + // Each type parameter is visited as: register name, scan bound, scan default. for type_param in &type_params.type_params { - let (name, range, has_default) = match type_param { - ast::TypeParam::TypeVar(tv) => (tv.name.as_str(), tv.range, tv.default.is_some()), - ast::TypeParam::ParamSpec(ps) => (ps.name.as_str(), ps.range, ps.default.is_some()), - ast::TypeParam::TypeVarTuple(tvt) => { - (tvt.name.as_str(), tvt.range, tvt.default.is_some()) - } - }; - if !seen_names.insert(name) { + if self.recursion_depth >= self.recursion_limit { return Err(SymbolTableError { - error: format!("duplicate type parameter '{name}'"), - location: Some( - self.source_file - .to_source_code() - .source_location(range.start(), PositionEncoding::Utf8), - ), - }); - } - if has_default { - default_seen = true; - } else if default_seen { - return Err(SymbolTableError { - error: format!( - "non-default type parameter '{name}' follows default type parameter" - ), - location: Some( - self.source_file - .to_source_code() - .source_location(range.start(), PositionEncoding::Utf8), - ), + error: RECURSION_ERROR.to_owned(), + location: None, }); } - } - - // Register .type_params as a type parameter (automatically becomes cell variable) - self.register_name(".type_params", SymbolUsage::TypeParam, type_params.range)?; - - // First register all type parameters - for type_param in &type_params.type_params { - match type_param { - ast::TypeParam::TypeVar(ast::TypeParamTypeVar { - name, - bound, - range: type_var_range, - default, - node_index: _, - }) => { - self.register_name(name.as_str(), SymbolUsage::TypeParam, *type_var_range)?; + self.recursion_depth += 1; + let result = (|| { + match type_param { + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { + name, + bound, + range: type_var_range, + default, + .. + }) => { + self.register_name(name.as_str(), SymbolUsage::TypeParam, *type_var_range)?; + if name.as_str() == "__classdict__" { + return Err(SymbolTableError { + error: format!( + "reserved name '{}' cannot be used for type parameter", + name.as_str() + ), + location: Some(self.source_file.to_source_code().source_location( + type_var_range.start(), + PositionEncoding::Utf8, + )), + }); + } - // Process bound in a separate scope - if let Some(binding) = bound { - let scope_info = if binding.is_tuple_expr() { - "a TypeVar constraint" - } else { - "a TypeVar bound" - }; - self.scan_type_param_bound_or_default(binding, name.as_str(), scope_info)?; - } + // Process bound in a separate scope + if let Some(binding) = bound { + let scope_info = if binding.is_tuple_expr() { + "a TypeVar constraint" + } else { + "a TypeVar bound" + }; + self.scan_type_param_bound_or_default( + binding, + name.as_str(), + scope_info, + )?; + } - // Process default in a separate scope - if let Some(default_value) = default { - self.scan_type_param_bound_or_default( - default_value, - name.as_str(), - "a TypeVar default", - )?; + // Process default in a separate scope + if let Some(default_value) = default { + self.scan_type_param_bound_or_default( + default_value, + name.as_str(), + "a TypeVar default", + )?; + } } - } - ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { - name, - range: param_spec_range, - default, - node_index: _, - }) => { - self.register_name(name, SymbolUsage::TypeParam, *param_spec_range)?; + ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { + name, + range: param_spec_range, + default, + .. + }) => { + self.register_name(name, SymbolUsage::TypeParam, *param_spec_range)?; + if name == "__classdict__" { + return Err(SymbolTableError { + error: format!( + "reserved name '{name}' cannot be used for type parameter" + ), + location: Some(self.source_file.to_source_code().source_location( + param_spec_range.start(), + PositionEncoding::Utf8, + )), + }); + } - // Process default in a separate scope - if let Some(default_value) = default { - self.scan_type_param_bound_or_default( - default_value, - name, - "a ParamSpec default", - )?; + // Process default in a separate scope + if let Some(default_value) = default { + self.scan_type_param_bound_or_default( + default_value, + name, + "a ParamSpec default", + )?; + } } - } - ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { - name, - range: type_var_tuple_range, - default, - node_index: _, - }) => { - self.register_name(name, SymbolUsage::TypeParam, *type_var_tuple_range)?; + ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { + name, + range: type_var_tuple_range, + default, + .. + }) => { + self.register_name(name, SymbolUsage::TypeParam, *type_var_tuple_range)?; + if name == "__classdict__" { + return Err(SymbolTableError { + error: format!( + "reserved name '{name}' cannot be used for type parameter" + ), + location: Some(self.source_file.to_source_code().source_location( + type_var_tuple_range.start(), + PositionEncoding::Utf8, + )), + }); + } - // Process default in a separate scope - if let Some(default_value) = default { - self.scan_type_param_bound_or_default( - default_value, - name, - "a TypeVarTuple default", - )?; + // Process default in a separate scope + if let Some(default_value) = default { + self.scan_type_param_bound_or_default( + default_value, + name, + "a TypeVarTuple default", + )?; + } } } - } + Ok(()) + })(); + self.recursion_depth -= 1; + result?; } Ok(()) } @@ -2568,51 +2793,86 @@ impl SymbolTableBuilder { } fn scan_pattern(&mut self, pattern: &ast::Pattern) -> SymbolTableResult { - match pattern { - ast::Pattern::MatchValue(ast::PatternMatchValue { value, .. }) => { - self.scan_expression(value, ExpressionContext::Load)? - } - ast::Pattern::MatchSingleton(_) => {} - ast::Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { - self.scan_patterns(patterns)? - } - ast::Pattern::MatchMapping(ast::PatternMatchMapping { - keys, - patterns, - rest, - .. - }) => { - self.scan_expressions(keys, ExpressionContext::Load)?; - self.scan_patterns(patterns)?; - if let Some(rest) = rest { - self.register_ident(rest, SymbolUsage::Assigned)?; - } - } - ast::Pattern::MatchClass(ast::PatternMatchClass { cls, arguments, .. }) => { - self.scan_expression(cls, ExpressionContext::Load)?; - self.scan_patterns(&arguments.patterns)?; - for kw in &arguments.keywords { - self.scan_pattern(&kw.pattern)?; + if self.recursion_depth >= self.recursion_limit { + return Err(SymbolTableError { + error: RECURSION_ERROR.to_owned(), + location: None, + }); + } + self.recursion_depth += 1; + let result = (|| { + use ast::Pattern::{ + MatchAs, MatchClass, MatchMapping, MatchOr, MatchSequence, MatchSingleton, + MatchStar, MatchValue, + }; + match pattern { + MatchValue(ast::PatternMatchValue { value, .. }) => { + self.scan_expression(value, ExpressionContext::Load)? + } + MatchSingleton(_) => {} + MatchSequence(ast::PatternMatchSequence { patterns, .. }) => { + self.scan_patterns(patterns)? + } + MatchMapping(ast::PatternMatchMapping { + keys, + patterns, + rest, + .. + }) => { + self.scan_expressions(keys, ExpressionContext::Load)?; + self.scan_patterns(patterns)?; + if let Some(rest) = rest { + if rest.as_str() == "_" { + return Err(SymbolTableError { + error: "invalid syntax".to_owned(), + location: Some( + self.source_file.to_source_code().source_location( + rest.range.start(), + PositionEncoding::Utf8, + ), + ), + }); + } + self.register_name(rest.as_str(), SymbolUsage::Assigned, pattern.range())?; + } } - } - ast::Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => { - if let Some(name) = name { - self.register_ident(name, SymbolUsage::Assigned)?; + MatchClass(ast::PatternMatchClass { cls, arguments, .. }) => { + self.scan_expression(cls, ExpressionContext::Load)?; + self.scan_patterns(&arguments.patterns)?; + for kw in &arguments.keywords { + self.check_name( + kw.attr.as_str(), + ExpressionContext::Store, + kw.pattern.range(), + )?; + } + for kw in &arguments.keywords { + self.scan_pattern(&kw.pattern)?; + } } - } - ast::Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => { - if let Some(pattern) = pattern { - self.scan_pattern(pattern)?; + MatchStar(ast::PatternMatchStar { name, .. }) => { + if let Some(name) = name { + self.register_name(name.as_str(), SymbolUsage::Assigned, pattern.range())?; + } } - if let Some(name) = name { - self.register_ident(name, SymbolUsage::Assigned)?; + MatchAs(ast::PatternMatchAs { + pattern: as_pattern, + name, + .. + }) => { + if let Some(as_pattern) = as_pattern { + self.scan_pattern(as_pattern)?; + } + if let Some(name) = name { + self.register_name(name.as_str(), SymbolUsage::Assigned, pattern.range())?; + } } + MatchOr(ast::PatternMatchOr { patterns, .. }) => self.scan_patterns(patterns)?, } - ast::Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => { - self.scan_patterns(patterns)? - } - } - Ok(()) + Ok(()) + })(); + self.recursion_depth -= 1; + result } /// Scan default parameter values (evaluated in the enclosing scope) @@ -2636,79 +2896,43 @@ impl SymbolTableBuilder { .any(|arg| arg.default.is_some()) } + fn has_positional_defaults(parameters: &ast::Parameters) -> bool { + parameters + .posonlyargs + .iter() + .chain(parameters.args.iter()) + .any(|arg| arg.default.is_some()) + } + + #[expect( + clippy::too_many_arguments, + reason = "keeps parameter/default scanning options explicit at call sites" + )] fn enter_scope_with_parameters( &mut self, name: &str, parameters: &ast::Parameters, line_number: u32, - has_return_annotation: bool, + returns: Option<&ast::Expr>, scope_type: CompilerScope, skip_defaults: bool, + skip_annotations: bool, ) -> SymbolTableResult { // Evaluate eventual default parameters (unless already scanned before type_param_block): if !skip_defaults { self.scan_parameter_defaults(parameters)?; } - // Annotations are scanned in outer scope: - for annotation in parameters - .posonlyargs - .iter() - .chain(parameters.args.iter()) - .chain(parameters.kwonlyargs.iter()) - .filter_map(|arg| arg.parameter.annotation.as_ref()) - { - self.scan_annotation(annotation)?; - } - if let Some(annotation) = parameters - .vararg - .as_ref() - .and_then(|arg| arg.annotation.as_ref()) - { - self.scan_annotation(annotation)?; - } - if let Some(annotation) = parameters - .kwarg - .as_ref() - .and_then(|arg| arg.annotation.as_ref()) - { - self.scan_annotation(annotation)?; + let is_function_scope = matches!( + scope_type, + CompilerScope::Function | CompilerScope::AsyncFunction + ); + if is_function_scope && !skip_annotations { + self.scan_function_annotations(parameters, returns, line_number)?; } - // Check if this function has any annotations (parameter or return) - let has_param_annotations = parameters - .posonlyargs - .iter() - .chain(parameters.args.iter()) - .chain(parameters.kwonlyargs.iter()) - .any(|p| p.parameter.annotation.is_some()) - || parameters - .vararg - .as_ref() - .is_some_and(|p| p.annotation.is_some()) - || parameters - .kwarg - .as_ref() - .is_some_and(|p| p.annotation.is_some()); - - let has_any_annotations = has_param_annotations || has_return_annotation; - - // Take annotation_block if this function has any annotations. - // When in class scope, the class's annotation_block was saved before scanning - // function annotations, so the current annotation_block belongs to this function. - let annotation_block = if has_any_annotations { - self.tables.last_mut().unwrap().annotation_block.take() - } else { - None - }; - self.enter_scope(name, scope_type, line_number); - // Move annotation_block to function scope only if we have one - if let Some(block) = annotation_block { - self.tables.last_mut().unwrap().annotation_block = Some(block); - } - // Fill scope with parameter names: self.scan_parameters(¶meters.posonlyargs)?; self.scan_parameters(¶meters.args)?; @@ -2757,7 +2981,7 @@ impl SymbolTableBuilder { Ok(()) } - // Mirrors CPython symtable_extend_namedexpr_scope(): assignment expressions + // Mirrors symtable_extend_namedexpr_scope(): assignment expressions // inside comprehensions bind in the nearest function/module-like scope, not // in the synthetic comprehension scope itself. fn extend_namedexpr_scope(&mut self, name: &str, range: TextRange) -> SymbolTableResult { @@ -2780,11 +3004,15 @@ impl SymbolTableBuilder { if self.tables[table_idx] .symbols .get(mangled.as_str()) - .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::ITER)) + .is_some_and(|symbol| { + symbol + .flags + .contains(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_COMP_ITER) + }) { return Err(SymbolTableError { error: format!( - "assignment expression cannot rebind comprehension iteration variable '{mangled}'" + "assignment expression cannot rebind comprehension iteration variable '{name}'" ), location, }); @@ -2797,17 +3025,17 @@ impl SymbolTableBuilder { let parent_is_global = self.tables[table_idx] .symbols .get(mangled.as_str()) - .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::GLOBAL)); + .is_some_and(|symbol| symbol.flags.contains(SymbolFlags::DEF_GLOBAL)); let current = self.tables.last_mut().unwrap(); let current_symbol = current .symbols .entry(mangled.clone()) .or_insert_with(|| Symbol::new(mangled.as_str())); if parent_is_global { - current_symbol.flags.insert(SymbolFlags::GLOBAL); + current_symbol.flags.insert(SymbolFlags::DEF_GLOBAL); current_symbol.scope = SymbolScope::GlobalExplicit; } else { - current_symbol.flags.insert(SymbolFlags::NONLOCAL); + current_symbol.flags.insert(SymbolFlags::DEF_NONLOCAL); current_symbol.scope = SymbolScope::Free; } @@ -2815,7 +3043,7 @@ impl SymbolTableBuilder { .symbols .entry(mangled.clone()) .or_insert_with(|| Symbol::new(mangled.as_str())); - symbol.flags.insert(SymbolFlags::ASSIGNED); + symbol.flags.insert(SymbolFlags::DEF_LOCAL); return Ok(()); } CompilerScope::Module => { @@ -2824,14 +3052,14 @@ impl SymbolTableBuilder { .symbols .entry(mangled.clone()) .or_insert_with(|| Symbol::new(mangled.as_str())); - current_symbol.flags.insert(SymbolFlags::GLOBAL); + current_symbol.flags.insert(SymbolFlags::DEF_GLOBAL); current_symbol.scope = SymbolScope::GlobalExplicit; let symbol = self.tables[table_idx] .symbols .entry(mangled.clone()) .or_insert_with(|| Symbol::new(mangled.as_str())); - symbol.flags.insert(SymbolFlags::GLOBAL); + symbol.flags.insert(SymbolFlags::DEF_GLOBAL); symbol.scope = SymbolScope::GlobalExplicit; return Ok(()); } @@ -2847,12 +3075,23 @@ impl SymbolTableBuilder { location, }); } - CompilerScope::Annotation => { + CompilerScope::TypeAlias => { + return Err(SymbolTableError { + error: + "assignment expression within a comprehension cannot be used in a type alias" + .to_string(), + location, + }); + } + CompilerScope::TypeVariable => { return Err(SymbolTableError { - error: "named expression cannot be used within an annotation".to_string(), + error: + "assignment expression within a comprehension cannot be used in a TypeVar bound" + .to_string(), location, }); } + CompilerScope::Annotation => {} CompilerScope::Comprehension => unreachable!(), } } @@ -2872,10 +3111,26 @@ impl SymbolTableBuilder { .source_location(range.start(), PositionEncoding::Utf8); let location = Some(location); - // Note: __debug__ checks are handled by check_name function, so no check needed here. + // symtable_add_def_ctx() runs check_name() for definition + // roles covered by DEF_PARAM | DEF_LOCAL | DEF_IMPORT before adding + // the symbol. Several Rust callers reach register_name() directly + // instead of going through scan_expression(Name), so keep the guard here. + if matches!( + role, + SymbolUsage::Assigned + | SymbolUsage::Imported + | SymbolUsage::AnnotationAssigned + | SymbolUsage::Parameter + | SymbolUsage::AnnotationParameter + | SymbolUsage::Iter + | SymbolUsage::TypeParam + ) { + self.check_name(name, ExpressionContext::Store, range)?; + } let scope_depth = self.tables.len(); let table = self.tables.last_mut().unwrap(); + let current_scope = table.typ; // Add type param names to mangled_names set for selective mangling if matches!(role, SymbolUsage::TypeParam) @@ -2884,6 +3139,7 @@ impl SymbolTableBuilder { set.insert(name.to_owned()); } + let original_name = name; let name = maybe_mangle_name( self.class_name.as_deref(), table.mangled_names.as_ref(), @@ -2893,43 +3149,61 @@ impl SymbolTableBuilder { let symbol = if let Some(symbol) = table.symbols.get_mut(name.as_ref()) { let flags = &symbol.flags; - // INNER_LOOP_CONFLICT: comprehension inner loop cannot rebind - // a variable that was used as a named expression target + // Mirrors CPython's INNER_LOOP_CONFLICT check. extend_namedexpr_scope() + // marks named-expression targets as global or nonlocal in the comprehension. // Example: [i for i in range(5) if (j := 0) for j in range(5)] // Here 'j' is used in named expr first, then as inner loop iter target - if self.in_comp_inner_loop_target - && flags.contains(SymbolFlags::ASSIGNED_IN_COMPREHENSION) + if matches!(role, SymbolUsage::Iter) + && flags.intersects(SymbolFlags::DEF_GLOBAL | SymbolFlags::DEF_NONLOCAL) { return Err(SymbolTableError { error: format!( - "comprehension inner loop cannot rebind assignment expression target '{name}'" + "comprehension inner loop cannot rebind assignment expression target '{original_name}'" ), location, }); } + if matches!( + role, + SymbolUsage::Parameter | SymbolUsage::AnnotationParameter + ) && flags.contains(SymbolFlags::DEF_PARAM) + { + return Err(SymbolTableError { + error: format!("duplicate argument '{original_name}' in function definition"), + location, + }); + } + // Role already set.. + if matches!(role, SymbolUsage::TypeParam) && flags.contains(SymbolFlags::DEF_TYPE_PARAM) + { + return Err(SymbolTableError { + error: format!("duplicate type parameter '{name}'"), + location, + }); + } match role { SymbolUsage::Global if !symbol.is_global() => { - if flags.contains(SymbolFlags::PARAMETER) { + if flags.contains(SymbolFlags::DEF_PARAM) { return Err(SymbolTableError { error: format!("name '{name}' is parameter and global"), location, }); } - if flags.contains(SymbolFlags::REFERENCED) { + if flags.contains(SymbolFlags::USE) { return Err(SymbolTableError { error: format!("name '{name}' is used prior to global declaration"), location, }); } - if flags.contains(SymbolFlags::ANNOTATED) { + if flags.contains(SymbolFlags::DEF_ANNOT) { return Err(SymbolTableError { error: format!("annotated name '{name}' can't be global"), location, }); } - if flags.contains(SymbolFlags::ASSIGNED) { + if flags.contains(SymbolFlags::DEF_LOCAL) { return Err(SymbolTableError { error: format!( "name '{name}' is assigned to before global declaration" @@ -2939,25 +3213,25 @@ impl SymbolTableBuilder { } } SymbolUsage::Nonlocal => { - if flags.contains(SymbolFlags::PARAMETER) { + if flags.contains(SymbolFlags::DEF_PARAM) { return Err(SymbolTableError { error: format!("name '{name}' is parameter and nonlocal"), location, }); } - if flags.contains(SymbolFlags::REFERENCED) { + if flags.contains(SymbolFlags::USE) { return Err(SymbolTableError { error: format!("name '{name}' is used prior to nonlocal declaration"), location, }); } - if flags.contains(SymbolFlags::ANNOTATED) { + if flags.contains(SymbolFlags::DEF_ANNOT) { return Err(SymbolTableError { error: format!("annotated name '{name}' can't be nonlocal"), location, }); } - if flags.contains(SymbolFlags::ASSIGNED) { + if flags.contains(SymbolFlags::DEF_LOCAL) { return Err(SymbolTableError { error: format!( "name '{name}' is assigned to before nonlocal declaration" @@ -2966,6 +3240,21 @@ impl SymbolTableBuilder { }); } } + SymbolUsage::AnnotationAssigned + if current_scope != CompilerScope::Module + && flags + .intersects(SymbolFlags::DEF_GLOBAL | SymbolFlags::DEF_NONLOCAL) => + { + let usage = if flags.contains(SymbolFlags::DEF_GLOBAL) { + "global" + } else { + "nonlocal" + }; + return Err(SymbolTableError { + error: format!("annotated name '{name}' can't be {usage}"), + location, + }); + } _ => { // Ok? } @@ -2990,18 +3279,22 @@ impl SymbolTableBuilder { table.symbols.entry(name.into_owned()).or_insert(symbol) }; + if matches!(role, SymbolUsage::Global | SymbolUsage::Nonlocal) { + symbol.location = location; + } + // Set proper scope and flags on symbol: let flags = &mut symbol.flags; match role { SymbolUsage::Nonlocal => { symbol.scope = SymbolScope::Free; - flags.insert(SymbolFlags::NONLOCAL); + flags.insert(SymbolFlags::DEF_NONLOCAL); } SymbolUsage::Imported => { - flags.insert(SymbolFlags::ASSIGNED | SymbolFlags::IMPORTED); + flags.insert(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_IMPORT); } SymbolUsage::Parameter => { - flags.insert(SymbolFlags::PARAMETER); + flags.insert(SymbolFlags::DEF_PARAM); // Parameters are always added to varnames first let name_str = symbol.name.clone(); if !self.current_varnames.contains(&name_str) { @@ -3009,7 +3302,7 @@ impl SymbolTableBuilder { } } SymbolUsage::AnnotationParameter => { - flags.insert(SymbolFlags::PARAMETER | SymbolFlags::ANNOTATED); + flags.insert(SymbolFlags::DEF_PARAM | SymbolFlags::DEF_ANNOT); // Annotated parameters are also added to varnames let name_str = symbol.name.clone(); if !self.current_varnames.contains(&name_str) { @@ -3017,61 +3310,41 @@ impl SymbolTableBuilder { } } SymbolUsage::AnnotationAssigned => { - flags.insert(SymbolFlags::ASSIGNED | SymbolFlags::ANNOTATED); + flags.insert(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_ANNOT); } SymbolUsage::Assigned => { - flags.insert(SymbolFlags::ASSIGNED); - // Local variables (assigned) are added to varnames if they are local scope - // and not already in varnames - if symbol.scope == SymbolScope::Local { - let name_str = symbol.name.clone(); - if !self.current_varnames.contains(&name_str) { - self.current_varnames.push(name_str); - } - } - } - SymbolUsage::AssignedNamedExprInComprehension => { - flags.insert(SymbolFlags::ASSIGNED | SymbolFlags::ASSIGNED_IN_COMPREHENSION); - // Named expressions in comprehensions might also be locals - if symbol.scope == SymbolScope::Local { - let name_str = symbol.name.clone(); - if !self.current_varnames.contains(&name_str) { - self.current_varnames.push(name_str); - } - } + flags.insert(SymbolFlags::DEF_LOCAL); } SymbolUsage::Global => { symbol.scope = SymbolScope::GlobalExplicit; - flags.insert(SymbolFlags::GLOBAL); + flags.insert(SymbolFlags::DEF_GLOBAL); } SymbolUsage::Used => { - flags.insert(SymbolFlags::REFERENCED); + flags.insert(SymbolFlags::USE); } SymbolUsage::Iter => { - flags.insert(SymbolFlags::ITER); + flags.insert(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_COMP_ITER); } SymbolUsage::TypeParam => { - flags.insert(SymbolFlags::ASSIGNED | SymbolFlags::TYPE_PARAM); + flags.insert(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_TYPE_PARAM); } } - // and even more checking - // it is not allowed to assign to iterator variables (by named expressions) - if flags.contains(SymbolFlags::ITER) - && flags.contains(SymbolFlags::ASSIGNED_IN_COMPREHENSION) - { - return Err(SymbolTableError { - error: format!( - "assignment expression cannot rebind comprehension iteration variable '{}'", - symbol.name - ), - location, - }); - } Ok(()) } } +fn is_docstring_expr(expr: &ast::Expr) -> bool { + matches!( + expr, + ast::Expr::StringLiteral(_) + | ast::Expr::Constant(ast::ExprConstant { + value: ast::ConstantValue::Str(_), + .. + }) + ) +} + pub(crate) fn mangle_name<'a>(class_name: Option<&str>, name: &'a str) -> Cow<'a, str> { let class_name = match class_name { Some(n) => n, @@ -3110,7 +3383,27 @@ pub(crate) fn maybe_mangle_name<'a>( #[cfg(test)] mod tests { - use super::mangle_name; + use super::{CompilerScope, SymbolFlags, SymbolTable, mangle_name}; + use rustpython_compiler_core::SourceFileBuilder; + + fn scan_source(source: &str) -> SymbolTable { + scan_source_result(source).unwrap() + } + + fn scan_source_result(source: &str) -> Result { + let source_file = SourceFileBuilder::new("source_path", source).finish(); + let parsed = ruff_python_parser::parse( + source_file.source_text(), + ruff_python_parser::Mode::Module.into(), + ) + .unwrap() + .into_syntax(); + let module = match parsed { + ruff_python_ast::Mod::Module(module) => module, + _ => unreachable!(), + }; + SymbolTable::scan_program(&module, source_file) + } #[test] fn mangle_name_leaves_private_name_in_underscore_only_class() { @@ -3124,4 +3417,470 @@ mod tests { assert_eq!(mangle_name(Some("_a"), "__a"), "_a__a"); assert_eq!(mangle_name(Some("__a"), "__a"), "_a__a"); } + + #[test] + fn duplicate_parameter_check_uses_mangled_name_like_cpython() { + let err = scan_source_result("class C:\n def f(__x, _C__x):\n pass\n") + .expect_err("expected duplicate argument after class-private mangling"); + + assert_eq!( + err.error, + "duplicate argument '_C__x' in function definition" + ); + } + + #[test] + fn super_name_marks_class_use_in_lambda_scope_like_cpython() { + let table = scan_source("def f():\n return lambda: super()\n"); + let function = table + .sub_tables + .iter() + .find(|table| table.name == "f") + .expect("missing function scope"); + let lambda = function + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::Lambda) + .expect("missing lambda scope"); + + assert!( + lambda.lookup("__class__").is_some(), + "CPython symtable Name_kind treats super as a __class__ use in any function-like scope" + ); + } + + #[test] + fn comprehension_iteration_target_sets_comp_iter_flag_like_cpython() { + let table = scan_source("result = [i for i in xs]\n"); + let comprehension = table + .inlined_comprehension_blocks + .iter() + .find(|table| table.typ == CompilerScope::Comprehension) + .expect("missing comprehension scope"); + let symbol = comprehension + .lookup("i") + .expect("missing comprehension iteration target"); + + assert!( + symbol.flags.contains(SymbolFlags::DEF_COMP_ITER), + "CPython symtable_add_def_helper sets DEF_COMP_ITER on comprehension iteration targets" + ); + } + + #[test] + fn inlined_comprehension_children_are_spliced_like_cpython() { + let table = scan_source("result = [(lambda: i) for i in xs]\n"); + + assert!( + !table + .sub_tables + .iter() + .any(|table| table.typ == CompilerScope::Comprehension), + "CPython removes inlined comprehension entries from ste_children" + ); + assert!( + table + .sub_tables + .iter() + .any(|table| table.typ == CompilerScope::Lambda), + "CPython splices children of inlined comprehensions into the parent children list" + ); + + let comprehension = table + .inlined_comprehension_blocks + .iter() + .find(|table| table.typ == CompilerScope::Comprehension) + .expect("missing inlined comprehension block"); + assert!( + comprehension.comp_inlined, + "CPython keeps the comprehension entry addressable through st_blocks with ste_comp_inlined set" + ); + } + + #[test] + fn future_annotations_annassign_still_scans_annotation_symbols_like_cpython() { + let table = scan_source("from __future__ import annotations\nx: T\n"); + let annotation_block = table + .annotation_block + .as_ref() + .expect("CPython still creates an AnnotationBlock for future annotations"); + + assert!( + annotation_block.lookup("T").is_some(), + "CPython symtable_visit_annotation still visits the annotation expression with future annotations" + ); + } + + #[test] + fn annotation_like_format_parameter_is_marked_used_like_cpython() { + let table = scan_source("def f(x: T): pass\n"); + let annotation_block = table + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::Annotation) + .expect("missing function annotation block"); + let format = annotation_block + .lookup(".format") + .expect("missing annotation .format parameter"); + assert!( + format + .flags + .contains(SymbolFlags::DEF_PARAM | SymbolFlags::USE), + "CPython symtable_enter_block() adds both DEF_PARAM and USE for annotation-like .format" + ); + + let table = scan_source("type A = T\n"); + let alias = table + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::TypeAlias) + .expect("missing type alias scope"); + let format = alias + .lookup(".format") + .expect("missing type alias .format parameter"); + assert!( + format + .flags + .contains(SymbolFlags::DEF_PARAM | SymbolFlags::USE), + "CPython TypeAliasBlock .format has DEF_PARAM | USE" + ); + + let table = scan_source("def f[T: B](): pass\n"); + let type_params = table + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::TypeParams) + .expect("missing type params scope"); + let type_variable = type_params + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::TypeVariable) + .expect("missing type variable scope"); + let format = type_variable + .lookup(".format") + .expect("missing type variable .format parameter"); + assert!( + format + .flags + .contains(SymbolFlags::DEF_PARAM | SymbolFlags::USE), + "CPython TypeVariableBlock .format has DEF_PARAM | USE" + ); + } + + #[test] + fn function_signature_annotation_block_is_sibling_like_cpython() { + let table = scan_source("def f(x: T): pass\n"); + assert_eq!(table.sub_tables[0].typ, CompilerScope::Annotation); + assert!(table.sub_tables[0].annotations_used); + assert_eq!(table.sub_tables[1].typ, CompilerScope::Function); + assert!( + table.sub_tables[1].annotation_block.is_none(), + "CPython stores the function signature AnnotationBlock as a child keyed by arguments, not on the function block" + ); + + let table = scan_source("def f(x): pass\n"); + assert_eq!(table.sub_tables[0].typ, CompilerScope::Annotation); + assert!(!table.sub_tables[0].annotations_used); + assert_eq!(table.sub_tables[1].typ, CompilerScope::Function); + } + + #[test] + fn future_function_signature_annotation_block_is_hidden_like_cpython() { + let table = scan_source("from __future__ import annotations\ndef f(x: T): pass\n"); + assert_eq!(table.sub_tables[0].typ, CompilerScope::Function); + assert_eq!( + table.hidden_annotation_blocks[0].typ, + CompilerScope::Annotation + ); + assert!(table.hidden_annotation_blocks[0].annotations_used); + assert!( + table.sub_tables[0].annotation_block.is_none(), + "CPython future AnnotationBlock stays in st_blocks and is not attached to the FunctionBlock" + ); + + let table = scan_source("from __future__ import annotations\ndef f(x): pass\n"); + assert_eq!(table.sub_tables[0].typ, CompilerScope::Function); + assert_eq!( + table.hidden_annotation_blocks[0].typ, + CompilerScope::Annotation + ); + assert!(!table.hidden_annotation_blocks[0].annotations_used); + } + + #[test] + fn annassign_marks_current_scope_annotations_used_like_cpython() { + let table = scan_source("x: int\n"); + assert!( + table.annotations_used, + "CPython AnnAssign_kind sets ste_annotations_used on the current scope" + ); + + let table = scan_source("class C:\n x: int\n"); + let class = table + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::Class) + .expect("missing class scope"); + assert!( + class.annotations_used, + "CPython AnnAssign_kind sets ste_annotations_used on class scopes" + ); + + let table = scan_source("def f():\n x: int\n"); + let function = table + .sub_tables + .iter() + .find(|table| table.typ == CompilerScope::Function) + .expect("missing function scope"); + assert!( + function.annotations_used, + "CPython AnnAssign_kind also marks function-local annotations" + ); + } + + #[test] + fn class_base_child_scope_precedes_class_scope_like_cpython() { + let table = scan_source("class C((lambda: Base)()):\n pass\n"); + assert_eq!(table.sub_tables[0].typ, CompilerScope::Lambda); + assert_eq!(table.sub_tables[1].typ, CompilerScope::Class); + } + + #[test] + fn try_handler_child_scope_precedes_else_scope_like_cpython() { + let table = scan_source( + "\ +def f(x): + try: + pass + except Exception: + y = 1 + def h(): + return y + else: + def e(): + return x +", + ); + let function = table + .sub_tables + .iter() + .find(|table| table.name == "f") + .expect("missing function scope"); + + let function_child_names = function + .sub_tables + .iter() + .filter(|table| table.typ == CompilerScope::Function) + .map(|table| table.name.as_str()) + .collect::>(); + assert_eq!(function_child_names, vec!["h", "e"]); + } + + #[test] + fn function_default_child_scope_precedes_decorator_scope_like_cpython() { + let table = scan_source( + "\ +@(lambda decorator_arg: decorator_arg) +def f(x=(lambda: 1)()): + pass +", + ); + let lambdas = table + .sub_tables + .iter() + .filter(|table| table.typ == CompilerScope::Lambda) + .collect::>(); + + assert_eq!(lambdas.len(), 2); + assert!( + lambdas[0].varnames.is_empty(), + "CPython symtable visits function defaults before decorators" + ); + assert_eq!(lambdas[1].varnames, vec!["decorator_arg"]); + } + + #[test] + fn future_annotations_still_rejects_named_expr_in_annotation_like_cpython() { + let err = + scan_source_result("from __future__ import annotations\nx: (y := int)\n").unwrap_err(); + + assert_eq!( + err.error, + "named expression cannot be used within an annotation" + ); + } + + #[test] + fn import_star_outside_module_uses_cpython_symtable_message() { + let err = scan_source_result("def f():\n from m import *\n").unwrap_err(); + + assert_eq!(err.error, "import * only allowed at module level"); + } + + #[test] + fn import_as_error_location_uses_alias_location_like_cpython() { + let source = "import module as __debug__\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!(err.error, "cannot assign to __debug__"); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 1); + assert_eq!( + location.character_offset.get(), + 8, + "CPython reports LOCATION(a) for import aliases, at the imported name" + ); + } + + #[test] + fn function_def_error_location_uses_statement_location_like_cpython() { + let source = "def __debug__():\n pass\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!(err.error, "cannot assign to __debug__"); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 1); + assert_eq!( + location.character_offset.get(), + 1, + "CPython reports LOCATION(s) for FunctionDef, at 'def'" + ); + } + + #[test] + fn global_after_assign_error_location_uses_statement_location_like_cpython() { + let source = "def f():\n x = 1\n global x\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!( + err.error, + "name 'x' is assigned to before global declaration" + ); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 3); + assert_eq!( + location.character_offset.get(), + 5, + "CPython reports LOCATION(s) for global directives, at 'global'" + ); + } + + #[test] + fn type_param_debug_name_is_checked_like_cpython_add_def_ctx() { + let source = "class C[__debug__]:\n pass\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!(err.error, "cannot assign to __debug__"); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 1); + assert_eq!( + location.character_offset.get(), + 9, + "CPython symtable_add_def_ctx checks DEF_TYPE_PARAM | DEF_LOCAL at LOCATION(tp)" + ); + } + + #[test] + fn except_handler_name_error_location_uses_handler_location_like_cpython() { + let source = "try:\n pass\nexcept Exception as __debug__:\n pass\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!(err.error, "cannot assign to __debug__"); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 3); + assert_eq!( + location.character_offset.get(), + 1, + "CPython reports LOCATION(eh) for except-handler names, at 'except'" + ); + } + + #[test] + fn match_star_capture_error_location_uses_pattern_location_like_cpython() { + let source = "match subject:\n case [*__debug__]:\n pass\n"; + let err = scan_source_result(source).unwrap_err(); + + assert_eq!(err.error, "cannot assign to __debug__"); + let location = err.location.unwrap(); + assert_eq!(location.line.get(), 2); + assert_eq!( + location.character_offset.get(), + 11, + "CPython reports LOCATION(p) for MatchStar, at the '*'" + ); + } + + #[test] + fn named_expr_in_lambda_inside_comprehension_iter_is_rejected_like_cpython() { + let err = scan_source_result("[x for x in (lambda: (y := 1))()]\n").unwrap_err(); + + assert_eq!( + err.error, + "assignment expression cannot be used in a comprehension iterable expression" + ); + } + + #[test] + fn yield_in_lambda_inside_comprehension_body_is_not_comprehension_yield_like_cpython() { + scan_source_result("[(lambda: (yield x)) for x in xs]\n").expect( + "CPython checks ste_comprehension on the current lambda block, not the enclosing comprehension", + ); + } + + #[test] + fn yield_in_comprehension_scans_value_before_comprehension_error_like_cpython() { + let err = scan_source_result("[(yield (x := 1)) for x in xs]\n").unwrap_err(); + + assert_eq!( + err.error, + "assignment expression cannot rebind comprehension iteration variable 'x'" + ); + } + + #[test] + fn named_expr_in_function_annotation_comprehension_is_allowed_like_cpython() { + scan_source_result("def f(x: [(y := int) for _ in xs]): pass\n").expect( + "CPython skips AnnotationBlock while extending namedexpr scope from a comprehension", + ); + } + + #[test] + fn named_expr_in_class_annotation_comprehension_uses_cpython_message() { + let err = scan_source_result("class C:\n x: [(y := int) for _ in xs]\n").unwrap_err(); + + assert_eq!( + err.error, + "assignment expression within a comprehension cannot be used in a class body" + ); + } + + #[test] + fn named_expr_in_type_alias_comprehension_uses_cpython_message() { + let err = scan_source_result("type A = [(y := int) for _ in xs]\n").unwrap_err(); + + assert_eq!( + err.error, + "assignment expression within a comprehension cannot be used in a type alias" + ); + } + + #[test] + fn named_expr_in_type_parameters_block_uses_cpython_message() { + let err = scan_source_result("class C[T]((base := object)): pass\n").unwrap_err(); + + assert_eq!( + err.error, + "named expression cannot be used within the definition of a generic" + ); + } + + #[test] + fn named_expr_in_typevar_bound_comprehension_uses_cpython_message() { + let err = scan_source_result("def f[T: [(y := int) for _ in xs]](): pass\n").unwrap_err(); + + assert_eq!( + err.error, + "assignment expression within a comprehension cannot be used in a TypeVar bound" + ); + } } diff --git a/crates/codegen/src/unparse.rs b/crates/codegen/src/unparse.rs index d7f754e2f9d..679560642e5 100644 --- a/crates/codegen/src/unparse.rs +++ b/crates/codegen/src/unparse.rs @@ -58,6 +58,63 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { self.f.write_fmt(f) } + fn unparse_float(&mut self, value: f64) -> fmt::Result { + #[allow(clippy::correctness, clippy::assertions_on_constants)] + const { + assert!(f64::MAX_10_EXP == 308) + }; + + if value.is_infinite() { + self.p("1e309") + } else { + self.p(&rustpython_literal::float::to_string(value)) + } + } + + fn unparse_complex(&mut self, real: f64, imag: f64) -> fmt::Result { + self.p(&rustpython_literal::complex::to_string(real, imag).replace("inf", "1e309")) + } + + fn unparse_constant_value(&mut self, value: &ast::ConstantValue) -> fmt::Result { + match value { + ast::ConstantValue::None => self.p("None"), + ast::ConstantValue::Boolean(value) => self.p(if *value { "True" } else { "False" }), + ast::ConstantValue::Str(value) => UnicodeEscape::new_repr(value.as_ref().into()) + .str_repr() + .fmt(self.f), + ast::ConstantValue::Bytes(value) => AsciiEscape::new_repr(value.as_ref()) + .bytes_repr() + .fmt(self.f), + ast::ConstantValue::Integer(value) => self.p(value.as_ref()), + ast::ConstantValue::Tuple(elements) => { + self.p("(")?; + let mut first = true; + for element in elements { + self.p_delim(&mut first, ", ")?; + self.unparse_constant_value(element)?; + } + self.p_if(elements.len() == 1, ",")?; + self.p(")") + } + ast::ConstantValue::Frozenset(elements) => { + if elements.is_empty() { + self.p("frozenset()") + } else { + self.p("frozenset({")?; + let mut first = true; + for element in elements { + self.p_delim(&mut first, ", ")?; + self.unparse_constant_value(element)?; + } + self.p("})") + } + } + ast::ConstantValue::Float(value) => self.unparse_float(*value), + ast::ConstantValue::Complex { real, imag } => self.unparse_complex(*real, *imag), + ast::ConstantValue::Ellipsis => self.p("..."), + } + } + fn unparse_expr(&mut self, ast: &ast::Expr, level: u8) -> fmt::Result { macro_rules! op_prec { ($op_ty:ident, $x:expr, $enu:path, $($var:ident($op:literal, $prec:ident)),*$(,)?) => { @@ -87,6 +144,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { values, node_index: _, range: _range, + .. }) => { let (op, prec) = op_prec!(bin, op, ast::BoolOp, And("and", AND), Or("or", OR)); group_if!(prec, { @@ -102,6 +160,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { value, node_index: _, range: _range, + .. }) => { group_if!(precedence::TUPLE, { self.unparse_expr(target, precedence::ATOM)?; @@ -115,6 +174,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { right, node_index: _, range: _range, + .. }) => { let right_associative = matches!(op, ast::Operator::Pow); let (op, prec) = op_prec!( @@ -146,6 +206,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { operand, node_index: _, range: _range, + .. }) => { let (op, prec) = op_prec!( un, @@ -166,6 +227,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { body, node_index: _, range: _range, + .. }) => { group_if!(precedence::TEST, { if let Some(parameters) = parameters { @@ -183,6 +245,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { orelse, node_index: _, range: _range, + .. }) => { group_if!(precedence::TEST, { self.unparse_expr(body, precedence::TEST + 1)?; @@ -196,6 +259,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { items, node_index: _, range: _range, + .. }) => { self.p("{")?; let mut first = true; @@ -214,6 +278,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { elts, node_index: _, range: _range, + .. }) => { self.p("{")?; let mut first = true; @@ -228,6 +293,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { generators, node_index: _, range: _range, + .. }) => { self.p("[")?; self.unparse_expr(elt, precedence::TEST)?; @@ -239,6 +305,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { generators, node_index: _, range: _range, + .. }) => { self.p("{")?; self.unparse_expr(elt, precedence::TEST)?; @@ -251,6 +318,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { generators, node_index: _, range: _range, + .. }) => { self.p("{")?; self.unparse_expr(key, precedence::TEST)?; @@ -265,6 +333,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { generators, node_index: _, range: _range, + .. }) => { self.p("(")?; self.unparse_expr(elt, precedence::TEST)?; @@ -275,6 +344,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { value, node_index: _, range: _range, + .. }) => { group_if!(precedence::AWAIT, { self.p("await ")?; @@ -285,6 +355,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { value, node_index: _, range: _range, + .. }) => { if let Some(value) = value { write!(self, "(yield {})", UnparseExpr::new(value, self.source))?; @@ -296,6 +367,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { value, node_index: _, range: _range, + .. }) => { write!( self, @@ -309,6 +381,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { comparators, node_index: _, range: _range, + .. }) => { group_if!(precedence::CMP, { let new_lvl = precedence::CMP + 1; @@ -326,6 +399,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { arguments: ast::Arguments { args, keywords, .. }, node_index: _, range: _range, + .. }) => { self.unparse_expr(func, precedence::ATOM)?; self.p("(")?; @@ -379,26 +453,13 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { .bytes_repr() .fmt(self.f)? } - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => { - #[allow(clippy::correctness, clippy::assertions_on_constants)] - const { - assert!(f64::MAX_10_EXP == 308) - }; - - let inf_str = "1e309"; - match value { - ast::Number::Int(int) => int.fmt(self.f)?, - &ast::Number::Float(fp) => { - if fp.is_infinite() { - self.p(inf_str)? - } else { - self.p(&rustpython_literal::float::to_string(fp))? - } - } - &ast::Number::Complex { real, imag } => self - .p(&rustpython_literal::complex::to_string(real, imag) - .replace("inf", inf_str))?, - } + ast::Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { + ast::Number::Int(int) => int.fmt(self.f)?, + &ast::Number::Float(fp) => self.unparse_float(fp)?, + &ast::Number::Complex { real, imag } => self.unparse_complex(real, imag)?, + }, + ast::Expr::Constant(ast::ExprConstant { value, .. }) => { + self.unparse_constant_value(value)? } ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { value, .. }) => { self.p(if *value { "True" } else { "False" })? @@ -460,6 +521,7 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { step, node_index: _, range: _range, + .. }) => { if let Some(lower) = lower { self.unparse_expr(lower, precedence::TEST)?; @@ -554,7 +616,9 @@ impl<'a, 'b, 'c> Unparser<'a, 'b, 'c> { let buffered = fmt::from_fn(|f| Unparser::new(f, self.source).unparse_expr(val, precedence::TEST + 1)) .to_string(); - if let Some(ast::DebugText { leading, trailing }) = debug_text { + if let Some(debug_text) = debug_text { + let leading = debug_text.leading.as_str(); + let trailing = debug_text.trailing.as_str(); self.p(leading)?; self.p(self.source.slice(val.range()))?; self.p(trailing)?; diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 725be665f73..4498e74ca49 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -16,6 +16,7 @@ wasm_js = ["getrandom/wasm_js"] [dependencies] rustpython-literal = { workspace = true } +rustpython-unicode = { workspace = true } rustpython-wtf8 = { workspace = true } ascii = { workspace = true } @@ -28,7 +29,6 @@ malachite-q = { workspace = true } malachite-base = { workspace = true } num-traits = { workspace = true } parking_lot = { workspace = true, optional = true } -unicode_names2 = { workspace = true } radium = { workspace = true } lock_api = { workspace = true } diff --git a/crates/common/src/borrow.rs b/crates/common/src/borrow.rs index 2be5f8275c8..70d755ff155 100644 --- a/crates/common/src/borrow.rs +++ b/crates/common/src/borrow.rs @@ -34,6 +34,17 @@ impl_from!('a, T, BorrowedValue<'a, T>, ); impl<'a, T: ?Sized> BorrowedValue<'a, T> { + /// Whether reaching the value holds a lock that other threads wait on. + /// + /// An immutable object hands out a plain reference and answers `false`; + /// one whose storage can change hands out a guard. A caller about to wait + /// for something unrelated -- a peer, a file, a signal -- can use this to + /// decide whether it may keep the borrow for the duration. + #[must_use] + pub const fn is_locked(&self) -> bool { + !matches!(self, Self::Ref(_)) + } + pub fn map(s: Self, f: F) -> BorrowedValue<'a, U> where F: FnOnce(&T) -> &U, diff --git a/crates/common/src/encodings.rs b/crates/common/src/encodings.rs index 913f0521e16..b9ce02b88cb 100644 --- a/crates/common/src/encodings.rs +++ b/crates/common/src/encodings.rs @@ -414,7 +414,7 @@ pub mod errors { let mut out = String::with_capacity(num_chars * 4); for c in err_str.code_points() { let c_u32 = c.to_u32(); - if let Some(c_name) = c.to_char().and_then(unicode_names2::name) { + if let Some(c_name) = c.to_char().and_then(rustpython_unicode::character_name) { write!(out, "\\N{{{c_name}}}").unwrap(); } else if c_u32 >= 0x10000 { write!(out, "\\U{c_u32:08x}").unwrap(); diff --git a/crates/common/src/float_ops.rs b/crates/common/src/float_ops.rs index fed080dcec8..4f117e80c46 100644 --- a/crates/common/src/float_ops.rs +++ b/crates/common/src/float_ops.rs @@ -4,14 +4,22 @@ use num_traits::{Signed, ToPrimitive}; #[must_use] pub const fn decompose_float(value: f64) -> (f64, i32) { - if 0.0 == value { - (0.0, 0i32) - } else { - let bits = value.to_bits(); - let exponent: i32 = ((bits >> 52) & 0x7ff) as i32 - 1022; - let mantissa_bits = bits & (0x000f_ffff_ffff_ffff) | (1022 << 52); - (f64::from_bits(mantissa_bits), exponent) + if value == 0.0 { + return (0.0, 0); } + let bits = value.to_bits(); + // Subnormals carry a biased exponent of 0 and no implicit leading mantissa + // bit, so the normal decomposition below would misread them. Scale them up + // into the normal range first (exact, since it is a power-of-two shift) and + // fold the scale back into the returned exponent. + let (bits, exponent_adjust) = if (bits >> 52) & 0x7ff == 0 { + ((value * (1u64 << 54) as f64).to_bits(), -54) + } else { + (bits, 0) + }; + let exponent: i32 = ((bits >> 52) & 0x7ff) as i32 - 1022 + exponent_adjust; + let mantissa_bits = bits & (0x000f_ffff_ffff_ffff) | (1022 << 52); + (f64::from_bits(mantissa_bits), exponent) } /// Equate an integer to a float. @@ -270,3 +278,527 @@ pub fn round_float_digits(x: f64, ndigits: i32) -> Option { } Some(result) } + +/// Error from [`from_hex`], mapping to the exception the caller should raise. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HexFloatError { + /// ValueError "invalid hexadecimal floating-point string" + Invalid, + /// ValueError "hexadecimal string too long to convert" + TooLong, + /// OverflowError "hexadecimal value too large to represent as a float" + Overflow, +} + +const DBL_MANT_DIG: i64 = 53; +const DBL_MIN_EXP: i64 = -1021; +const DBL_MAX_EXP: i64 = 1024; + +/// Read byte at `i`, returning `None` past the end so that digit/sign/space +/// scans stop at the string boundary. +#[inline] +fn byte_at(bytes: &[u8], i: usize) -> Option { + bytes.get(i).copied() +} + +/// '0'-'9' -> 0..9, 'a'-'f'/'A'-'F' -> 10..15, else `None`. +#[inline] +const fn hex_from_char(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, + } +} + +/// Peek at byte `i` and decode it as a hex digit, or `None` if it is out of +/// range or not a hex digit. +#[inline] +fn hex_digit_at(bytes: &[u8], i: usize) -> Option { + byte_at(bytes, i).and_then(hex_from_char) +} + +/// `t` must be an ASCII-lowercase literal. Returns true if every byte of `t` +/// matched case-insensitively starting at `s`. +fn case_insensitive_match(bytes: &[u8], s: usize, t: &[u8]) -> bool { + let mut si = s; + let mut ti = 0; + while ti < t.len() && byte_at(bytes, si).is_some_and(|b| b.to_ascii_lowercase() == t[ti]) { + si += 1; + ti += 1; + } + ti == t.len() +} + +/// Returns `Some((value, endptr))` when the text at `p` parses as inf/nan, +/// otherwise `None`. +fn parse_inf_or_nan(bytes: &[u8], p: usize) -> Option<(f64, usize)> { + let mut s = p; + let mut negate = false; + if byte_at(bytes, s) == Some(b'-') { + negate = true; + s += 1; + } else if byte_at(bytes, s) == Some(b'+') { + s += 1; + } + if case_insensitive_match(bytes, s, b"inf") { + s += 3; + if case_insensitive_match(bytes, s, b"inity") { + s += 5; + } + let value = if negate { + f64::NEG_INFINITY + } else { + f64::INFINITY + }; + Some((value, s)) + } else if case_insensitive_match(bytes, s, b"nan") { + s += 3; + let value = if negate { + f64::from_bits(0xfff8_0000_0000_0000) + } else { + f64::from_bits(0x7ff8_0000_0000_0000) + }; + Some((value, s)) + } else { + None + } +} + +/// Correctly-rounded scalbn. Every call site scales an already-representable +/// value, so the result is exact. +const fn ldexp(x: f64, mut n: i32) -> f64 { + let x1p1023 = f64::from_bits(0x7fe0000000000000); + let x1p53 = f64::from_bits(0x4340000000000000); + let x1p_1022 = f64::from_bits(0x0010000000000000); + let mut y = x; + if n > 1023 { + y *= x1p1023; + n -= 1023; + if n > 1023 { + y *= x1p1023; + n -= 1023; + if n > 1023 { + n = 1023; + } + } + } else if n < -1022 { + y *= x1p_1022 * x1p53; + n += 1022 - 53; + if n < -1022 { + y *= x1p_1022 * x1p53; + n += 1022 - 53; + if n < -1022 { + n = -1022; + } + } + } + y * f64::from_bits(((0x3ff + n) as u64) << 52) +} + +/// Parse the already-validated `[+-]?[0-9]+` slice `bytes[start..end]` as base-10 +/// signed, saturating to i64::MIN/MAX on overflow like strtol. +fn strtol_saturating(bytes: &[u8], start: usize, end: usize) -> i64 { + let mut i = start; + let mut neg = false; + if i < end && (bytes[i] == b'+' || bytes[i] == b'-') { + neg = bytes[i] == b'-'; + i += 1; + } + let mut val: i64 = 0; + let mut overflowed = false; + while i < end { + let d = (bytes[i] - b'0') as i64; + match val.checked_mul(10).and_then(|v| v.checked_add(d)) { + Some(v) => val = v, + None => { + overflowed = true; + break; + } + } + i += 1; + } + if overflowed { + if neg { i64::MIN } else { i64::MAX } + } else if neg { + -val + } else { + val + } +} + +/// Parse a hexadecimal floating-point string (the `float.fromhex` grammar). +/// +/// The raw string is consumed as-is: leading and trailing whitespace are handled +/// internally using the ASCII space set, so callers must not trim first. +pub fn from_hex(s: &str) -> Result { + let bytes = s.as_bytes(); + let s_end = bytes.len(); + + let mut negate = false; + let mut idx = 0usize; + let mut x; + + // leading whitespace + while byte_at(bytes, idx).is_some_and(rustpython_wtf8::is_py_ascii_whitespace) { + idx += 1; + } + + // infinities and nans + if let Some((value, end)) = parse_inf_or_nan(bytes, idx) { + idx = end; + return finish_hex(bytes, s_end, idx, negate, value); + } + + // optional sign + if byte_at(bytes, idx) == Some(b'-') { + idx += 1; + negate = true; + } else if byte_at(bytes, idx) == Some(b'+') { + idx += 1; + } + + // [0x] + let s_store = idx; + if byte_at(bytes, idx) == Some(b'0') { + idx += 1; + if matches!(byte_at(bytes, idx), Some(b'x' | b'X')) { + idx += 1; + } else { + idx = s_store; + } + } + + // coefficient: [. ] + let coeff_start = idx; + while hex_digit_at(bytes, idx).is_some() { + idx += 1; + } + let s_store = idx; + let coeff_end = if byte_at(bytes, idx) == Some(b'.') { + idx += 1; + while hex_digit_at(bytes, idx).is_some() { + idx += 1; + } + idx - 1 + } else { + idx + }; + + // ndigits = total # of hex digits; fdigits = # after point + let ndigits_total = (coeff_end - coeff_start) as i64; + let fdigits = (coeff_end - s_store) as i64; + if ndigits_total == 0 { + return Err(HexFloatError::Invalid); + } + let insane_bound = core::cmp::min( + DBL_MIN_EXP - DBL_MANT_DIG - i64::MIN / 2, + i64::MAX / 2 + 1 - DBL_MAX_EXP, + ) / 4; + if ndigits_total > insane_bound { + return Err(HexFloatError::TooLong); + } + + // [p ] + let exp = if matches!(byte_at(bytes, idx), Some(b'p' | b'P')) { + idx += 1; + let exp_start = idx; + if matches!(byte_at(bytes, idx), Some(b'-' | b'+')) { + idx += 1; + } + if !matches!(byte_at(bytes, idx), Some(b'0'..=b'9')) { + return Err(HexFloatError::Invalid); + } + idx += 1; + while matches!(byte_at(bytes, idx), Some(b'0'..=b'9')) { + idx += 1; + } + strtol_saturating(bytes, exp_start, idx) + } else { + 0 + }; + + // HEX_DIGIT(j): jth hex digit counting from the least significant. + let hex_digit = |j: i64| -> i32 { + let byte_idx = if j < fdigits { + coeff_end as i64 - j + } else { + coeff_end as i64 - 1 - j + }; + hex_digit_at(bytes, byte_idx as usize).expect("hex digit within coefficient") as i32 + }; + + // Discard leading zeros, and catch extreme overflow and underflow. + let mut ndigits = ndigits_total; + while ndigits > 0 && hex_digit(ndigits - 1) == 0 { + ndigits -= 1; + } + if ndigits == 0 || exp < i64::MIN / 2 { + x = 0.0; + return finish_hex(bytes, s_end, idx, negate, x); + } + if exp > i64::MAX / 2 { + return Err(HexFloatError::Overflow); + } + + // Adjust exponent for fractional part. + let exp = exp - 4 * fdigits; + + // top_exp = 1 more than exponent of most significant bit of coefficient. + let mut top_exp = exp + 4 * (ndigits - 1); + let mut digit = hex_digit(ndigits - 1); + while digit != 0 { + top_exp += 1; + digit /= 2; + } + + // catch almost all nonextreme cases of overflow and underflow here + if top_exp < DBL_MIN_EXP - DBL_MANT_DIG { + x = 0.0; + return finish_hex(bytes, s_end, idx, negate, x); + } + if top_exp > DBL_MAX_EXP { + return Err(HexFloatError::Overflow); + } + + // lsb = exponent of least significant bit of the rounded value. + let lsb = core::cmp::max(top_exp, DBL_MIN_EXP) - DBL_MANT_DIG; + + x = 0.0; + if exp >= lsb { + // no rounding required + let mut i = ndigits - 1; + while i >= 0 { + x = 16.0 * x + hex_digit(i) as f64; + i -= 1; + } + x = ldexp(x, exp as i32); + return finish_hex(bytes, s_end, idx, negate, x); + } + + // rounding required. key_digit is the index of the hex digit + // containing the first bit to be rounded away. + let half_eps: i32 = 1 << ((lsb - exp - 1) % 4) as i32; + let key_digit = (lsb - exp - 1) / 4; + let mut i = ndigits - 1; + while i > key_digit { + x = 16.0 * x + hex_digit(i) as f64; + i -= 1; + } + let digit = hex_digit(key_digit); + x = 16.0 * x + (digit & (16 - 2 * half_eps)) as f64; + + // round-half-even + if (digit & half_eps) != 0 { + let round_up = if (digit & (3 * half_eps - 1)) != 0 + || (half_eps == 8 && key_digit + 1 < ndigits && (hex_digit(key_digit + 1) & 1) != 0) + { + true + } else { + let mut r = false; + let mut i = key_digit - 1; + while i >= 0 { + if hex_digit(i) != 0 { + r = true; + break; + } + i -= 1; + } + r + }; + if round_up { + x += (2 * half_eps) as f64; + if top_exp == DBL_MAX_EXP && x == ldexp((2 * half_eps) as f64, DBL_MANT_DIG as i32) { + // overflow corner case + return Err(HexFloatError::Overflow); + } + } + } + x = ldexp(x, (exp + 4 * key_digit) as i32); + + finish_hex(bytes, s_end, idx, negate, x) +} + +/// Skip trailing whitespace, require the whole string was consumed, and apply +/// the sign. +fn finish_hex( + bytes: &[u8], + s_end: usize, + mut idx: usize, + negate: bool, + x: f64, +) -> Result { + while byte_at(bytes, idx).is_some_and(rustpython_wtf8::is_py_ascii_whitespace) { + idx += 1; + } + if idx != s_end { + return Err(HexFloatError::Invalid); + } + Ok(if negate { -x } else { x }) +} + +#[cfg(test)] +mod from_hex_tests { + use super::{HexFloatError, from_hex}; + + fn bits(s: &str) -> u64 { + from_hex(s).unwrap().to_bits() + } + + #[test] + fn from_hex_exact_bits() { + assert_eq!(bits("0x1p-1074"), 0x0000000000000001); + assert_eq!(bits("0x1.fffffffffffffp+1023"), 0x7fefffffffffffff); + // round-half-even ties + assert_eq!(bits("0x1.00000000000008p0"), 0x3ff0000000000000); + assert_eq!(bits("0x1.00000000000018p0"), 0x3ff0000000000002); + assert_eq!(bits("-0x1p0"), 0xbff0000000000000); + assert_eq!(bits("0x0p0"), 0x0000000000000000); + assert_eq!(bits("-0x0p0"), 0x8000000000000000); + } + + #[test] + fn from_hex_inf_nan() { + assert_eq!(bits("inf"), 0x7ff0000000000000); + assert_eq!(bits("-inf"), 0xfff0000000000000); + assert_eq!(bits("Infinity"), 0x7ff0000000000000); + + let n = from_hex("nan").unwrap(); + assert!(n.is_nan()); + assert_eq!(n.to_bits(), 0x7ff8000000000000); + let neg = from_hex("-nan").unwrap(); + assert!(neg.is_nan()); + assert_eq!(neg.to_bits(), 0xfff8000000000000); + } + + #[test] + fn from_hex_whitespace() { + assert_eq!(bits(" 0x1p0 "), 0x3ff0000000000000); + assert_eq!(bits("\t0x1p0\n"), 0x3ff0000000000000); + } + + #[test] + fn from_hex_errors() { + assert_eq!(from_hex("0x1p1024"), Err(HexFloatError::Overflow)); + assert_eq!(from_hex("0x1z"), Err(HexFloatError::Invalid)); + assert_eq!(from_hex(""), Err(HexFloatError::Invalid)); + assert_eq!(from_hex("0x1 p0"), Err(HexFloatError::Invalid)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::hash_float; + + /// Exact `2**e` for `e` in `[-1074, 1023]`, built from bits so extreme + /// exponents don't overflow through an intermediate `2**|e|`. + fn pow2(e: i32) -> f64 { + if e >= -1022 { + f64::from_bits(((e + 1023) as u64) << 52) + } else { + f64::from_bits(1u64 << (e + 1074)) + } + } + + /// `decompose_float` is a frexp returning the *magnitude* mantissa: for a + /// nonzero `value`, `m` lies in `[0.5, 1)` and `m * 2**e == value.abs()`, + /// including for subnormals which have no implicit leading mantissa bit. + /// (Its sole caller reintroduces the sign via `value.signum()`.) + #[test] + fn decompose_float_frexp_contract() { + let mut values = alloc::vec![ + 0.0, + f64::from_bits(1), // smallest subnormal + f64::from_bits(2), + f64::from_bits(0x000f_ffff_ffff_ffff), // largest subnormal + f64::MIN_POSITIVE, // DBL_MIN, smallest normal + f64::from_bits(f64::MIN_POSITIVE.to_bits() - 1), // predecessor + 1.0, + 1.5, + 0.1, + core::f64::consts::PI, + ]; + for e in -1074..=1023 { + values.push(pow2(e)); + values.push(-pow2(e)); + } + for &v in &values { + let (m, e) = decompose_float(v); + if v == 0.0 { + assert_eq!((m, e), (0.0, 0)); + continue; + } + assert!( + (0.5..1.0).contains(&m), + "mantissa {m} out of [0.5, 1) for value {v:e}" + ); + // Reconstruct: m * 2**e must round-trip to the magnitude. Fold one + // power of two into the mantissa so `e` stays within `pow2`'s range + // (frexp yields e up to 1024 for 2**1023). + let reconstructed = (m * 2.0) * pow2(e - 1); + assert_eq!( + reconstructed.to_bits(), + v.abs().to_bits(), + "reconstruction failed for {v:e}: m={m}, e={e}" + ); + } + } + + /// Subnormal frexp regression: hash of the smallest positive subnormal. + #[test] + fn hash_float_smallest_subnormal() { + // hash(5e-324) == 16777216 (CPython 3.14 ground truth). The pre-fix + // bit-twiddling frexp returned 8404992 here. + assert_eq!(hash_float(f64::from_bits(1)), Some(16777216)); + } + + /// Differential float-hash table captured from CPython 3.14.5, spanning + /// subnormal boundaries, powers of two across the whole exponent range, and + /// a spread of normals. + #[test] + fn hash_float_matches_cpython() { + const HASH_CASES: &[(u64, i64)] = &[ + (0x0000000000000001, 16777216), // smallest subnormal 5e-324 + (0x0000000000000002, 33554432), // subnormal + (0x00000000deadbeef, 62678480394911744), // subnormal midrange + (0x0008000000000000, 16384), // subnormal high bit + (0x000fffffffffffff, 2305843009196949503), // largest subnormal + (0x0010000000000000, 32768), // DBL_MIN smallest normal + (0x8000000000000001, -16777216), // negative smallest subnormal + (0x0020000000000000, 65536), // 2**-1021 + (0x0170000000000000, 137438953472), // 2**-1000 + (0x39b0000000000000, 4194304), // 2**-100 + (0x3f50000000000000, 2251799813685248), // 2**-10 + (0x3fe0000000000000, 1152921504606846976), // 2**-1 + (0x3ff0000000000000, 1), // 2**0 + (0x4000000000000000, 2), // 2**1 + (0x4090000000000000, 1024), // 2**10 + (0x4630000000000000, 549755813888), // 2**100 + (0x7e70000000000000, 16777216), // 2**1000 + (0x7fe0000000000000, 140737488355328), // 2**1023 + (0xffe0000000000000, -140737488355328), // -2**1023 + (0x3ff8000000000000, 1152921504606846977), // 1.5 + (0x400921fb54442d18, 326490430436040707), // 3.141592653589793 + (0x7e37e43c8800759c, 1224995262755759164), // 1e+300 + (0x01a56e1fc2f8f359, 482449582752280463), // 1e-300 + (0x40c81cd6c8b43958, 1563361560246628409), // 12345.678 + (0x3fb999999999999a, 230584300921369408), // 0.1 + (0x4005666666666666, 1556444031219243010), // 2.675 + (0x4132d68700000000, 1234567), // 1234567.0 + (0x44dfe154f457ea13, 1428027733287631914), // 6.022e+23 + (0x3c07a42f549647fb, 851769299698974080), // 1.602e-19 + (0xbff0000000000000, -2), // -1.0 + (0xbfb999999999999a, -230584300921369408), // -0.1 + ]; + for &(bits, expected) in HASH_CASES { + let v = f64::from_bits(bits); + assert_eq!( + hash_float(v), + Some(expected), + "hash mismatch for {v:e} (bits {bits:#018x})" + ); + } + } +} diff --git a/crates/common/src/format.rs b/crates/common/src/format.rs index ea459886914..1c5c0a9c9de 100644 --- a/crates/common/src/format.rs +++ b/crates/common/src/format.rs @@ -1,4 +1,4 @@ -// spell-checker:ignore ddfe +// spell-checker:ignore ddfe DTSF use core::ops::Deref; use core::{cmp, str::FromStr}; use itertools::{Itertools, PeekingNext}; @@ -141,8 +141,8 @@ impl FormatParse for FormatGrouping { } } -impl From<&FormatGrouping> for char { - fn from(fg: &FormatGrouping) -> Self { +impl From for char { + fn from(fg: FormatGrouping) -> Self { match fg { FormatGrouping::Comma => ',', FormatGrouping::Underscore => '_', @@ -150,6 +150,12 @@ impl From<&FormatGrouping> for char { } } +impl From<&FormatGrouping> for char { + fn from(fg: &FormatGrouping) -> Self { + Self::from(*fg) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FormatType { String, @@ -221,11 +227,14 @@ pub struct FormatSpec { conversion: Option, fill: Option, align: Option, + align_specified: bool, sign: Option, + no_neg_0: bool, alternate_form: bool, width: Option, grouping_option: Option, precision: Option, + frac_grouping_option: Option, format_type: Option, } @@ -277,6 +286,14 @@ fn parse_alternate_form(text: &Wtf8) -> (bool, &Wtf8) { } } +fn parse_no_negative_zero(text: &Wtf8) -> (bool, &Wtf8) { + let mut chars = text.code_points(); + match chars.next().and_then(CodePoint::to_char) { + Some('z') => (true, chars.as_wtf8()), + _ => (false, text), + } +} + fn parse_zero(text: &Wtf8) -> (bool, &Wtf8) { let mut chars = text.code_points(); match chars.next().and_then(CodePoint::to_char) { @@ -285,22 +302,49 @@ fn parse_zero(text: &Wtf8) -> (bool, &Wtf8) { } } -fn parse_precision(text: &Wtf8) -> Result<(Option, &Wtf8), FormatSpecError> { +fn parse_char(text: &Wtf8, expected: char) -> (bool, &Wtf8) { let mut chars = text.code_points(); - Ok(match chars.next().and_then(CodePoint::to_char) { - Some('.') => { - let (size, remaining) = parse_number(chars.as_wtf8())?; - if let Some(size) = size { - if size > i32::MAX as usize { - return Err(FormatSpecError::PrecisionTooBig); - } - (Some(size), remaining) - } else { - (None, text) - } + if chars.next().and_then(CodePoint::to_char) == Some(expected) { + (true, chars.as_wtf8()) + } else { + (false, text) + } +} + +fn parse_precision( + text: &Wtf8, +) -> Result<(Option, Option, &Wtf8), FormatSpecError> { + let (dot, text) = parse_char(text, '.'); + if !dot { + return Ok((None, None, text)); + } + let (precision, text) = parse_number(text)?; + if let Some(precision) = precision + && precision > i32::MAX as usize + { + return Err(FormatSpecError::PrecisionTooBig); + } + let mut frac_grouping = None; + let (comma, text) = parse_char(text, ','); + if comma { + frac_grouping = Some(FormatGrouping::Comma); + } + let (underscore, text) = parse_char(text, '_'); + if underscore { + if frac_grouping.is_some() { + return Err(FormatSpecError::ExclusiveFormat(',', '_')); } - _ => (None, text), - }) + frac_grouping = Some(FormatGrouping::Underscore); + } + let (trailing_comma, _) = parse_char(text, ','); + if trailing_comma && frac_grouping == Some(FormatGrouping::Underscore) { + return Err(FormatSpecError::ExclusiveFormat(',', '_')); + } + // Not having a precision or underscore/comma after a dot is an error. + if precision.is_none() && frac_grouping.is_none() { + return Err(FormatSpecError::PrecisionMissing); + } + Ok((precision, frac_grouping, text)) } impl FormatSpec { @@ -312,7 +356,9 @@ impl FormatSpec { // get_integer in CPython let (conversion, text) = FormatConversion::parse(text); let (mut fill, mut align, text) = parse_fill_and_align(text); + let align_specified = align.is_some(); let (sign, text) = FormatSign::parse(text); + let (no_neg_0, text) = parse_no_negative_zero(text); let (alternate_form, text) = parse_alternate_form(text); let (zero, text) = parse_zero(text); let (width, text) = parse_number(text)?; @@ -322,10 +368,10 @@ impl FormatSpec { return Err(FormatSpecError::DecimalDigitsTooMany); } let (grouping_option, text) = FormatGrouping::parse(text); - if let Some(grouping) = &grouping_option { + if let Some(grouping) = grouping_option { Self::validate_separator(grouping, text)?; } - let (precision, text) = parse_precision(text)?; + let (precision, frac_grouping_option, text) = parse_precision(text)?; let (format_type, text) = FormatType::parse(text); if !text.is_empty() { return Err(FormatSpecError::InvalidFormatSpecifier); @@ -340,20 +386,24 @@ impl FormatSpec { conversion, fill, align, + align_specified, sign, + no_neg_0, alternate_form, width, grouping_option, precision, + frac_grouping_option, format_type, }) } - fn validate_separator(grouping: &FormatGrouping, text: &Wtf8) -> Result<(), FormatSpecError> { + fn validate_separator(grouping: FormatGrouping, text: &Wtf8) -> Result<(), FormatSpecError> { let mut chars = text.code_points().peekable(); + let grouping_char = char::from(grouping); match chars.peek().and_then(|cp| CodePoint::to_char(*cp)) { Some(c) if c == ',' || c == '_' => { - if c == char::from(grouping) { + if c == grouping_char { Err(FormatSpecError::UnspecifiedFormat(c, c)) } else { Err(FormatSpecError::ExclusiveFormat(',', '_')) @@ -373,15 +423,31 @@ impl FormatSpec { sep: char, disp_digit_cnt: i32, ) -> String { - // Don't add separators to the floating decimal point of numbers - let mut parts = magnitude_str.splitn(2, '.'); - let magnitude_int_str = parts.next().unwrap().to_string(); + // Group only the leading integer digits; the trailing remainder must + // never receive separators. For decimal and float output (interval 3) + // that remainder is the decimal point and fraction, an exponent + // (`e+NN`), or a trailing percent sign. Hex/octal/binary output + // (interval 4) has no such tail and its `a`-`f`/`e`/`E` are digits, so + // the whole magnitude is groupable. + let int_len = if inter == 4 { + magnitude_str.len() + } else { + magnitude_str + .bytes() + .position(|b| !b.is_ascii_digit()) + .unwrap_or(magnitude_str.len()) + }; + // No leading integer digits (e.g. "inf"/"nan") means nothing to group; + // leave any width padding to the fill/align step. + if int_len == 0 { + return magnitude_str; + } + let magnitude_int_str = magnitude_str[..int_len].to_string(); + let remainder = &magnitude_str[int_len..]; let dec_digit_cnt = magnitude_str.len() as i32 - magnitude_int_str.len() as i32; let int_digit_cnt = disp_digit_cnt - dec_digit_cnt; let mut result = Self::separate_integer(magnitude_int_str, inter, sep, int_digit_cnt); - if let Some(part) = parts.next() { - result.push_str(&format!(".{part}")) - } + result.push_str(remainder); result } @@ -440,6 +506,40 @@ impl FormatSpec { Err(FormatSpecError::UnspecifiedFormat('_', ch)) } _ => Ok(()), + }?; + if let Some(grouping) = self.frac_grouping_option + && matches!(format_type, FormatType::Number(_)) + { + let ch = char::from(format_type); + return Err(FormatSpecError::UnspecifiedFormat(char::from(grouping), ch)); + } + Ok(()) + } + + fn formatted_magnitude_is_zero(magnitude: &str) -> bool { + let mut saw_digit = false; + for byte in magnitude.bytes() { + if byte.is_ascii_digit() { + saw_digit = true; + if byte != b'0' { + return false; + } + } + } + saw_digit + } + + fn is_negative_after_zero_coercion(&self, num: f64, magnitude: &str) -> bool { + num.is_sign_negative() + && !num.is_nan() + && !(self.no_neg_0 && Self::formatted_magnitude_is_zero(magnitude)) + } + + fn validate_complex_padding_and_alignment(&self) -> Result<(), FormatSpecError> { + match &self.fill.unwrap_or_else(|| ' '.into()).to_char() { + Some('0') => Err(FormatSpecError::ZeroPadding), + _ if self.align == Some(FormatAlign::AfterSign) => Err(FormatSpecError::AlignmentFlag), + _ => Ok(()), } } @@ -468,7 +568,9 @@ impl FormatSpec { let disp_digit_cnt = if self.fill == Some('0'.into()) && self.align == Some(FormatAlign::AfterSign) { - let width = self.width.unwrap_or(magnitude_len) as i32 - prefix.len() as i32; + let width = self.width.unwrap_or(magnitude_len) as i32 + - prefix.len() as i32 + - self.frac_separator_count(&magnitude_str) as i32; cmp::max(width, magnitude_len as i32) } else { magnitude_len as i32 @@ -479,6 +581,41 @@ impl FormatSpec { } } + fn frac_digit_span(&self, magnitude_str: &str) -> Option<(FormatGrouping, usize, usize)> { + let grouping = self.frac_grouping_option?; + let start = magnitude_str.find('.')? + 1; + let end = magnitude_str[start..] + .bytes() + .position(|b| !b.is_ascii_digit()) + .map_or(magnitude_str.len(), |offset| start + offset); + (start < end).then_some((grouping, start, end)) + } + + fn frac_separator_count(&self, magnitude_str: &str) -> usize { + match self.frac_digit_span(magnitude_str) { + Some((_, start, end)) => (end - start - 1) / self.get_separator_interval(), + None => 0, + } + } + + fn add_frac_separators(&self, magnitude_str: String) -> String { + let Some((grouping, start, end)) = self.frac_digit_span(&magnitude_str) else { + return magnitude_str; + }; + let inter = self.get_separator_interval(); + let sep = char::from(grouping); + let mut result = magnitude_str[..start].to_string(); + let mut frac = &magnitude_str[start..end]; + while frac.len() > inter { + result.push_str(&frac[..inter]); + result.push(sep); + frac = &frac[inter..]; + } + result.push_str(frac); + result.push_str(&magnitude_str[end..]); + result + } + /// Returns true if this format spec uses the locale-aware 'n' format type. #[must_use] pub fn has_locale_format(&self) -> bool { @@ -567,6 +704,9 @@ impl FormatSpec { Some(FormatType::Number(Case::Lower)) => self.format_int_radix(magnitude, 10), _ => return self.format_int(num), }?; + if self.no_neg_0 { + return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")); + } let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale); @@ -616,7 +756,7 @@ impl FormatSpec { let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale); let format_sign = self.sign.unwrap_or(FormatSign::Minus); - let sign_str = if num.is_sign_negative() && !num.is_nan() { + let sign_str = if self.is_negative_after_zero_coercion(num, &magnitude_str) { "-" } else { match format_sign { @@ -641,6 +781,7 @@ impl FormatSpec { num: &Complex64, locale: &LocaleInfo, ) -> Result { + self.validate_format(FormatType::FixedPoint(Case::Lower))?; // Reuse format_complex_re_im with 'g' type to get the base formatted parts, // then apply locale grouping. This matches CPython's format_complex_internal: // 'n' → 'g', add_parens=0, skip_re=0. @@ -684,6 +825,7 @@ impl FormatSpec { // No parentheses for 'n' format (CPython: add_parens=0) let magnitude_str = format!("{grouped_re}{grouped_im}"); + self.validate_complex_padding_and_alignment()?; Ok(self.format_sign_and_align(&AsciiStr::new(&magnitude_str), "", FormatAlign::Right)) } @@ -703,11 +845,16 @@ impl FormatSpec { self.format_float(x as f64) } None => { + if self.no_neg_0 { + return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")); + } let first_letter = (input.to_string().as_bytes()[0] as char).to_uppercase(); Ok(first_letter.collect::() + &input.to_string()[1..]) } - Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(*c, "int")), - _ => Err(FormatSpecError::InvalidFormatSpecifier), + Some(format_type) => { + let ch = char::from(format_type); + Err(FormatSpecError::UnknownFormatCode(ch, "bool")) + } } } @@ -779,19 +926,46 @@ impl FormatSpec { magnitude if magnitude.is_nan() => Ok("nan".to_owned()), magnitude if magnitude.is_infinite() => Ok("inf".to_owned()), _ => match self.precision { - Some(precision) => Ok(float::format_general( - precision, - magnitude, - Case::Lower, - self.alternate_form, - true, - )), - None => Ok(float::to_string(magnitude)), + Some(precision) => { + // Empty presentation type with a precision behaves like + // `g` but repr-like: precision is clamped to at least 1, + // and an integer-looking result keeps a trailing `.0` + // (Py_DTSF_ADD_DOT_0). + let precision = if precision == 0 { 1 } else { precision }; + let s = float::format_general( + precision, + magnitude, + Case::Lower, + self.alternate_form, + true, + ); + Ok(if s.bytes().any(|b| matches!(b, b'.' | b'e' | b'E')) { + s + } else { + format!("{s}.0") + }) + } + None => { + let s = float::to_string(magnitude); + // Alternate form forces a decimal point into the + // repr-like output. Only exponent-form values lack one + // (`1e+16` -> `1.e+16`); fixed-form repr already carries + // a `.`. + Ok(if self.alternate_form && !s.contains('.') { + match s.find(['e', 'E']) { + Some(pos) => format!("{}.{}", &s[..pos], &s[pos..]), + None => format!("{s}."), + } + } else { + s + }) + } }, }, }; + let raw_magnitude_str = raw_magnitude_str?; let format_sign = self.sign.unwrap_or(FormatSign::Minus); - let sign_str = if num.is_sign_negative() && !num.is_nan() { + let sign_str = if self.is_negative_after_zero_coercion(num, &raw_magnitude_str) { "-" } else { match format_sign { @@ -800,7 +974,8 @@ impl FormatSpec { FormatSign::MinusOrSpace => " ", } }; - let magnitude_str = self.add_magnitude_separators(raw_magnitude_str?, sign_str); + let magnitude_str = self.add_magnitude_separators(raw_magnitude_str, sign_str); + let magnitude_str = self.add_frac_separators(magnitude_str); Ok( self.format_sign_and_align( &AsciiStr::new(&magnitude_str), @@ -850,14 +1025,24 @@ impl FormatSpec { Err(FormatSpecError::UnknownFormatCode('N', "int")) } Some(FormatType::String) => Err(FormatSpecError::UnknownFormatCode('s', "int")), - Some(FormatType::Character) => match (self.sign, self.alternate_form) { - (Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")), - (_, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")), - (_, _) => match num.to_u32() { - Some(n) if n <= 0x10ffff => Ok(core::char::from_u32(n).unwrap().to_string()), - Some(_) | None => Err(FormatSpecError::CodeNotInRange), - }, - }, + Some(FormatType::Character) => { + if self.precision.is_some() { + Err(FormatSpecError::PrecisionNotAllowed) + } else if self.no_neg_0 { + Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")) + } else { + match (self.sign, self.alternate_form) { + (Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")), + (_, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")), + _ => match num.to_u32() { + Some(n) if n <= 0x10ffff => { + Ok(core::char::from_u32(n).unwrap().to_string()) + } + Some(_) | None => Err(FormatSpecError::CodeNotInRange), + }, + } + } + } Some( FormatType::GeneralFormat(_) | FormatType::FixedPoint(_) @@ -870,6 +1055,9 @@ impl FormatSpec { Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(c, "int")), None => self.format_int_radix(magnitude, 10), }?; + if self.no_neg_0 { + return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")); + } let format_sign = self.sign.unwrap_or(FormatSign::Minus); let sign_str = match num.sign() { Sign::Minus => "-", @@ -895,13 +1083,27 @@ impl FormatSpec { self.validate_format(FormatType::String)?; match self.format_type { Some(FormatType::String) | None => { + if self.no_neg_0 { + return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string")); + } + if self.align == Some(FormatAlign::AfterSign) && self.align_specified { + return Err(FormatSpecError::StringAlignmentFlag); + } // CPython parity: precision truncates BEFORE width pads. // `'{:3.2s}'.format('abc')` -> 'ab ' (truncate to 'ab', pad to 3). let truncated: String = match self.precision { Some(p) => s.deref().chars().take(p).collect(), None => s.deref().to_owned(), }; - Ok(self.format_sign_and_align(&truncated, "", FormatAlign::Left)) + let spec = Self { + align: if self.align == Some(FormatAlign::AfterSign) { + Some(FormatAlign::Left) + } else { + self.align + }, + ..*self + }; + Ok(spec.format_sign_and_align(&truncated, "", FormatAlign::Left)) } _ => { let ch = char::from(self.format_type.as_ref().unwrap()); @@ -918,24 +1120,16 @@ impl FormatSpec { } else { format!("{formatted_re}{formatted_im}") }; - if let Some(FormatAlign::AfterSign) = &self.align { - return Err(FormatSpecError::AlignmentFlag); - } - match &self.fill.unwrap_or_else(|| ' '.into()).to_char() { - Some('0') => Err(FormatSpecError::ZeroPadding), - _ => Ok(self.format_sign_and_align( - &AsciiStr::new(&magnitude_str), - "", - FormatAlign::Right, - )), - } + self.validate_complex_padding_and_alignment()?; + Ok(self.format_sign_and_align(&AsciiStr::new(&magnitude_str), "", FormatAlign::Right)) } fn format_complex_re_im(&self, num: &Complex64) -> Result<(String, String), FormatSpecError> { // Format real part let formatted_re = if num.re != 0.0 || num.re.is_negative_zero() || self.format_type.is_some() { - let sign_re = if num.re.is_sign_negative() && !num.is_nan() { + let re = self.format_complex_float(num.re)?; + let sign_re = if self.is_negative_after_zero_coercion(num.re, &re) { "-" } else { match self.sign.unwrap_or(FormatSign::Minus) { @@ -944,21 +1138,24 @@ impl FormatSpec { FormatSign::MinusOrSpace => " ", } }; - let re = self.format_complex_float(num.re)?; format!("{sign_re}{re}") } else { String::new() }; // Format imaginary part - let sign_im = if num.im.is_sign_negative() && !num.im.is_nan() { + let im = self.format_complex_float(num.im)?; + let sign_im = if self.is_negative_after_zero_coercion(num.im, &im) { "-" } else if formatted_re.is_empty() { - "" + match self.sign.unwrap_or(FormatSign::Minus) { + FormatSign::Plus => "+", + FormatSign::Minus => "", + FormatSign::MinusOrSpace => " ", + } } else { "+" }; - let im = self.format_complex_float(num.im)?; Ok((formatted_re, format!("{sign_im}{im}j"))) } @@ -1024,17 +1221,16 @@ impl FormatSpec { }, }, }?; - match &self.grouping_option { + let magnitude_str = match &self.grouping_option { Some(fg) => { let sep = char::from(fg); let inter = self.get_separator_interval().try_into().unwrap(); let len = magnitude_str.len() as i32; - let separated_magnitude = - Self::add_magnitude_separators_for_char(magnitude_str, inter, sep, len); - Ok(separated_magnitude) + Self::add_magnitude_separators_for_char(magnitude_str, inter, sep, len) } - None => Ok(magnitude_str), - } + None => magnitude_str, + }; + Ok(self.add_frac_separators(magnitude_str)) } fn format_sign_and_align( @@ -1125,6 +1321,7 @@ impl Deref for AsciiStr<'_> { pub enum FormatSpecError { DecimalDigitsTooMany, PrecisionTooBig, + PrecisionMissing, InvalidFormatSpecifier, UnspecifiedFormat(char, char), ExclusiveFormat(char, char), @@ -1135,6 +1332,8 @@ pub enum FormatSpecError { CodeNotInRange, ZeroPadding, AlignmentFlag, + NegativeZeroCoercionNotAllowed(&'static str), + StringAlignmentFlag, NotImplemented(char, &'static str), } @@ -1240,8 +1439,7 @@ impl FieldName { FieldType::Index(index) } else if first .as_str() - .ok() - .is_some_and(|s| s.bytes().all(|b| b.is_ascii_digit())) + .is_ok_and(|s| s.bytes().all(|b| b.is_ascii_digit())) { // All-digit segment whose value overflows usize itself. return Err(FormatParseError::TooManyDecimalDigits); @@ -1484,11 +1682,14 @@ mod tests { conversion: None, fill: None, align: None, + align_specified: false, sign: None, + no_neg_0: false, alternate_form: false, width: Some(33), grouping_option: None, precision: None, + frac_grouping_option: None, format_type: None, }); assert_eq!(FormatSpec::parse("33"), expected); @@ -1500,11 +1701,14 @@ mod tests { conversion: None, fill: Some('<'.into()), align: Some(FormatAlign::Right), + align_specified: true, sign: None, + no_neg_0: false, alternate_form: false, width: Some(33), grouping_option: None, precision: None, + frac_grouping_option: None, format_type: None, }); assert_eq!(FormatSpec::parse("<>33"), expected); @@ -1516,11 +1720,14 @@ mod tests { conversion: None, fill: Some('<'.into()), align: Some(FormatAlign::Right), + align_specified: true, sign: Some(FormatSign::Minus), + no_neg_0: false, alternate_form: true, width: Some(23), grouping_option: Some(FormatGrouping::Comma), precision: Some(11), + frac_grouping_option: None, format_type: Some(FormatType::Binary), }); assert_eq!(FormatSpec::parse("<>-#23,.11b"), expected); @@ -1562,6 +1769,164 @@ mod tests { assert_eq!(format_bool("%", false), Ok("0.000000%".to_owned())); } + #[test] + fn format_string_zero_padding_uses_left_alignment() { + let spec = FormatSpec::parse("08s").unwrap(); + let value = "result".to_owned(); + + assert_eq!(spec.format_string(&value), Ok("result00".to_owned())); + } + + #[test] + fn format_string_explicit_after_sign_alignment_is_invalid() { + let spec = FormatSpec::parse("=8s").unwrap(); + let value = "result".to_owned(); + + assert_eq!( + spec.format_string(&value), + Err(FormatSpecError::StringAlignmentFlag) + ); + } + + #[test] + fn format_complex_rejects_zero_padding_before_after_sign_alignment() { + for text in [ + "08.1f", "=08.1f", "0=8.1f", "#08.1f", "0>8.1f", "0<8.1f", "0^8.1f", + ] { + let spec = FormatSpec::parse(text).unwrap(); + assert_eq!( + spec.format_complex(&Complex64::new(1.0, 2.0)), + Err(FormatSpecError::ZeroPadding), + "{text}" + ); + } + + let spec = FormatSpec::parse("=8.1f").unwrap(); + assert_eq!( + spec.format_complex(&Complex64::new(1.0, 2.0)), + Err(FormatSpecError::AlignmentFlag) + ); + } + + #[test] + fn format_int_zero_padding_stays_after_sign() { + let spec = FormatSpec::parse("08").unwrap(); + + assert_eq!( + spec.format_int(&BigInt::from(-42)), + Ok("-0000042".to_owned()) + ); + } + + #[test] + fn format_complex_locale_rejects_zero_padding_before_after_sign_alignment() { + let locale = LocaleInfo { + thousands_sep: String::new(), + decimal_point: ".".to_owned(), + grouping: vec![], + }; + for text in ["08n", "=08n", "0=8n", "#08n", "0>8n", "0<8n", "0^8n"] { + let spec = FormatSpec::parse(text).unwrap(); + assert_eq!( + spec.format_complex_locale(&Complex64::new(1.0, 2.0), &locale), + Err(FormatSpecError::ZeroPadding), + "{text}" + ); + } + + let spec = FormatSpec::parse("=8n").unwrap(); + assert_eq!( + spec.format_complex_locale(&Complex64::new(1.0, 2.0), &locale), + Err(FormatSpecError::AlignmentFlag) + ); + } + + #[test] + fn format_negative_zero_coercion() { + let int_spec = FormatSpec::parse("z8").unwrap(); + assert_eq!( + int_spec.format_int(&BigInt::from(-42)), + Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")) + ); + assert_eq!( + FormatSpec::parse("zs") + .unwrap() + .format_int(&BigInt::from(0)), + Err(FormatSpecError::UnknownFormatCode('s', "int")) + ); + assert_eq!( + FormatSpec::parse("z.1d") + .unwrap() + .format_int(&BigInt::from(0)), + Err(FormatSpecError::PrecisionNotAllowed) + ); + assert_eq!( + FormatSpec::parse("+zc") + .unwrap() + .format_int(&BigInt::from(0)), + Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer")) + ); + + let float_spec = FormatSpec::parse("z.2f").unwrap(); + assert_eq!(float_spec.format_float(-0.0001), Ok("0.00".to_owned())); + + let complex_spec = FormatSpec::parse("z").unwrap(); + assert_eq!( + complex_spec.format_complex(&Complex64::new(-0.0, -0.0)), + Ok("(0+0j)".to_owned()) + ); + let pure_imaginary = Complex64::new(0.0, -0.0); + assert_eq!( + FormatSpec::parse("+z") + .unwrap() + .format_complex(&pure_imaginary), + Ok("+0j".to_owned()) + ); + assert_eq!( + FormatSpec::parse(" z") + .unwrap() + .format_complex(&pure_imaginary), + Ok(" 0j".to_owned()) + ); + + let string_value = "value".to_owned(); + assert_eq!( + FormatSpec::parse("z").unwrap().format_string(&string_value), + Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string")) + ); + assert_eq!( + FormatSpec::parse("zd") + .unwrap() + .format_string(&string_value), + Err(FormatSpecError::UnknownFormatCode('d', "str")) + ); + assert_eq!( + FormatSpec::parse("zs").unwrap().format_bool(false), + Err(FormatSpecError::UnknownFormatCode('s', "bool")) + ); + + let locale = LocaleInfo { + thousands_sep: ",".to_owned(), + decimal_point: ".".to_owned(), + grouping: vec![3, 0], + }; + let locale_spec = FormatSpec::parse("zn").unwrap(); + assert_eq!( + locale_spec.format_float_locale(-0.0, &locale), + Ok("0".to_owned()) + ); + assert_eq!( + locale_spec.format_complex_locale(&Complex64::new(-0.0, -0.0), &locale), + Ok("0+0j".to_owned()) + ); + assert_eq!( + FormatSpec::parse("z.1n") + .unwrap() + .format_int_locale(&BigInt::from(0), &locale), + Err(FormatSpecError::PrecisionNotAllowed) + ); + } + #[test] fn format_int() { assert_eq!( @@ -1657,6 +2022,205 @@ mod tests { assert_eq!(result, "000001,234"); } + fn fmt_float(spec: &str, value: f64) -> String { + FormatSpec::parse(spec) + .unwrap() + .format_float(value) + .unwrap() + } + + #[test] + fn format_float_grouping_never_touches_exponent() { + // Grouping must group only the mantissa's integer digits, never the + // exponent digits (was "1e,+20") or a trailing percent sign. + assert_eq!(fmt_float(",g", 1e20), "1e+20"); + assert_eq!(fmt_float("_g", 1e-10), "1e-10"); + assert_eq!(fmt_float(",e", 1e20), "1.000000e+20"); + assert_eq!(fmt_float(",", 1e16), "1e+16"); + assert_eq!(fmt_float(",.0%", 1.0), "100%"); + assert_eq!(fmt_float(",.2%", 12345.0), "1,234,500.00%"); + // Fixed-form grouping still groups the integer part. + assert_eq!(fmt_float(",", 1234567.0), "1,234,567.0"); + } + + #[test] + fn format_float_grouping_inf_nan() { + // No integer digits to group; width padding is left to fill/align, so + // separators never land inside "inf"/"nan". + assert_eq!(fmt_float(",", f64::INFINITY), "inf"); + assert_eq!(fmt_float("06,", f64::INFINITY), "000inf"); + assert_eq!(fmt_float("06,", f64::NAN), "000nan"); + assert_eq!(fmt_float("06,%", f64::INFINITY), "00inf%"); + } + + #[test] + fn format_float_fractional_grouping() { + // Fraction digits group away from the decimal point, so the last group + // may be shorter than the interval. + assert_eq!(fmt_float(".6,f", 1234.56789), "1234.567,890"); + assert_eq!(fmt_float(".7,f", 1234.56789), "1234.567,890,0"); + assert_eq!(fmt_float(".4,f", 1.1), "1.100,0"); + assert_eq!(fmt_float(".3,f", 1.1), "1.100"); + assert_eq!(fmt_float(".6_f", 1234.56789), "1234.567_890"); + // Omitting the precision keeps the type's default. + assert_eq!(fmt_float(".,f", 1.1), "1.100,000"); + // The two parts are independent and may use different separators. + assert_eq!(fmt_float(",.6,f", 1234.56789), "1,234.567,890"); + assert_eq!(fmt_float(",.6_f", 1234.56789), "1,234.567_890"); + assert_eq!(fmt_float("_.6,f", 1234.56789), "1_234.567,890"); + } + + #[test] + fn format_float_fractional_grouping_never_touches_tail() { + // Only the digits between the point and any tail are groupable: the + // exponent and a trailing percent sign must stay intact. + assert_eq!(fmt_float(".6,e", 12345678900.0), "1.234,568e+10"); + assert_eq!(fmt_float(".6,E", 1234.5678), "1.234,568E+03"); + assert_eq!(fmt_float(".8,%", 1.2345e-05), "0.001,234,50%"); + // Values with no point have nothing to group. + assert_eq!(fmt_float(".6,f", f64::INFINITY), "inf"); + assert_eq!(fmt_float(".6,f", f64::NAN), "nan"); + assert_eq!(fmt_float(".0,f", 1234.56789), "1235"); + } + + #[test] + fn format_float_fractional_grouping_counts_toward_width() { + // Separators are inserted before padding, so they consume width. + assert_eq!(fmt_float("020.6,f", 1234.56789), "000000001234.567,890"); + assert_eq!(fmt_float("015.6,f", 1.5), "0000001.500,000"); + assert_eq!(fmt_float("<20.6,f", 1234.56789), "1234.567,890 "); + // Zero padding of the integer part must reserve room for them too. + assert_eq!(fmt_float("020,.6,f", 1234.56789), "0,000,001,234.567,890"); + assert_eq!(fmt_float("+020,.6_f", 1e-10), "+000,000,000.000_000"); + assert_eq!(fmt_float("= 015,.6,E", 1234.0), " 01.234,000E+03"); + assert_eq!(fmt_float("-015_._e", 1.1), "001.100_000e+00"); + } + + #[test] + fn format_parse_fractional_grouping_errors() { + // Mixing the two separators is rejected wherever it appears. + assert_eq!( + FormatSpec::parse(".,_f"), + Err(FormatSpecError::ExclusiveFormat(',', '_')) + ); + assert_eq!( + FormatSpec::parse("._,f"), + Err(FormatSpecError::ExclusiveFormat(',', '_')) + ); + // A repeated separator is left in the spec and rejected as a whole. + assert_eq!( + FormatSpec::parse(".,,f"), + Err(FormatSpecError::InvalidFormatSpecifier) + ); + assert_eq!( + FormatSpec::parse(".__f"), + Err(FormatSpecError::InvalidFormatSpecifier) + ); + // A dot needs either digits or a separator after it. + assert_eq!( + FormatSpec::parse("."), + Err(FormatSpecError::PrecisionMissing) + ); + assert_eq!( + FormatSpec::parse(".f"), + Err(FormatSpecError::PrecisionMissing) + ); + // 'n' draws its separators from the locale. + assert_eq!( + FormatSpec::parse(".6,n").unwrap().format_float(1234.5678), + Err(FormatSpecError::UnspecifiedFormat(',', 'n')) + ); + assert_eq!( + FormatSpec::parse("._n").unwrap().format_float(1234.5678), + Err(FormatSpecError::UnspecifiedFormat('_', 'n')) + ); + // The integer separator is reported first when both are present. + assert_eq!( + FormatSpec::parse("_.6,n").unwrap().format_float(1234.5678), + Err(FormatSpecError::UnspecifiedFormat('_', 'n')) + ); + // The complex locale path rewrites 'n' to 'g', so it must validate first. + let locale = LocaleInfo { + thousands_sep: ",".to_owned(), + decimal_point: ".".to_owned(), + grouping: vec![3, 0], + }; + assert_eq!( + FormatSpec::parse(".6,n") + .unwrap() + .format_complex_locale(&Complex64::new(1.0, 2.345678), &locale), + Err(FormatSpecError::UnspecifiedFormat(',', 'n')) + ); + } + + #[test] + fn format_float_empty_type_with_precision() { + // Empty presentation type with a precision is repr-like: precision is + // clamped to at least 1 and integer-looking output keeps a `.0`. + assert_eq!(fmt_float(".2", 1.0), "1.0"); + assert_eq!(fmt_float(".6", 100.0), "100.0"); + assert_eq!(fmt_float(".17", 1234567.0), "1234567.0"); + assert_eq!(fmt_float(".0", 0.5), "0.5"); + assert_eq!(fmt_float(".0", 0.0001), "0.0001"); + assert_eq!(fmt_float(".2", 0.0), "0.0"); + assert_eq!(fmt_float(".0", 0.0), "0e+00"); + assert_eq!(fmt_float(".2", 100.0), "1e+02"); + } + + #[test] + fn format_float_alternate_form_forces_point() { + // Alternate form injects a decimal point into exponent-form repr. + assert_eq!(fmt_float("#", 1e16), "1.e+16"); + assert_eq!(fmt_float("#", 1e-5), "1.e-05"); + // Fixed-form repr already has a point, so it is unchanged. + assert_eq!(fmt_float("#", 100.0), "100.0"); + assert_eq!(fmt_float("#", 1.5), "1.5"); + } + + #[test] + fn format_int_hex_grouping_preserved() { + // Underscore grouping of hex/octal groups every 4 digits, including the + // `a`-`f` letters. + assert_eq!( + FormatSpec::parse("_x") + .unwrap() + .format_int(&BigInt::from(1000000)) + .unwrap(), + "f_4240" + ); + assert_eq!( + FormatSpec::parse("_X") + .unwrap() + .format_int(&BigInt::from(0xABCDEFu32)) + .unwrap(), + "AB_CDEF" + ); + } + + #[test] + fn format_int_character_rejects_precision() { + // 'c' rejects precision, and precision is checked before sign/alt form. + assert_eq!( + FormatSpec::parse(".2c") + .unwrap() + .format_int(&BigInt::from(65)), + Err(FormatSpecError::PrecisionNotAllowed) + ); + assert_eq!( + FormatSpec::parse("+.2c") + .unwrap() + .format_int(&BigInt::from(65)), + Err(FormatSpecError::PrecisionNotAllowed) + ); + // Without precision, 'c' still renders the code point. + assert_eq!( + FormatSpec::parse("c") + .unwrap() + .format_int(&BigInt::from(65)), + Ok("A".to_owned()) + ); + } + #[test] fn format_parse() { let expected = Ok(FormatString { diff --git a/crates/common/src/hash.rs b/crates/common/src/hash.rs index 5b16c89e7bc..1c58abc20f7 100644 --- a/crates/common/src/hash.rs +++ b/crates/common/src/hash.rs @@ -49,9 +49,15 @@ impl HashSecret { let k1 = u64::from_le_bytes(right.try_into().unwrap()); Self { k0, k1 } } -} -impl HashSecret { + /// Build a secret from explicit SipHash keys, bypassing seed derivation. + /// Lets an embedder reproduce a fixed keying (e.g. a deterministic run) that + /// [`new`](Self::new) cannot express through its `u32` seed. + #[must_use] + pub const fn from_keys(k0: u64, k1: u64) -> Self { + Self { k0, k1 } + } + pub fn hash_value(&self, data: &T) -> PyHash { fix_sentinel(mod_int(self.hash_one(data) as _)) } @@ -94,7 +100,7 @@ pub const fn hash_pointer(value: usize) -> PyHash { #[inline] #[must_use] -pub fn hash_float(value: f64) -> Option { +pub const fn hash_float(value: f64) -> Option { // cpython _Py_HashDouble if !value.is_finite() { return if value.is_infinite() { @@ -111,6 +117,8 @@ pub fn hash_float(value: f64) -> Option { let mut m = frexp.0; let mut e = frexp.1; let mut x: PyUHash = 0; + + #[expect(clippy::while_float, reason = "keep this loop like CPython does it")] while m != 0.0 { x = ((x << 28) & MODULUS) | (x >> (BITS - 28)); m *= 268_435_456.0; // 2**28 @@ -137,13 +145,14 @@ pub fn hash_float(value: f64) -> Option { #[must_use] pub fn hash_bigint(value: &BigInt) -> PyHash { - let ret = match value.to_i64() { - Some(i) => mod_int(i), - None => (value % MODULUS).to_i64().unwrap_or_else(|| unsafe { - // SAFETY: MODULUS < i64::MAX, so value % MODULUS is guaranteed to be in the range of i64 - core::hint::unreachable_unchecked() - }), + let ret = if let Some(v) = value.to_i64() { + mod_int(v) + } else { + // SAFETY: + // MODULUS < i64::MAX, so value % MODULUS is guaranteed to be in the range of i64 + unsafe { (value % MODULUS).to_i64().unwrap_unchecked() } }; + fix_sentinel(ret) } @@ -197,3 +206,132 @@ pub fn keyed_hash(key: u64, buf: &[u8]) -> u64 { buf.hash(&mut hasher); hasher.finish() } + +/// tuplehash: fold the element hashes of a tuple (xxHash-based). +/// +/// The caller supplies each element's hash lazily; a hash computation may fail, +/// in which case the error short-circuits the fold. +pub fn hash_tuple( + element_hashes: impl IntoIterator>, +) -> Result { + const PRIME1: PyUHash = cfg_select! { + target_pointer_width = "64" => 11400714785074694791, + target_pointer_width = "32" => 2654435761, + _ => unreachable!(), + }; + + const PRIME2: PyUHash = cfg_select! { + target_pointer_width = "64" => 14029467366897019727, + target_pointer_width = "32" => 2246822519, + _ => unreachable!(), + }; + + const PRIME5: PyUHash = cfg_select! { + target_pointer_width = "64" => 2870177450012600261, + target_pointer_width = "32" => 374761393, + _ => unreachable!(), + }; + + const ROTATE: u32 = cfg_select! { + target_pointer_width = "64" => 31, + target_pointer_width = "32" => 13, + _ => unreachable!(), + }; + + let mut acc = PRIME5; + let mut len: PyUHash = 0; + + for element_hash in element_hashes { + let lane = element_hash? as PyUHash; + acc = acc.wrapping_add(lane.wrapping_mul(PRIME2)); + acc = acc.rotate_left(ROTATE); + acc = acc.wrapping_mul(PRIME1); + len += 1; + } + + acc = acc.wrapping_add(len ^ (PRIME5 ^ 3527539)); + + let acc_py_hash = acc as PyHash; + if acc_py_hash == -1 { + return Ok(1546275796); + } + + Ok(acc_py_hash) +} + +/// frozenset_hash: order-independent XOR-fold of a frozenset's element hashes. +/// +/// The entry hashes are fed in one at a time via [`FrozenSetHash::add`], so the +/// caller keeps ownership of the iteration (which may hold a lock and compute +/// each element hash fallibly). The fold is commutative, so element order does +/// not affect the result. +pub struct FrozenSetHash { + hash: u64, +} + +impl FrozenSetHash { + #[must_use] + pub fn new(len: usize) -> Self { + // Factor in the number of active entries + Self { + hash: (len as u64 + 1).wrapping_mul(1927868237), + } + } + + pub fn add(&mut self, element_hash: PyHash) { + // Work to increase the bit dispersion for closely spaced hash values. + // This is important because some use cases have many combinations of a + // small number of elements with nearby hashes so that many distinct + // combinations collapse to only a handful of distinct hash values. + const fn shuffle_bits(h: u64) -> u64 { + ((h ^ 89869747) ^ (h.wrapping_shl(16))).wrapping_mul(3644798167) + } + // Xor-in shuffled bits from every entry's hash field because xor is + // commutative and a frozenset hash should be independent of order. + self.hash ^= shuffle_bits(element_hash as u64); + } + + #[must_use] + pub fn finish(self) -> PyHash { + let mut hash = self.hash; + // Disperse patterns arising in nested frozen-sets + hash ^= (hash >> 11) ^ (hash >> 25); + hash = hash.wrapping_mul(69069).wrapping_add(907133923); + // -1 is reserved as an error code + if hash == u64::MAX { + hash = 590923713; + } + hash as PyHash + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_keys_is_stable_and_seed_independent() { + const K0: u64 = 0x0706_0504_0302_0100; + const K1: u64 = 0x0f0e_0d0c_0b0a_0908; + const LOCKED_DIGEST: PyHash = -1862661396243998188; + + // Two secrets built from the same explicit keys hash identically, and + // the digest does not depend on the seed-derivation path. + let a = HashSecret::from_keys(K0, K1); + let b = HashSecret::from_keys(K0, K1); + assert_eq!(a.hash_str("hello"), b.hash_str("hello")); + assert_eq!( + a.hash_bytes(b"a fixed message"), + b.hash_bytes(b"a fixed message") + ); + + // Explicit keys drive the SipHasher-2-4 directly. `keyed_hash` pins + // k1 = 0, so a secret built with the same k0 and k1 = 0 must reproduce + // its raw digest. + let zero_k1 = HashSecret::from_keys(K0, 0); + assert_eq!(keyed_hash(K0, b"payload"), zero_k1.hash_one(b"payload")); + + // Locked digest so an accidental keying change is caught. + assert_eq!(a.hash_str("determinism"), LOCKED_DIGEST); + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 53a8e0d752b..d1e04b46d57 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -21,6 +21,7 @@ pub mod rc; pub mod refcount; pub mod static_cell; pub mod str; +pub mod wtf8_index; pub use rustpython_wtf8 as wtf8; diff --git a/crates/common/src/refcount.rs b/crates/common/src/refcount.rs index c589ead40f6..4d52e1382e6 100644 --- a/crates/common/src/refcount.rs +++ b/crates/common/src/refcount.rs @@ -1,10 +1,14 @@ use crate::atomic::{Ordering, PyAtomic, Radium}; // State layout (usize): -// [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count] +// [1 bit: destructed] [1 bit: published] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count] // 64-bit: N=30, M=31. 32-bit: N=14, M=15. const FLAG_BITS: u32 = 3; const DESTRUCTED: usize = 1 << (usize::BITS - 1); +/// Object was published to a lock-free cache; memory reclamation is +/// deferred through QSBR so concurrent try-incref readers never touch +/// freed memory. Sticky once set. +const PUBLISHED: usize = 1 << (usize::BITS - 2); const LEAKED: usize = 1 << (usize::BITS - 3); const TOTAL_COUNT_WIDTH: u32 = usize::BITS - FLAG_BITS; const WEAK_WIDTH: u32 = TOTAL_COUNT_WIDTH / 2; @@ -72,8 +76,8 @@ impl State { /// Reference count using state layout with LEAKED support. /// /// State layout (usize): -/// 64-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [30 bits: weak_count] [31 bits: strong_count] -/// 32-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [14 bits: weak_count] [15 bits: strong_count] +/// 64-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [30 bits: weak_count] [31 bits: strong_count] +/// 32-bit: [1 bit: destructed] [1 bit: published] [1 bit: leaked] [14 bits: weak_count] [15 bits: strong_count] pub struct RefCount { state: PyAtomic, } @@ -187,6 +191,17 @@ impl RefCount { pub fn is_leaked(&self) -> bool { State::from_raw(self.state.load(Ordering::Acquire)).leaked() } + + /// Mark the object as published to a lock-free cache (sticky). + #[inline] + pub fn mark_published(&self) { + self.state.fetch_or(PUBLISHED, Ordering::Release); + } + + #[inline] + pub fn is_published(&self) -> bool { + (self.state.load(Ordering::Acquire) & PUBLISHED) != 0 + } } // Deferred Drop Infrastructure @@ -279,3 +294,24 @@ pub fn flush_deferred_drops() { } }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn published_bit_survives_refcount_traffic() { + let rc = RefCount::new(); // strong = 1 + assert!(!rc.is_published()); + rc.mark_published(); + assert!(rc.is_published()); + rc.inc(); // strong = 2 + assert!(rc.is_published()); + assert!(!rc.dec()); // strong = 1 + assert!(rc.is_published()); + assert!(rc.safe_inc()); // strong = 2 + assert!(!rc.dec()); // strong = 1 + assert!(rc.dec()); // strong = 0 -> true + assert!(rc.is_published()); + } +} diff --git a/crates/common/src/str.rs b/crates/common/src/str.rs index c006a5f4db4..39ec7da1de5 100644 --- a/crates/common/src/str.rs +++ b/crates/common/src/str.rs @@ -1,7 +1,8 @@ // spell-checker:ignore uncomputed -use crate::atomic::{PyAtomic, Radium}; +use crate::atomic::{OncePtr, PyAtomic, Radium}; use crate::format::CharLen; use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf}; +use crate::wtf8_index::Wtf8Index; use ascii::{AsciiChar, AsciiStr, AsciiString}; use core::fmt; use core::ops::{Bound, RangeBounds}; @@ -112,11 +113,70 @@ pub enum PyKindStr<'a> { Wtf8(&'a Wtf8), } +/// How far from an end an index is resolved by walking rather than by building +/// the code point index. +/// +/// PyPy spells this `MAX_UNROLL_NEXT_CODEPOINT_POS`, in a guard that also asks +/// the JIT whether the index is a constant, so that the walk unrolls. There is +/// no JIT here to ask, and the walk is short rather than free -- but four steps +/// still beat a pass over the whole buffer, and skipping the build is what +/// keeps `s[0]` and `s[1:-1]` on a long string from paying for a table. +const MAX_WALK_TO_INDEX: usize = 4; + #[derive(Debug, Clone)] pub struct StrData { data: Box, kind: StrKind, len: StrLen, + index: Wtf8IndexSlot, +} + +/// A [`Wtf8Index`] built on first use. +/// +/// The table is a pure function of `data`, so publishing it races benignly: a +/// thread that loses the exchange drops its own copy and reads the winner's. +#[derive(Default)] +struct Wtf8IndexSlot(OncePtr); + +impl Wtf8IndexSlot { + #[inline(always)] + fn new() -> Self { + Self(OncePtr::new()) + } + + #[inline] + fn get_or_build(&self, data: &Wtf8, char_len: usize) -> &Wtf8Index { + let index = self + .0 + .get_or_init(|| Box::new(Wtf8Index::new(data, char_len))); + // The slot owns the table, never replaces it, and outlives the borrow. + unsafe { index.as_ref() } + } +} + +impl fmt::Debug for Wtf8IndexSlot { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self.0.get() { + Some(_) => f.write_str(""), + None => f.write_str(""), + } + } +} + +impl Clone for Wtf8IndexSlot { + /// A fresh slot: the clone copies the buffer, so it has to index that copy, + /// and the table is rebuilt on demand rather than eagerly here. + fn clone(&self) -> Self { + Self::new() + } +} + +impl Drop for Wtf8IndexSlot { + fn drop(&mut self) { + if let Some(index) = self.0.get() { + drop(unsafe { Box::from_raw(index.as_ptr()) }); + } + } } struct StrLen(PyAtomic); @@ -163,6 +223,7 @@ impl Default for StrData { data: >::default(), kind: StrKind::Ascii, len: StrLen::zero(), + index: Wtf8IndexSlot::new(), } } } @@ -193,6 +254,7 @@ impl From> for StrData { len: value.len().into(), data: value.into(), kind: StrKind::Ascii, + index: Wtf8IndexSlot::new(), } } } @@ -212,6 +274,7 @@ impl From for StrData { data: ch.to_string().into(), kind: StrKind::Utf8, len: 1.into(), + index: Wtf8IndexSlot::new(), } } } @@ -226,6 +289,7 @@ impl From for StrData { data: Wtf8Buf::from(ch).into(), kind: StrKind::Wtf8, len: 1.into(), + index: Wtf8IndexSlot::new(), } } } @@ -241,7 +305,12 @@ impl StrData { StrKind::Ascii => data.len().into(), _ => StrLen::uncomputed(), }; - Self { data, kind, len } + Self { + data, + kind, + len, + index: Wtf8IndexSlot::new(), + } } /// # Safety @@ -253,6 +322,7 @@ impl StrData { data, kind, len: char_len.into(), + index: Wtf8IndexSlot::new(), } } @@ -322,11 +392,117 @@ impl StrData { len } + /// The byte offset the `index`-th code point starts at. + /// + /// An `index` at or past the end answers the buffer's byte length, so a + /// caller walking to a bound does not have to special-case it. + /// + /// O(1), but the first call on a non-ASCII string builds an index over the + /// whole buffer, so a caller that resolves a single index and stops is + /// better served by [`Self::nth_char`]. + pub fn char_index_to_byte(&self, index: usize) -> usize { + // For ASCII the two units coincide, and the table would be a Nth entry + // saying N. + if self.kind.is_ascii() { + return index.min(self.data.len()); + } + let char_len = self.char_len(); + if index >= char_len { + return self.data.len(); + } + self.index + .get_or_build(&self.data, char_len) + .byte_offset(&self.data, index) + } + + /// The byte offset of code point `index`, for a caller that resolves one + /// index and stops. + /// + /// Building the table costs a pass over the whole buffer, so it is worth it + /// only for a caller that comes back; an index within + /// [`MAX_WALK_TO_INDEX`] steps of either end is cheaper to walk to, and + /// walking keeps `s[0]` on a long string from paying for a table it will + /// never use again. Anything further in builds, on the reasoning that a + /// string indexed once in the middle tends to be indexed again. + fn char_index_to_byte_once(&self, index: usize) -> usize { + if index <= MAX_WALK_TO_INDEX { + return self + .data + .code_point_indices() + .nth(index) + .map_or(self.data.len(), |(byte, _)| byte); + } + let from_end = self.char_len() - index; + if from_end <= MAX_WALK_TO_INDEX { + return self + .data + .code_point_indices() + .nth_back(from_end - 1) + .map_or(self.data.len(), |(byte, _)| byte); + } + self.char_index_to_byte(index) + } + + /// The byte range spanned by the code points in `range`, whose end must not + /// exceed the string's code point count. + /// + /// A range that reaches within [`MAX_WALK_TO_INDEX`] of *both* ends is + /// walked to for the same reason a single index near one end is -- a slice + /// like `s[1:-1]` should not build a table over the whole string. + #[must_use] + pub fn char_range_to_bytes(&self, range: core::ops::Range) -> core::ops::Range { + if self.kind.is_ascii() { + return range; + } + let from_end = self.char_len() - range.end; + if range.start <= MAX_WALK_TO_INDEX && from_end <= MAX_WALK_TO_INDEX { + // Two walks over disjoint ends, each of at most MAX_WALK_TO_INDEX + // steps -- one iterator driven from both sides would have them meet + // on a short string. + let start = self + .data + .code_point_indices() + .nth(range.start) + .map_or(self.data.len(), |(byte, _)| byte); + let end = match from_end { + 0 => self.data.len(), + n => self + .data + .code_point_indices() + .nth_back(n - 1) + .map_or(self.data.len(), |(byte, _)| byte), + }; + return start..end; + } + self.char_index_to_byte(range.start)..self.char_index_to_byte(range.end) + } + + /// The character index of the character starting at byte offset `bytepos`, + /// the inverse of [`Self::char_index_to_byte`]. + /// + /// `bytepos` must be a character boundary at or before the end. + /// + /// Logarithmic rather than constant, because the index is keyed the other + /// way -- but a search whose bounds came from `char_index_to_byte` has the + /// table already, and this is what turns a byte offset back into the answer + /// a caller asked for in characters. + pub fn byte_to_char_index(&self, bytepos: usize) -> usize { + if self.kind.is_ascii() { + return bytepos; + } + let char_len = self.char_len(); + self.index + .get_or_build(&self.data, char_len) + .char_index_at_byte(&self.data, bytepos, char_len) + } + pub fn nth_char(&self, index: usize) -> CodePoint { match self.as_str_kind() { PyKindStr::Ascii(s) => s[index].into(), - PyKindStr::Utf8(s) => s.chars().nth(index).unwrap().into(), - PyKindStr::Wtf8(w) => w.code_points().nth(index).unwrap(), + _ => self.data[self.char_index_to_byte_once(index)..] + .code_points() + .next() + .unwrap(), } } } @@ -416,20 +592,21 @@ pub fn codepoint_range_end(s: &Wtf8, n_chars: usize) -> Option { } #[must_use] -pub fn zfill(bytes: &[u8], width: usize) -> Vec { +/// Returns `None` for a width whose result cannot be allocated. +pub fn zfill(bytes: &[u8], width: usize) -> Option> { if width <= bytes.len() { - bytes.to_vec() - } else { - let (sign, s) = match bytes.first() { - Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]), - _ => (&b""[..], bytes), - }; - let mut filled = Vec::new(); - filled.extend_from_slice(sign); - filled.extend(core::iter::repeat_n(b'0', width - bytes.len())); - filled.extend_from_slice(s); - filled + return Some(bytes.to_vec()); } + let (sign, s) = match bytes.first() { + Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]), + _ => (&b""[..], bytes), + }; + let mut filled = Vec::new(); + filled.try_reserve_exact(width).ok()?; + filled.extend_from_slice(sign); + filled.extend(core::iter::repeat_n(b'0', width - bytes.len())); + filled.extend_from_slice(s); + Some(filled) } /// Convert a string to ascii compatible, escaping unicode-s into escape diff --git a/crates/common/src/wtf8_index.rs b/crates/common/src/wtf8_index.rs new file mode 100644 index 00000000000..4b2e5ebee5b --- /dev/null +++ b/crates/common/src/wtf8_index.rs @@ -0,0 +1,299 @@ +// spell-checker:ignore rpython rlib rutf +//! Random access into a WTF-8 buffer. +//! +//! WTF-8 is variable width, so a buffer's n-th code point can only be found by +//! decoding the n-1 before it: [`Wtf8`]'s iterators are sequential, and +//! resolving an index through them is O(n). Code that indexes the same string +//! repeatedly -- a regex scan restarting at successive positions, say -- then +//! walks the whole buffer once per index, which is quadratic in its length. +//! +//! [`Wtf8Index`] is the side table that makes the lookup O(1): one 24-byte +//! group per 64 code points, so 0.375 bytes per code point. It is a cache, and +//! holds no state of its own beyond the buffer's shape -- building it twice for +//! the same buffer yields the same table. +//! +//! The layout is PyPy's `UTF8_INDEX_STORAGE` (`rpython/rlib/rutf8.py`). + +use crate::wtf8::Wtf8; + +/// One group of 64 code points. +#[derive(Clone, Copy)] +struct Group { + /// The byte offset the group's first code point starts at. + base: usize, + /// `ofs[i]` is the byte offset of the group's `4 * i + 1`-th code point, + /// relative to `base`. One entry covers four code points, so the widest + /// offset an entry has to hold is that of the 61st code point of a group, + /// at most `61 * 4 = 244` bytes in -- inside a `u8`, which is what buys the + /// table its density. + ofs: [u8; 16], +} + +/// A code-point-index to byte-offset table for one WTF-8 buffer. +pub struct Wtf8Index { + groups: Box<[Group]>, +} + +impl Wtf8Index { + /// Builds the table for `data`, whose code point count is `char_len`. + /// + /// O(`data.len()`), and touches every byte, so it pays for itself only when + /// the caller goes on to index the buffer more than a couple of times. + #[must_use] + pub fn new(data: &Wtf8, char_len: usize) -> Self { + let mut groups = vec![ + Group { + base: 0, + ofs: [0; 16], + }; + char_len / 64 + 1 + ]; + // Signed: the countdown overshoots the last group -- the loop stops on + // the first negative value rather than at a group boundary. + let mut remaining = char_len as isize; + let mut base = 0; + let mut current = 0; + loop { + groups[current].base = base; + let mut next = base; + let mut group_filled = true; + for i in 0..16 { + // Past the end, step as if one more single-byte code point + // followed, so the entry stays in range and is never read. + next = if remaining == 0 { + next + 1 + } else { + next_pos(data, next) + }; + groups[current].ofs[i] = (next - base) as u8; + remaining -= 4; + if remaining < 0 { + debug_assert_eq!(current + 1, groups.len()); + group_filled = false; + break; + } + next = next_pos(data, next_pos(data, next_pos(data, next))); + } + if !group_filled { + break; + } + current += 1; + base = next; + } + Self { + groups: groups.into_boxed_slice(), + } + } + + /// The byte offset of `data`'s `index`-th code point. + /// + /// `data` must be the buffer the table was built for, and `index` must be + /// below its code point count. + #[inline] + #[must_use] + pub fn byte_offset(&self, data: &Wtf8, index: usize) -> usize { + let group = &self.groups[index >> 6]; + // The entry sits on the 4k+1-th code point of the group, so a lookup is + // one table read plus at most two steps in either direction. + let pos = group.base + group.ofs[(index >> 2) & 0x0F] as usize; + match index & 0x3 { + 0 => prev_pos(data, pos), + 1 => pos, + 2 => next_pos(data, pos), + _ => next_pos(data, next_pos(data, pos)), + } + } + + /// The index of the code point starting at byte offset `bytepos`, the + /// inverse of [`Self::byte_offset`]. + /// + /// `data` must be the buffer the table was built for, `char_len` its code + /// point count, and `bytepos` a code point boundary at or before its end. + /// + /// Logarithmic rather than constant: the table is keyed by code point + /// index, so going the other way is a search through it. The bracketing + /// below is what keeps that search short -- a code point occupies one to + /// four bytes, which pins the answer to a narrow band around `bytepos` + /// before the first comparison. + #[must_use] + pub fn char_index_at_byte(&self, data: &Wtf8, bytepos: usize, char_len: usize) -> usize { + let bytes_remaining = data.len() - bytepos; + // At least one byte per remaining code point, and at most four, so the + // group holding the answer lies between these. + let mut group_min = + usize::max(bytepos / 4, char_len.saturating_sub(bytes_remaining + 1)) >> 6; + let mut group_max = usize::min(bytepos, char_len.saturating_sub(bytes_remaining / 4)) >> 6; + while group_min < group_max { + let middle = group_min.midpoint(group_max) + 1; + if bytepos < self.groups[middle].base { + group_max = middle - 1; + } else { + group_min = middle; + } + } + + let base = self.groups[group_min].base; + if base == bytepos { + return group_min << 6; + } + // Walk the group's entries to the last one at or before `bytepos`, + // then step the remaining code points, of which there are at most + // three -- an entry covers four. + let entries = if group_min == self.groups.len() - 1 { + ((char_len - 1) >> 2) & 0x0F + } else { + 16 + }; + let mut index = group_min << 6; + let mut pos = base; + for entry in 0..entries { + let at = base + self.groups[group_min].ofs[entry] as usize; + if at >= bytepos { + break; + } + pos = at; + index = (group_min << 6) + (entry << 2) + 1; + } + while pos < bytepos { + pos = next_pos(data, pos); + index += 1; + } + index + } + + /// The table's heap footprint, in bytes. + #[must_use] + pub fn byte_size(&self) -> usize { + core::mem::size_of_val(&*self.groups) + } +} + +/// The byte offset of the code point after the one at `pos`. +/// +/// `data` must be well-formed WTF-8 and `pos` a code point boundary before its +/// end -- reading only the lead byte is what makes this branch-light. +#[inline] +fn next_pos(data: &Wtf8, pos: usize) -> usize { + match data.as_bytes()[pos] { + 0x00..=0x7F => pos + 1, + 0x80..=0xDF => pos + 2, + 0xE0..=0xEF => pos + 3, + _ => pos + 4, + } +} + +/// The byte offset of the code point before the one at `pos`, which must not be +/// zero. +/// +/// A `pos` one past the end reads as the extra code point [`Wtf8Index::new`] +/// steps over there. +#[inline] +fn prev_pos(data: &Wtf8, pos: usize) -> usize { + let data = data.as_bytes(); + let mut pos = pos - 1; + if pos >= data.len() || data[pos] <= 0x7F { + return pos; + } + pos -= 1; + if data[pos] >= 0xC0 { + return pos; + } + pos -= 1; + if data[pos] >= 0xC0 { + return pos; + } + pos - 1 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wtf8::{CodePoint, Wtf8Buf}; + + /// Every index of `s`, both ways, against the offsets its own iterator + /// reports. + fn check(s: &Wtf8) { + let expected: Vec = s + .code_point_indices() + .map(|(byte_offset, _)| byte_offset) + .collect(); + let char_len = expected.len(); + let index = Wtf8Index::new(s, char_len); + for (i, &want) in expected.iter().enumerate() { + assert_eq!( + index.byte_offset(s, i), + want, + "index {i} of {s:?} ({char_len} code points)" + ); + assert_eq!( + index.char_index_at_byte(s, want, char_len), + i, + "byte {want} of {s:?} ({char_len} code points)" + ); + } + // One past the last code point is a boundary too, and the searches that + // use this ask for it as an end bound. + assert_eq!( + index.char_index_at_byte(s, s.len(), char_len), + char_len, + "end of {s:?}" + ); + } + + fn wtf8(s: &str) -> Wtf8Buf { + Wtf8Buf::from(s) + } + + #[test] + fn empty() { + check(wtf8("").as_ref()); + } + + #[test] + fn widths() { + // One case per encoded width, and the boundaries between them. + check(wtf8("abc").as_ref()); + check(wtf8("\u{80}\u{7ff}").as_ref()); + check(wtf8("\u{800}\u{ffff}").as_ref()); + check(wtf8("\u{10000}\u{10ffff}").as_ref()); + check(wtf8("a\u{80}\u{800}\u{10000}").as_ref()); + } + + #[test] + fn group_boundaries() { + // A group covers 64 code points and an entry four, so the interesting + // lengths are the ones on and around both. + for len in [1, 3, 4, 5, 63, 64, 65, 127, 128, 129, 255, 256, 257] { + for unit in ["a", "\u{80}", "\u{800}", "\u{10000}"] { + check(wtf8(&unit.repeat(len)).as_ref()); + } + // Mixed widths, so a group's entries do not share a stride. + check(wtf8(&"a\u{80}\u{800}\u{10000}".repeat(len)).as_ref()); + } + } + + #[test] + fn lone_surrogates() { + let mut s = wtf8("a"); + for cp in [0xD800, 0xDBFF, 0xDC00, 0xDFFF] { + s.push(CodePoint::from_u32(cp).unwrap()); + s.push_str("b"); + } + check(s.as_ref()); + + // Surrogates only, spanning more than one group. + let mut s = wtf8(""); + for i in 0..200 { + s.push(CodePoint::from_u32(0xD800 + (i % 0x400)).unwrap()); + } + check(s.as_ref()); + } + + #[test] + fn byte_size_is_one_group_per_64_code_points() { + let s = wtf8(&"\u{10000}".repeat(200)); + let index = Wtf8Index::new(s.as_ref(), 200); + assert_eq!(index.byte_size(), (200 / 64 + 1) * size_of::()); + assert_eq!(size_of::(), 24); + } +} diff --git a/crates/compiler-core/Cargo.toml b/crates/compiler-core/Cargo.toml index 7d6d116f530..22201597d90 100644 --- a/crates/compiler-core/Cargo.toml +++ b/crates/compiler-core/Cargo.toml @@ -18,6 +18,7 @@ bitflagset = { workspace = true } itertools = { workspace = true } malachite-bigint = { workspace = true } num-complex = { workspace = true } +num-traits = { workspace = true } lz4_flex = { workspace = true } diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index e5ef24815a1..ba1639170a7 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -17,6 +17,7 @@ use core::{ use itertools::Itertools; use malachite_bigint::BigInt; use num_complex::Complex64; +use num_traits::Zero; use rustpython_wtf8::{Wtf8, Wtf8Buf}; pub use crate::bytecode::{ @@ -467,6 +468,13 @@ bitflags! { const COROUTINE = 0x0080; const ITERABLE_COROUTINE = 0x0100; const ASYNC_GENERATOR = 0x0200; + const FUTURE_DIVISION = 0x20000; + const FUTURE_ABSOLUTE_IMPORT = 0x40000; + const FUTURE_WITH_STATEMENT = 0x80000; + const FUTURE_PRINT_FUNCTION = 0x100000; + const FUTURE_UNICODE_LITERALS = 0x200000; + const FUTURE_BARRY_AS_BDFL = 0x400000; + const FUTURE_GENERATOR_STOP = 0x800000; const FUTURE_ANNOTATIONS = 0x1000000; /// If a code object represents a function and has a docstring, /// this bit is set and the first item in co_consts is the docstring. @@ -549,6 +557,14 @@ impl TryFrom<&[u8]> for CodeUnit { } } +impl TryFrom<[u8; 2]> for CodeUnit { + type Error = MarshalError; + + fn try_from(value: [u8; 2]) -> Result { + Ok(Self::new(value[0].try_into()?, value[1].into())) + } +} + pub struct CodeUnits { units: UnsafeCell>, adaptive_counters: Box<[AtomicU16]>, @@ -602,12 +618,13 @@ impl TryFrom<&[u8]> for CodeUnits { type Error = MarshalError; fn try_from(value: &[u8]) -> Result { - if !value.len().is_multiple_of(2) { + let (chunks, []) = value.as_chunks::<2>() else { return Err(Self::Error::InvalidBytecode); - } + }; - let units = value - .chunks_exact(2) + let units = chunks + .iter() + .copied() .map(CodeUnit::try_from) .collect::, _>>()?; Ok(units.into()) @@ -768,12 +785,15 @@ impl CodeUnits { /// Store a pointer-sized value atomically in the pointer cache at `index`. /// /// Uses a single `AtomicUsize` store to prevent torn writes when - /// multiple threads specialize the same instruction concurrently. + /// multiple threads specialize the same instruction concurrently. The + /// tear-free width also makes this the right slot for non-pointer guard + /// values (e.g. dict keys-version stamps) that must never be observed + /// half-written. /// /// # Safety /// - `index` must be in bounds. - /// - `value` must be `0` or a valid `*const PyObject` encoded as `usize`. - /// - Callers must follow the cache invalidation/upgrade protocol: + /// - When the slot holds a `*const PyObject` encoded as `usize` (or `0`), + /// callers must follow the cache invalidation/upgrade protocol: /// invalidate the version guard before writing and publish the new /// version after writing. pub unsafe fn write_cache_ptr(&self, index: usize, value: usize) { @@ -909,6 +929,31 @@ pub enum ConstantData { Ellipsis, } +impl ConstantData { + /// Whether or not python would return True/False for the given constant data. + /// + /// ```py + /// bool(0) # False + /// bool(1) # True + /// bool([]) # False + /// bool(...) # True + /// ``` + #[must_use] + pub fn truthiness(&self) -> bool { + match self { + Self::Tuple { elements } | Self::Frozenset { elements } => !elements.is_empty(), + Self::Integer { value } => !value.is_zero(), + Self::Float { value } => *value != 0.0, + Self::Complex { value } => value.re != 0.0 || value.im != 0.0, + Self::Boolean { value } => *value, + Self::Str { value } => !value.is_empty(), + Self::Bytes { value } => !value.is_empty(), + Self::Code { .. } | Self::Slice { .. } | Self::Ellipsis => true, + Self::None => false, + } + } +} + impl PartialEq for ConstantData { fn eq(&self, other: &Self) -> bool { match (self, other) { diff --git a/crates/compiler-core/src/bytecode/instruction.rs b/crates/compiler-core/src/bytecode/instruction.rs index 69714a0fe66..63e29222570 100644 --- a/crates/compiler-core/src/bytecode/instruction.rs +++ b/crates/compiler-core/src/bytecode/instruction.rs @@ -18,23 +18,29 @@ macro_rules! define_opcodes { } ) => { #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[repr($typ)] $opcode_vis enum $opcode_name { - $($op_name),* + $($op_name = $op_id),* } impl $opcode_name { #[doc = concat!("Converts this opcode to [`", stringify!($instr_name), "`].")] #[must_use] + #[inline] $opcode_vis const fn as_instruction(&self) -> $instr_name { - match self { - $( - Self::$op_name => $instr_name::$op_name $({ $arg_name: Arg::marker() })?, - )* - } + // SAFETY: `$opcode_name` and `$instr_name` are both `#[repr($typ)]` + // enums sharing identical explicit discriminants, and every + // `$instr_name` payload field is the zero-sized `Arg` marker + // (see the `size_of` assertion near its definition), so both + // enums have the same one-`$typ`-wide representation: just the + // discriminant. Converting a live `$opcode_name` value therefore + // yields the `$instr_name` variant with the matching discriminant. + unsafe { core::mem::transmute(*self) } } /// Map a specialized or instrumented opcode back to its adaptive (base) variant. #[must_use] + #[inline] $opcode_vis const fn deoptimize(self) -> Self { match self.deopt() { Some(v) => v, @@ -49,6 +55,9 @@ macro_rules! define_opcodes { } // NOTE: Keep private. Will be exposed under `try_from_u8/try_from_u16`. + // Kept as a match rather than a range check + transmute: `$op_id` + // values are not contiguous (specialized/instrumented opcodes leave + // gaps), so validity can't be expressed as a simple bound. pub(super) const fn try_from_numeric(value: $typ) -> Result { match value { $($op_id => Ok(Self::$op_name),)* @@ -58,10 +67,11 @@ macro_rules! define_opcodes { // NOTE: Keep private. Will be exposed under `as_u8/as_u16`. #[must_use] + #[inline] pub(super) const fn as_numeric(self) -> $typ { - match self { - $(Self::$op_name => $op_id,)* - } + // `$opcode_name` is `#[repr($typ)]` with an explicit `$op_id` + // discriminant on every variant, so this is a plain identity cast. + self as $typ } } @@ -95,15 +105,24 @@ macro_rules! define_opcodes { ),* } + // Every `$instr_name` payload field is the zero-sized `Arg` marker, so + // (combined with the `#[repr($typ)]` above) each variant's representation + // is exactly its `$typ` discriminant with no padding. `as_opcode` and + // `$opcode_name::as_instruction` rely on this to convert via + // `mem::transmute` instead of a per-variant match. + const _: () = assert!(core::mem::size_of::<$instr_name>() == core::mem::size_of::<$typ>()); + impl $instr_name { #[doc = concat!("Get the corresponding [`", stringify!($opcode_name), "`].")] #[must_use] + #[inline] $instr_vis const fn as_opcode(&self) -> $opcode_name { - match self { - $( - Self::$op_name $({ $arg_name: _ })? => $opcode_name::$op_name, - )* - } + // SAFETY: symmetric to `$opcode_name::as_instruction` above: + // `*self`'s representation is exactly its `$typ` discriminant + // (checked by the `size_of` assertion above this impl), and that + // discriminant is always a valid `$opcode_name` discriminant + // because both enums share the same explicit `$op_id` list. + unsafe { core::mem::transmute(*self) } } #[must_use] @@ -754,9 +773,8 @@ impl Opcode { /// Stack effect when the instruction takes its branch (jump=true). /// /// CPython equivalent: `stack_effect(opcode, oparg, jump=True)`. - /// For most instructions this equals the fallthrough effect. - /// Override for instructions where branch and fallthrough differ - /// (e.g. [`Self::ForIter`]: fallthrough = +1, branch = −1). + /// Current opcode metadata has the same real-opcode stack effect + /// for jump and fallthrough stack-depth calculation. #[must_use] pub fn stack_effect_jump(&self, oparg: u32) -> i32 { self.stack_effect(oparg) @@ -1415,6 +1433,26 @@ mod tests { assert!(!AnyInstruction::from(PseudoOpcode::Jump).has_const()); } + #[test] + fn stack_effects_match_cpython_opcode_metadata() { + assert_eq!(Opcode::ForIter.stack_effect_info(0).popped(), 1); + assert_eq!(Opcode::ForIter.stack_effect_info(0).pushed(), 2); + assert_eq!(Opcode::ForIter.stack_effect(0), 1); + assert_eq!(Opcode::ForIter.stack_effect_jump(0), 1); + + assert_eq!(Opcode::EndAsyncFor.stack_effect_info(0).popped(), 2); + assert_eq!(Opcode::EndAsyncFor.stack_effect_info(0).pushed(), 0); + assert_eq!(Opcode::PopJumpIfFalse.stack_effect(0), -1); + assert_eq!(Opcode::PopJumpIfFalse.stack_effect_jump(0), -1); + + assert_eq!(PseudoOpcode::SetupFinally.stack_effect_info(0).pushed(), 1); + assert_eq!(PseudoOpcode::SetupFinally.stack_effect(0), 0); + assert_eq!(PseudoOpcode::SetupFinally.stack_effect_jump(0), 1); + assert_eq!(PseudoOpcode::SetupCleanup.stack_effect_info(0).pushed(), 2); + assert_eq!(PseudoOpcode::SetupCleanup.stack_effect(0), 0); + assert_eq!(PseudoOpcode::SetupCleanup.stack_effect_jump(0), 2); + } + #[test] fn no_fallthrough_flags_match_cpython_basicblock_nofallthrough() { assert!(Opcode::JumpForward.is_no_fallthrough()); @@ -1431,4 +1469,228 @@ mod tests { assert!(AnyInstruction::from(PseudoOpcode::Jump).is_no_fallthrough()); } + + /// Snapshot of the chained `match` implementations that `Opcode::deopt` + /// and `Opcode::cache_entries` used before they were rewritten as table + /// lookups. Exists only to pin the observable behavior of the table + /// lookups against the logic they replaced. + mod reference { + use super::Opcode; + + pub(super) const fn deopt(op: Opcode) -> Option { + Some(match op { + Opcode::ResumeCheck => Opcode::Resume, + Opcode::LoadConstMortal | Opcode::LoadConstImmortal => Opcode::LoadConst, + Opcode::ToBoolAlwaysTrue + | Opcode::ToBoolBool + | Opcode::ToBoolInt + | Opcode::ToBoolList + | Opcode::ToBoolNone + | Opcode::ToBoolStr => Opcode::ToBool, + Opcode::BinaryOpMultiplyInt + | Opcode::BinaryOpAddInt + | Opcode::BinaryOpSubtractInt + | Opcode::BinaryOpMultiplyFloat + | Opcode::BinaryOpAddFloat + | Opcode::BinaryOpSubtractFloat + | Opcode::BinaryOpAddUnicode + | Opcode::BinaryOpSubscrListInt + | Opcode::BinaryOpSubscrListSlice + | Opcode::BinaryOpSubscrTupleInt + | Opcode::BinaryOpSubscrStrInt + | Opcode::BinaryOpSubscrDict + | Opcode::BinaryOpSubscrGetitem + | Opcode::BinaryOpExtend + | Opcode::BinaryOpInplaceAddUnicode => Opcode::BinaryOp, + Opcode::StoreSubscrDict | Opcode::StoreSubscrListInt => Opcode::StoreSubscr, + Opcode::SendGen => Opcode::Send, + Opcode::UnpackSequenceTwoTuple + | Opcode::UnpackSequenceTuple + | Opcode::UnpackSequenceList => Opcode::UnpackSequence, + Opcode::StoreAttrInstanceValue + | Opcode::StoreAttrSlot + | Opcode::StoreAttrWithHint => Opcode::StoreAttr, + Opcode::LoadGlobalModule | Opcode::LoadGlobalBuiltin => Opcode::LoadGlobal, + Opcode::LoadSuperAttrAttr | Opcode::LoadSuperAttrMethod => Opcode::LoadSuperAttr, + Opcode::LoadAttrInstanceValue + | Opcode::LoadAttrModule + | Opcode::LoadAttrWithHint + | Opcode::LoadAttrSlot + | Opcode::LoadAttrClass + | Opcode::LoadAttrClassWithMetaclassCheck + | Opcode::LoadAttrProperty + | Opcode::LoadAttrGetattributeOverridden + | Opcode::LoadAttrMethodWithValues + | Opcode::LoadAttrMethodNoDict + | Opcode::LoadAttrMethodLazyDict + | Opcode::LoadAttrNondescriptorWithValues + | Opcode::LoadAttrNondescriptorNoDict => Opcode::LoadAttr, + Opcode::CompareOpFloat | Opcode::CompareOpInt | Opcode::CompareOpStr => { + Opcode::CompareOp + } + Opcode::ContainsOpSet | Opcode::ContainsOpDict => Opcode::ContainsOp, + Opcode::JumpBackwardNoJit | Opcode::JumpBackwardJit => Opcode::JumpBackward, + Opcode::ForIterList + | Opcode::ForIterTuple + | Opcode::ForIterRange + | Opcode::ForIterGen => Opcode::ForIter, + Opcode::CallBoundMethodExactArgs + | Opcode::CallPyExactArgs + | Opcode::CallType1 + | Opcode::CallStr1 + | Opcode::CallTuple1 + | Opcode::CallBuiltinClass + | Opcode::CallBuiltinO + | Opcode::CallBuiltinFast + | Opcode::CallBuiltinFastWithKeywords + | Opcode::CallLen + | Opcode::CallIsinstance + | Opcode::CallListAppend + | Opcode::CallMethodDescriptorO + | Opcode::CallMethodDescriptorFastWithKeywords + | Opcode::CallMethodDescriptorNoargs + | Opcode::CallMethodDescriptorFast + | Opcode::CallAllocAndEnterInit + | Opcode::CallPyGeneral + | Opcode::CallBoundMethodGeneral + | Opcode::CallNonPyGeneral => Opcode::Call, + Opcode::CallKwBoundMethod | Opcode::CallKwPy | Opcode::CallKwNonPy => { + Opcode::CallKw + } + _ => return None, + }) + } + + pub(super) const fn deoptimize(op: Opcode) -> Opcode { + match deopt(op) { + Some(v) => v, + None => match op.to_base() { + Some(v) => v, + None => op, + }, + } + } + + pub(super) const fn cache_entries(op: Opcode) -> usize { + match deoptimize(op) { + Opcode::StoreSubscr => 1, + Opcode::ToBool => 3, + Opcode::BinaryOp => 5, + Opcode::Call => 3, + Opcode::CallKw => 3, + Opcode::CompareOp => 1, + Opcode::ContainsOp => 1, + Opcode::ForIter => 1, + Opcode::JumpBackward => 1, + Opcode::LoadAttr => 9, + Opcode::LoadGlobal => 4, + Opcode::LoadSuperAttr => 1, + Opcode::PopJumpIfFalse => 1, + Opcode::PopJumpIfNone => 1, + Opcode::PopJumpIfNotNone => 1, + Opcode::PopJumpIfTrue => 1, + Opcode::Send => 1, + Opcode::StoreAttr => 4, + Opcode::UnpackSequence => 1, + _ => 0, + } + } + } + + #[test] + fn cache_entries_and_deopt_tables_match_reference_impl() { + let mut checked = 0; + for byte in 0u8..=255 { + let Ok(op) = Opcode::try_from_u8(byte) else { + continue; + }; + + assert_eq!( + op.deopt(), + reference::deopt(op), + "deopt() mismatch for {op:?}" + ); + assert_eq!( + op.cache_entries(), + reference::cache_entries(op), + "cache_entries() mismatch for {op:?}" + ); + checked += 1; + } + + // Sanity check that the loop actually exercised opcodes rather than + // silently skipping all of them. + assert!(checked > 200); + } + + /// `Opcode::as_numeric`, `Opcode::as_instruction` and + /// `Instruction::as_opcode` used to be per-variant matches; they are now + /// an identity cast and two `mem::transmute`s respectively. `byte` (an + /// input independent of any of those three functions) together with the + /// untouched `try_from_u8`/`TryFrom` conversions serve as the + /// reference: every opcode reachable from a byte must convert back to + /// that exact byte and round-trip through `Instruction`. + #[test] + fn opcode_instruction_numeric_conversions_match_try_from_numeric() { + let mut checked = 0; + for byte in 0u8..=255 { + let Ok(op) = Opcode::try_from_u8(byte) else { + continue; + }; + + assert_eq!(op.as_numeric(), byte, "as_numeric() mismatch for {op:?}"); + + let instr = op.as_instruction(); + assert_eq!( + instr.as_opcode(), + op, + "as_instruction()/as_opcode() round trip mismatch for {op:?}" + ); + + let instr_via_try_from = Instruction::try_from(byte).unwrap(); + assert_eq!( + instr_via_try_from.as_opcode(), + op, + "Instruction::try_from({byte}) mismatch" + ); + + checked += 1; + } + + assert!(checked > 200); + } + + /// Same as [`opcode_instruction_numeric_conversions_match_try_from_numeric`] + /// but for the `u16`-discriminant pseudo-opcode instantiation of + /// `define_opcodes!`. + #[test] + fn pseudo_opcode_instruction_numeric_conversions_match_try_from_numeric() { + let mut checked = 0; + for value in 0u16..=u16::MAX { + let Ok(op) = PseudoOpcode::try_from_u16(value) else { + continue; + }; + + assert_eq!(op.as_numeric(), value, "as_numeric() mismatch for {op:?}"); + + let instr = op.as_instruction(); + assert_eq!( + instr.as_opcode(), + op, + "as_instruction()/as_opcode() round trip mismatch for {op:?}" + ); + + let instr_via_try_from = PseudoInstruction::try_from(value).unwrap(); + assert_eq!( + instr_via_try_from.as_opcode(), + op, + "PseudoInstruction::try_from({value}) mismatch" + ); + + checked += 1; + } + + // All 11 `PseudoInstruction` variants should have been exercised. + assert_eq!(checked, 11); + } } diff --git a/crates/compiler-core/src/bytecode/oparg.rs b/crates/compiler-core/src/bytecode/oparg.rs index 03628604a3f..8f706003091 100644 --- a/crates/compiler-core/src/bytecode/oparg.rs +++ b/crates/compiler-core/src/bytecode/oparg.rs @@ -777,19 +777,20 @@ oparg_enum!( #[derive(Copy, Clone)] pub struct UnpackExArgs { pub before: u8, - pub after: u8, + pub after: u32, } impl From for UnpackExArgs { fn from(value: u32) -> Self { - let [before, after, ..] = value.to_le_bytes(); + let before = (value & 0xFF) as u8; + let after = value >> 8; Self { before, after } } } impl From for u32 { fn from(value: UnpackExArgs) -> Self { - Self::from_le_bytes([value.before, value.after, 0, 0]) + Self::from(value.before) | (value.after << 8) } } diff --git a/crates/compiler-core/src/bytecode/opcode_metadata.rs b/crates/compiler-core/src/bytecode/opcode_metadata.rs index 64c8d3c5330..16db49bea0d 100644 --- a/crates/compiler-core/src/bytecode/opcode_metadata.rs +++ b/crates/compiler-core/src/bytecode/opcode_metadata.rs @@ -6,114 +6,292 @@ use crate::{bytecode::instruction::StackEffect, marshal::MarshalError}; impl super::Opcode { /// Returns [`Self`] as [`u8`]. #[must_use] + #[inline] pub const fn as_u8(self) -> u8 { self.as_numeric() } #[must_use] + #[inline] pub const fn cache_entries(self) -> usize { - match self.deoptimize() { - Self::StoreSubscr => 1, - Self::ToBool => 3, - Self::BinaryOp => 5, - Self::Call => 3, - Self::CallKw => 3, - Self::CompareOp => 1, - Self::ContainsOp => 1, - Self::ForIter => 1, - Self::JumpBackward => 1, - Self::LoadAttr => 9, - Self::LoadGlobal => 4, - Self::LoadSuperAttr => 1, - Self::PopJumpIfFalse => 1, - Self::PopJumpIfNone => 1, - Self::PopJumpIfNotNone => 1, - Self::PopJumpIfTrue => 1, - Self::Send => 1, - Self::StoreAttr => 4, - Self::UnpackSequence => 1, - _ => 0, - } + const CACHE_ENTRIES: [u8; 256] = [ + 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 4, 4, 1, 1, 0, 1, 4, 4, 4, 1, 1, + 3, 3, 3, 3, 3, 3, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 3, 3, 0, 1, 0, 0, + ]; + + CACHE_ENTRIES[self.as_numeric() as usize] as usize } #[must_use] + #[inline] pub const fn deopt(self) -> Option { - Some(match self { - Self::ResumeCheck => Self::Resume, - Self::LoadConstMortal | Self::LoadConstImmortal => Self::LoadConst, - Self::ToBoolAlwaysTrue - | Self::ToBoolBool - | Self::ToBoolInt - | Self::ToBoolList - | Self::ToBoolNone - | Self::ToBoolStr => Self::ToBool, - Self::BinaryOpMultiplyInt - | Self::BinaryOpAddInt - | Self::BinaryOpSubtractInt - | Self::BinaryOpMultiplyFloat - | Self::BinaryOpAddFloat - | Self::BinaryOpSubtractFloat - | Self::BinaryOpAddUnicode - | Self::BinaryOpSubscrListInt - | Self::BinaryOpSubscrListSlice - | Self::BinaryOpSubscrTupleInt - | Self::BinaryOpSubscrStrInt - | Self::BinaryOpSubscrDict - | Self::BinaryOpSubscrGetitem - | Self::BinaryOpExtend - | Self::BinaryOpInplaceAddUnicode => Self::BinaryOp, - Self::StoreSubscrDict | Self::StoreSubscrListInt => Self::StoreSubscr, - Self::SendGen => Self::Send, - Self::UnpackSequenceTwoTuple | Self::UnpackSequenceTuple | Self::UnpackSequenceList => { - Self::UnpackSequence - } - Self::StoreAttrInstanceValue | Self::StoreAttrSlot | Self::StoreAttrWithHint => { - Self::StoreAttr - } - Self::LoadGlobalModule | Self::LoadGlobalBuiltin => Self::LoadGlobal, - Self::LoadSuperAttrAttr | Self::LoadSuperAttrMethod => Self::LoadSuperAttr, - Self::LoadAttrInstanceValue - | Self::LoadAttrModule - | Self::LoadAttrWithHint - | Self::LoadAttrSlot - | Self::LoadAttrClass - | Self::LoadAttrClassWithMetaclassCheck - | Self::LoadAttrProperty - | Self::LoadAttrGetattributeOverridden - | Self::LoadAttrMethodWithValues - | Self::LoadAttrMethodNoDict - | Self::LoadAttrMethodLazyDict - | Self::LoadAttrNondescriptorWithValues - | Self::LoadAttrNondescriptorNoDict => Self::LoadAttr, - Self::CompareOpFloat | Self::CompareOpInt | Self::CompareOpStr => Self::CompareOp, - Self::ContainsOpSet | Self::ContainsOpDict => Self::ContainsOp, - Self::JumpBackwardNoJit | Self::JumpBackwardJit => Self::JumpBackward, - Self::ForIterList | Self::ForIterTuple | Self::ForIterRange | Self::ForIterGen => { - Self::ForIter - } - Self::CallBoundMethodExactArgs - | Self::CallPyExactArgs - | Self::CallType1 - | Self::CallStr1 - | Self::CallTuple1 - | Self::CallBuiltinClass - | Self::CallBuiltinO - | Self::CallBuiltinFast - | Self::CallBuiltinFastWithKeywords - | Self::CallLen - | Self::CallIsinstance - | Self::CallListAppend - | Self::CallMethodDescriptorO - | Self::CallMethodDescriptorFastWithKeywords - | Self::CallMethodDescriptorNoargs - | Self::CallMethodDescriptorFast - | Self::CallAllocAndEnterInit - | Self::CallPyGeneral - | Self::CallBoundMethodGeneral - | Self::CallNonPyGeneral => Self::Call, - Self::CallKwBoundMethod | Self::CallKwPy | Self::CallKwNonPy => Self::CallKw, - _ => return None, - }) + const DEOPT: [Option; 256] = [ + None, + None, + None, + Some(super::Opcode::BinaryOp), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::BinaryOp), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::CallKw), + Some(super::Opcode::CallKw), + Some(super::Opcode::CallKw), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::Call), + Some(super::Opcode::CompareOp), + Some(super::Opcode::CompareOp), + Some(super::Opcode::CompareOp), + Some(super::Opcode::ContainsOp), + Some(super::Opcode::ContainsOp), + Some(super::Opcode::ForIter), + Some(super::Opcode::ForIter), + Some(super::Opcode::ForIter), + Some(super::Opcode::ForIter), + Some(super::Opcode::JumpBackward), + Some(super::Opcode::JumpBackward), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadAttr), + Some(super::Opcode::LoadConst), + Some(super::Opcode::LoadConst), + Some(super::Opcode::LoadGlobal), + Some(super::Opcode::LoadGlobal), + Some(super::Opcode::LoadSuperAttr), + Some(super::Opcode::LoadSuperAttr), + Some(super::Opcode::Resume), + Some(super::Opcode::Send), + Some(super::Opcode::StoreAttr), + Some(super::Opcode::StoreAttr), + Some(super::Opcode::StoreAttr), + Some(super::Opcode::StoreSubscr), + Some(super::Opcode::StoreSubscr), + Some(super::Opcode::ToBool), + Some(super::Opcode::ToBool), + Some(super::Opcode::ToBool), + Some(super::Opcode::ToBool), + Some(super::Opcode::ToBool), + Some(super::Opcode::ToBool), + Some(super::Opcode::UnpackSequence), + Some(super::Opcode::UnpackSequence), + Some(super::Opcode::UnpackSequence), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ]; + + DEOPT[self.as_numeric() as usize] } /// Does this opcode have 'HAS_ARG_FLAG' set. @@ -664,6 +842,7 @@ impl super::Opcode { } #[must_use] + #[inline] pub const fn to_base(self) -> Option { Some(match self { Self::InstrumentedCall => Self::Call, @@ -723,16 +902,19 @@ impl super::Opcode { impl super::PseudoOpcode { /// Returns [`Self`] as [`u16`]. #[must_use] + #[inline] pub const fn as_u16(self) -> u16 { self.as_numeric() } #[must_use] + #[inline] pub const fn cache_entries(self) -> usize { 0 } #[must_use] + #[inline] pub const fn deopt(self) -> Option { None } @@ -818,6 +1000,7 @@ impl super::PseudoOpcode { } #[must_use] + #[inline] pub const fn to_base(self) -> Option { None } diff --git a/crates/compiler-core/src/marshal.rs b/crates/compiler-core/src/marshal.rs index a2a23054e4b..754854e7cba 100644 --- a/crates/compiler-core/src/marshal.rs +++ b/crates/compiler-core/src/marshal.rs @@ -19,6 +19,14 @@ pub enum MarshalError { InvalidLocation, /// Bad type marker BadType, + /// A type marker no reader knows + UnknownType, + /// A back reference that names nothing + InvalidRef, + /// A marker that stands for no object at all + NullObject, + /// A container length that is negative or does not fit, named by what it counts + BadSize(&'static str), } impl core::fmt::Display for MarshalError { @@ -29,6 +37,10 @@ impl core::fmt::Display for MarshalError { Self::InvalidUtf8 => f.write_str("invalid utf8"), Self::InvalidLocation => f.write_str("invalid source location"), Self::BadType => f.write_str("bad type marker"), + Self::UnknownType => f.write_str("unknown type code"), + Self::InvalidRef => f.write_str("invalid reference"), + Self::NullObject => f.write_str("NULL object in marshal data for object"), + Self::BadSize(what) => write!(f, "{what} size out of range"), } } } @@ -111,7 +123,7 @@ impl TryFrom for Type { b'A' => Self::AsciiInterned, b'z' => Self::ShortAscii, b'Z' => Self::ShortAsciiInterned, - _ => return Err(MarshalError::BadType), + _ => return Err(MarshalError::UnknownType), }) } } @@ -146,6 +158,13 @@ pub trait Read { fn read_u64(&mut self) -> Result { Ok(u64::from_le_bytes(*self.read_array()?)) } + + /// A length, read the way `r_long` reads one: it is signed, so a value + /// with the top bit set is out of range rather than four billion items. + fn read_len(&mut self, what: &'static str) -> Result { + let len = self.read_u32()? as i32; + usize::try_from(len).map_err(|_| MarshalError::BadSize(what)) + } } pub(crate) trait ReadBorrowed<'a>: Read { @@ -305,7 +324,7 @@ fn reserve_ref_slot(has_flag: bool, refs: &mut Vec>) -> Option(idx: usize, refs: &[Option]) -> Result { refs.get(idx) .and_then(|v| v.clone()) - .ok_or(MarshalError::InvalidBytecode) + .ok_or(MarshalError::InvalidRef) } /// Read a marshal bytes object (TYPE_STRING = b's'), resolving TYPE_REF @@ -408,7 +427,7 @@ fn read_marshal_str_vec( } let n = match type_byte { - b'(' => rdr.read_u32()? as usize, + b'(' => rdr.read_len("tuple")?, b')' => rdr.read_u8()? as usize, _ => return Err(MarshalError::BadType), }; @@ -471,7 +490,7 @@ fn read_marshal_const_tuple( } let n = match type_byte { - b'(' => rdr.read_u32()? as usize, + b'(' => rdr.read_len("tuple")?, b')' => rdr.read_u8()? as usize, _ => return Err(MarshalError::BadType), }; @@ -516,7 +535,7 @@ fn read_const_value( let code = deserialize_code_inner(rdr, bag, depth - 1, refs)?; bag.make_code(code) } else { - deserialize_value_typed(rdr, bag, depth, refs, typ)? + deserialize_value_typed(rdr, bag, depth, refs, typ, slot)? }; if let Some(idx) = slot { refs[idx] = Some(value.clone()); @@ -540,6 +559,10 @@ pub trait MarshalBag: Copy { fn make_str(&self, value: &Wtf8) -> Self::Value; + fn make_interned_str(&self, value: &Wtf8) -> Self::Value { + self.make_str(value) + } + fn make_bytes(&self, value: &[u8]) -> Self::Value; fn make_int(&self, value: BigInt) -> Self::Value; @@ -549,7 +572,19 @@ pub trait MarshalBag: Copy { fn make_code( &self, code: CodeObject<::Constant>, - ) -> Self::Value; + ) -> Result; + + /// Construct a runtime code object while retaining the exact values read + /// from ``co_consts``. Compiler bags ignore this second channel; runtime + /// bags use it for marshalable values (lists, dicts, sets, recursive + /// containers) that their compiler constant representation cannot hold. + fn make_code_with_constants( + &self, + code: CodeObject<::Constant>, + _constants: Vec, + ) -> Result { + self.make_code(code) + } fn make_stop_iter(&self) -> Result; @@ -564,6 +599,55 @@ pub trait MarshalBag: Copy { it: impl Iterator, ) -> Result; + /// Install partially-built containers in the marshal reference table + /// before reading their children, as CPython's `r_object()` does. + /// Runtime bags can opt in; constant bags retain collect-then-construct. + /// + /// `len` comes straight from the input and is only bounded by what a + /// length can hold, so a bag that opts in reports the room it cannot get + /// rather than taking it for granted. + fn make_tuple_placeholder(&self, _len: usize) -> Result> { + Ok(None) + } + + fn set_tuple_item( + &self, + _tuple: &Self::Value, + _index: usize, + _value: Self::Value, + ) -> Result<()> { + Err(MarshalError::BadType) + } + + fn make_list_placeholder(&self, _len: usize) -> Result> { + Ok(None) + } + + fn set_list_item(&self, _list: &Self::Value, _index: usize, _value: Self::Value) -> Result<()> { + Err(MarshalError::BadType) + } + + fn make_set_placeholder(&self) -> Option { + None + } + + fn insert_set_item(&self, _set: &Self::Value, _value: Self::Value) -> Result<()> { + Err(MarshalError::BadType) + } + + fn make_dict_placeholder(&self) -> Option { + None + } + + fn insert_dict_item( + &self, + _dict: &Self::Value, + _key: Self::Value, + _value: Self::Value, + ) -> Result<()> { + Err(MarshalError::BadType) + } + fn make_slice( &self, _start: Self::Value, @@ -581,6 +665,30 @@ pub trait MarshalBag: Copy { ) -> Option<::Constant> { None } + + /// Convert a runtime constant to the compiler-side shape stored in + /// ``CodeObject``. Runtime implementations may return a semantically + /// unused placeholder when the exact value is carried by + /// `make_code_with_constants` instead. + fn code_constant_from_value( + &self, + value: &Self::Value, + ) -> Result<::Constant> { + self.constant_ref_from_value(value) + .ok_or(MarshalError::BadType) + } + + fn bytes_from_value(&self, _value: &Self::Value) -> Option> { + None + } + + fn str_from_value(&self, _value: &Self::Value) -> Option { + None + } + + fn tuple_elements_from_value(&self, _value: &Self::Value) -> Option> { + None + } } impl MarshalBag for Bag { @@ -640,8 +748,8 @@ impl MarshalBag for Bag { fn make_code( &self, code: CodeObject<::Constant>, - ) -> Self::Value { - self.make_code(code) + ) -> Result { + Ok(self.make_code(code)) } fn make_stop_iter(&self) -> Result { @@ -682,6 +790,27 @@ impl MarshalBag for Bag { ) -> Option<::Constant> { Some(value.clone()) } + + fn bytes_from_value(&self, value: &Self::Value) -> Option> { + match value.borrow_constant() { + BorrowedConstant::Bytes { value } => Some(value.to_vec()), + _ => None, + } + } + + fn str_from_value(&self, value: &Self::Value) -> Option { + match value.borrow_constant() { + BorrowedConstant::Str { value } => Some(value.to_string_lossy().into_owned()), + _ => None, + } + } + + fn tuple_elements_from_value(&self, value: &Self::Value) -> Option> { + match value.borrow_constant() { + BorrowedConstant::Tuple { elements } => Some(elements.to_vec()), + _ => None, + } + } } pub const MAX_MARSHAL_STACK_DEPTH: usize = 2000; @@ -724,10 +853,7 @@ fn deserialize_value_after_header( // TYPE_REF: return previously stored object if type_code == Type::Ref as u8 { let idx = rdr.read_u32()? as usize; - return refs - .get(idx) - .and_then(|v| v.clone()) - .ok_or(MarshalError::InvalidBytecode); + return resolve_ref(idx, refs); } // Reserve ref slot before reading (matches write order) @@ -740,22 +866,10 @@ fn deserialize_value_after_header( }; let typ = Type::try_from(type_code)?; - // CPython's r_object() uses one global ref table: TYPE_CODE reserves its - // slot before reading code fields, and those fields may use later TYPE_REF - // indexes. Keep the same indexes even when Bag::Value and Constant differ. let value = if matches!(typ, Type::Code) { - let mut inner_refs: Vec::Constant>> = refs - .iter() - .map(|value| { - value - .as_ref() - .and_then(|value| bag.constant_ref_from_value(value)) - }) - .collect(); - let code = deserialize_code_inner(rdr, bag.constant_bag(), depth - 1, &mut inner_refs)?; - bag.make_code(code) + deserialize_code_value_inner(rdr, bag, depth - 1, refs)? } else { - deserialize_value_typed(rdr, bag, depth, refs, typ)? + deserialize_value_typed(rdr, bag, depth, refs, typ, slot)? }; if let Some(idx) = slot { @@ -764,12 +878,144 @@ fn deserialize_value_after_header( Ok(value) } +/// Decode a code object through the runtime bag. CPython's marshal reader +/// keeps one reference table for the code fields and `co_consts`; using +/// `Bag::Value` here preserves that index space and lets runtime-only +/// constants survive alongside the compiler representation. +fn deserialize_code_value_inner( + rdr: &mut R, + bag: Bag, + depth: usize, + refs: &mut Vec>, +) -> Result { + if depth == 0 { + return Err(MarshalError::InvalidBytecode); + } + let arg_count = rdr.read_u32()?; + let posonlyarg_count = rdr.read_u32()?; + let kwonlyarg_count = rdr.read_u32()?; + let max_stackdepth = rdr.read_u32()?; + let flags = CodeFlags::from_bits_truncate(rdr.read_u32()?); + let child_depth = depth - 1; + + let code_value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + let code_bytes = bag + .bytes_from_value(&code_value) + .ok_or(MarshalError::BadType)?; + + let consts_value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + let constant_values = bag + .tuple_elements_from_value(&consts_value) + .ok_or(MarshalError::BadType)?; + let constants = constant_values + .iter() + .map(|value| bag.code_constant_from_value(value)) + .collect::>>()? + .into_iter() + .collect(); + + let read_strings = + |rdr: &mut R, refs: &mut Vec>| -> Result> { + let tuple = deserialize_value_depth(rdr, bag, child_depth, refs)?; + bag.tuple_elements_from_value(&tuple) + .ok_or(MarshalError::BadType)? + .iter() + .map(|value| bag.str_from_value(value).ok_or(MarshalError::BadType)) + .collect() + }; + let names_raw = read_strings(rdr, refs)?; + let localsplusnames = read_strings(rdr, refs)?; + + let kinds_value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + let localspluskinds = bag + .bytes_from_value(&kinds_value) + .ok_or(MarshalError::BadType)?; + + let read_string = + |rdr: &mut R, refs: &mut Vec>| -> Result { + let value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + bag.str_from_value(&value).ok_or(MarshalError::BadType) + }; + let source_path_raw = read_string(rdr, refs)?; + let obj_name_raw = read_string(rdr, refs)?; + let qualname_raw = read_string(rdr, refs)?; + + let first_line_raw = rdr.read_u32()? as i32; + let first_line_number = if first_line_raw > 0 { + OneIndexed::new(first_line_raw as usize) + } else { + None + }; + let linetable_value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + let linetable = bag + .bytes_from_value(&linetable_value) + .ok_or(MarshalError::BadType)? + .into_boxed_slice(); + let exceptiontable_value = deserialize_value_depth(rdr, bag, child_depth, refs)?; + let exceptiontable = bag + .bytes_from_value(&exceptiontable_value) + .ok_or(MarshalError::BadType)? + .into_boxed_slice(); + + let lp = split_localplus( + &localsplusnames + .iter() + .map(|s| s.as_str()) + .collect::>(), + &localspluskinds, + arg_count, + kwonlyarg_count, + flags, + )?; + let instructions = CodeUnits::try_from(code_bytes.as_slice())?; + let locations = linetable_to_locations(&linetable, first_line_raw, instructions.len()); + let constant_bag = bag.constant_bag(); + let code = CodeObject { + instructions, + locations, + flags, + posonlyarg_count, + arg_count, + kwonlyarg_count, + source_path: constant_bag.make_name(&source_path_raw), + first_line_number, + max_stackdepth, + obj_name: constant_bag.make_name(&obj_name_raw), + qualname: constant_bag.make_name(&qualname_raw), + constants, + names: names_raw + .iter() + .map(|name| constant_bag.make_name(name)) + .collect(), + varnames: lp + .varnames + .iter() + .map(|name| constant_bag.make_name(name)) + .collect(), + cellvars: lp + .cellvars + .iter() + .map(|name| constant_bag.make_name(name)) + .collect(), + freevars: lp + .freevars + .iter() + .map(|name| constant_bag.make_name(name)) + .collect(), + localspluskinds: localspluskinds.into_boxed_slice(), + linetable, + exceptiontable, + }; + bag.make_code_with_constants(code, constant_values) +} + fn deserialize_value_typed( rdr: &mut R, bag: Bag, depth: usize, refs: &mut Vec>, typ: Type, + slot: Option, ) -> Result { if depth == 0 { return Err(MarshalError::InvalidBytecode); @@ -806,71 +1052,141 @@ fn deserialize_value_typed( let value = Complex64 { re, im }; bag.make_complex(value) } - Type::Ascii | Type::AsciiInterned | Type::Unicode | Type::Interned => { - let len = rdr.read_u32()?; - let value = rdr.read_wtf8(len)?; + Type::Ascii | Type::Unicode => { + let len = rdr.read_len("string")?; + let value = rdr.read_wtf8(len as u32)?; bag.make_str(value) } - Type::ShortAscii | Type::ShortAsciiInterned => { + Type::AsciiInterned | Type::Interned => { + let len = rdr.read_len("string")?; + let value = rdr.read_wtf8(len as u32)?; + bag.make_interned_str(value) + } + Type::ShortAscii => { let len = rdr.read_u8()? as u32; let value = rdr.read_wtf8(len)?; bag.make_str(value) } + Type::ShortAsciiInterned => { + let len = rdr.read_u8()? as u32; + let value = rdr.read_wtf8(len)?; + bag.make_interned_str(value) + } Type::SmallTuple => { let len = rdr.read_u8()? as usize; let d = depth - 1; - let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); - itertools::process_results(it, |it| bag.make_tuple(it))? + if let Some(index) = slot + && let Some(tuple) = bag.make_tuple_placeholder(len)? + { + refs[index] = Some(tuple.clone()); + for item_index in 0..len { + let item = deserialize_value_depth(rdr, bag, d, refs)?; + bag.set_tuple_item(&tuple, item_index, item)?; + } + tuple + } else { + let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); + itertools::process_results(it, |it| bag.make_tuple(it))? + } } Type::Null => { - return Err(MarshalError::BadType); + return Err(MarshalError::NullObject); } Type::Ref => { // Handled in deserialize_value_depth before calling this function return Err(MarshalError::BadType); } Type::Tuple => { - let len = rdr.read_u32()?; + let len = rdr.read_len("tuple")?; let d = depth - 1; - let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); - itertools::process_results(it, |it| bag.make_tuple(it))? + if let Some(index) = slot + && let Some(tuple) = bag.make_tuple_placeholder(len)? + { + refs[index] = Some(tuple.clone()); + for item_index in 0..len { + let item = deserialize_value_depth(rdr, bag, d, refs)?; + bag.set_tuple_item(&tuple, item_index, item)?; + } + tuple + } else { + let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); + itertools::process_results(it, |it| bag.make_tuple(it))? + } } Type::List => { - let len = rdr.read_u32()?; + let len = rdr.read_len("list")?; let d = depth - 1; - let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); - itertools::process_results(it, |it| bag.make_list(it))?? + if let Some(index) = slot + && let Some(list) = bag.make_list_placeholder(len)? + { + refs[index] = Some(list.clone()); + for item_index in 0..len { + let item = deserialize_value_depth(rdr, bag, d, refs)?; + bag.set_list_item(&list, item_index, item)?; + } + list + } else { + let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); + itertools::process_results(it, |it| bag.make_list(it))?? + } } Type::Set => { - let len = rdr.read_u32()?; + let len = rdr.read_len("set")?; let d = depth - 1; - let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); - itertools::process_results(it, |it| bag.make_set(it))?? + if let Some(index) = slot + && let Some(set) = bag.make_set_placeholder() + { + refs[index] = Some(set.clone()); + for _ in 0..len { + let item = deserialize_value_depth(rdr, bag, d, refs)?; + bag.insert_set_item(&set, item)?; + } + set + } else { + let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); + itertools::process_results(it, |it| bag.make_set(it))?? + } } Type::FrozenSet => { - let len = rdr.read_u32()?; + let len = rdr.read_len("set")?; let d = depth - 1; let it = (0..len).map(|_| deserialize_value_depth(rdr, bag, d, refs)); itertools::process_results(it, |it| bag.make_frozenset(it))?? } Type::Dict => { let d = depth - 1; - let mut pairs = Vec::new(); - loop { - let raw = rdr.read_u8()?; - if raw & !FLAG_REF == b'0' { - break; + if let Some(index) = slot + && let Some(dict) = bag.make_dict_placeholder() + { + refs[index] = Some(dict.clone()); + loop { + let raw = rdr.read_u8()?; + if raw & !FLAG_REF == b'0' { + break; + } + let key = deserialize_value_after_header(rdr, bag, d, refs, raw)?; + let value = deserialize_value_depth(rdr, bag, d, refs)?; + bag.insert_dict_item(&dict, key, value)?; } - let k = deserialize_value_after_header(rdr, bag, d, refs, raw)?; - let v = deserialize_value_depth(rdr, bag, d, refs)?; - pairs.push((k, v)); + dict + } else { + let mut pairs = Vec::new(); + loop { + let raw = rdr.read_u8()?; + if raw & !FLAG_REF == b'0' { + break; + } + let key = deserialize_value_after_header(rdr, bag, d, refs, raw)?; + let value = deserialize_value_depth(rdr, bag, d, refs)?; + pairs.push((key, value)); + } + bag.make_dict(pairs.into_iter())? } - bag.make_dict(pairs.into_iter())? } Type::Bytes => { // After marshaling, byte arrays are converted into bytes. - let len = rdr.read_u32()?; - let value = rdr.read_slice(len)?; + let len = rdr.read_len("bytes object")?; + let value = rdr.read_slice(len as u32)?; bag.make_bytes(value) } Type::Code => return Err(MarshalError::BadType), @@ -1102,6 +1418,25 @@ pub fn serialize_value( /// Split varnames/cellvars/freevars are reassembled into /// co_localsplusnames/co_localspluskinds. pub fn serialize_code(buf: &mut W, code: &CodeObject) { + serialize_code_with(buf, code, |buf, constant| { + serialize_value(buf, constant.borrow_constant().into()).unwrap_or_else(|x| match x {}); + Ok::<(), core::convert::Infallible>(()) + }) + .unwrap_or_else(|x| match x {}) +} + +/// Serialize a code object, writing each `co_consts` entry through +/// `write_constant`. +/// +/// A runtime caller passes its own object writer so that values its constant +/// representation carries but `BorrowedConstant` cannot describe — lists, +/// dicts, sets — reach the stream, and so a constant shared with the enclosing +/// object keeps its entry in that writer's reference table. +pub fn serialize_code_with( + buf: &mut W, + code: &CodeObject, + mut write_constant: impl FnMut(&mut W, &C) -> core::result::Result<(), E>, +) -> core::result::Result<(), E> { // 1–5: scalar fields buf.write_u32(code.arg_count); buf.write_u32(code.posonlyarg_count); @@ -1118,7 +1453,7 @@ pub fn serialize_code(buf: &mut W, code: &CodeObject) buf.write_u8(Type::Tuple as u8); write_len(buf, code.constants.len()); for constant in &*code.constants { - serialize_value(buf, constant.borrow_constant().into()).unwrap_or_else(|x| match x {}) + write_constant(buf, constant)?; } // 8: co_names (tuple of strings) @@ -1161,6 +1496,7 @@ pub fn serialize_code(buf: &mut W, code: &CodeObject) // 16: co_exceptiontable buf.write_u8(Type::Bytes as u8); write_vec(buf, &code.exceptiontable); + Ok(()) } fn write_marshal_str(buf: &mut W, s: &str) { diff --git a/crates/compiler/src/lib.rs b/crates/compiler/src/lib.rs index 9c0884c7520..7562e8939b9 100644 --- a/crates/compiler/src/lib.rs +++ b/crates/compiler/src/lib.rs @@ -1,10 +1,9 @@ -pub use ruff_python_ast::token::TokenKind; -use ruff_python_parser::{LexicalErrorType, ParseErrorType}; +pub use ruff_python_ast::token::{TokenKind, Tokens}; +use ruff_python_parser::ParseErrorType; use ruff_source_file::{PositionEncoding, SourceFile, SourceFileBuilder, SourceLocation}; -use ruff_text_size::TextSlice; -use thiserror::Error; - +use ruff_text_size::{Ranged, TextSize, TextSlice}; use rustpython_codegen::{compile, symboltable}; +use thiserror::Error; pub use rustpython_codegen::compile::CompileOpts; pub use rustpython_compiler_core::{Mode, bytecode::CodeObject}; @@ -15,230 +14,5057 @@ pub use ruff_python_parser as parser; pub use rustpython_codegen as codegen; pub use rustpython_compiler_core as core; -#[derive(Error, Debug)] -pub enum CompileErrorType { - #[error(transparent)] - Codegen(#[from] codegen::error::CodegenErrorType), - #[error(transparent)] - Parse(#[from] ParseErrorType), +#[derive(Error, Debug)] +pub enum CompileErrorType { + #[error(transparent)] + Codegen(#[from] codegen::error::CodegenErrorType), + #[error(transparent)] + Parse(#[from] ParseErrorType), +} + +#[derive(Error, Debug)] +pub struct ParseError { + #[source] + pub error: ParseErrorType, + pub raw_location: ruff_text_size::TextRange, + pub location: SourceLocation, + pub end_location: SourceLocation, + pub source_path: String, + /// Set when the error is an unclosed bracket (converted from EOF). + pub is_unclosed_bracket: bool, +} + +impl ::core::fmt::Display for ParseError { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + self.error.fmt(f) + } +} + +#[derive(Error, Debug)] +pub enum CompileError { + #[error(transparent)] + Codegen(#[from] codegen::error::CodegenError), + #[error(transparent)] + Parse(#[from] ParseError), +} + +impl CompileError { + #[must_use] + pub fn from_ruff_parse_error( + error: parser::ParseError, + source_file: &SourceFile, + mode: Mode, + ) -> Self { + let raw_location = error.location; + let diagnostic = match cpython_parse_diagnostic_override(&error, source_file, mode) { + Some(diagnostic) => diagnostic, + None => default_parse_diagnostic(error, source_file), + }; + + Self::Parse(ParseError { + error: diagnostic.error, + raw_location, + location: diagnostic.location, + end_location: diagnostic.end_location, + source_path: source_file.name().to_owned(), + is_unclosed_bracket: diagnostic.is_unclosed_bracket, + }) + } + + fn from_source_error( + source_file: &SourceFile, + message: String, + start: usize, + end: usize, + ) -> Self { + let start = TextSize::new(start as u32); + let end = TextSize::new(end as u32); + let (location, end_location) = source_locations(source_file, start, end); + Self::Parse(ParseError { + error: parser::ParseErrorType::OtherError(message), + raw_location: ruff_text_size::TextRange::new(start, end), + location, + end_location, + source_path: source_file.name().to_owned(), + is_unclosed_bracket: false, + }) + } + + #[must_use] + pub const fn location(&self) -> Option { + match self { + Self::Codegen(codegen_error) => codegen_error.location, + Self::Parse(parse_error) => Some(parse_error.location), + } + } + + #[must_use] + pub const fn python_location(&self) -> (usize, usize) { + if let Some(location) = self.location() { + (location.line.get(), location.character_offset.get()) + } else { + (0, 0) + } + } + + #[must_use] + pub fn python_end_location(&self) -> Option<(usize, usize)> { + match self { + Self::Codegen(_) => None, + Self::Parse(parse_error) => Some(( + parse_error.end_location.line.get(), + parse_error.end_location.character_offset.get(), + )), + } + } + + #[must_use] + pub fn source_path(&self) -> &str { + match self { + Self::Codegen(codegen_error) => &codegen_error.source_path, + Self::Parse(parse_error) => &parse_error.source_path, + } + } +} + +fn source_location(source_file: &SourceFile, offset: TextSize) -> SourceLocation { + source_file + .to_source_code() + .source_location(offset, PositionEncoding::Utf8) +} + +// Call only with UTF-8 character boundaries for Python-facing offsets. +fn source_location_in_code_points(source_file: &SourceFile, offset: TextSize) -> SourceLocation { + source_file + .to_source_code() + .source_location(offset, PositionEncoding::Utf32) +} + +fn source_locations( + source_file: &SourceFile, + start: TextSize, + end: TextSize, +) -> (SourceLocation, SourceLocation) { + let source_code = source_file.to_source_code(); + ( + source_code.source_location(start, PositionEncoding::Utf8), + source_code.source_location(end, PositionEncoding::Utf8), + ) +} + +struct NormalizedParseDiagnostic { + error: parser::ParseErrorType, + location: SourceLocation, + end_location: SourceLocation, + is_unclosed_bracket: bool, +} + +impl NormalizedParseDiagnostic { + const fn new( + error: parser::ParseErrorType, + location: SourceLocation, + end_location: SourceLocation, + ) -> Self { + Self { + error, + location, + end_location, + is_unclosed_bracket: false, + } + } + + fn other(source_file: &SourceFile, message: String, start: usize, end: usize) -> Self { + let (location, end_location) = source_locations( + source_file, + TextSize::new(start as u32), + TextSize::new(end as u32), + ); + Self::new( + parser::ParseErrorType::OtherError(message), + location, + end_location, + ) + } + + fn other_in_code_points( + source_file: &SourceFile, + message: String, + start: usize, + end: usize, + ) -> Self { + let start = TextSize::new(start as u32); + let end = TextSize::new(end as u32); + Self::new( + parser::ParseErrorType::OtherError(message), + source_location_in_code_points(source_file, start), + source_location_in_code_points(source_file, end), + ) + } + + const fn with_unclosed_bracket(mut self, is_unclosed_bracket: bool) -> Self { + self.is_unclosed_bracket = is_unclosed_bracket; + self + } +} + +fn cpython_parse_diagnostic_override( + error: &parser::ParseError, + source_file: &SourceFile, + mode: Mode, +) -> Option { + let source_text = source_file.source_text(); + + macro_rules! source_error { + ($expr:expr) => { + if let Some((message, start, end)) = $expr { + return Some(NormalizedParseDiagnostic::other( + source_file, + message, + start, + end, + )); + } + }; + } + + if let Some((message, offset)) = invalid_number_literal_error(source_text) { + return Some(NormalizedParseDiagnostic::other( + source_file, + message, + offset, + offset, + )); + } + source_error!(invalid_legacy_statement_error(source_text)); + source_error!(non_printable_character_error(source_text)); + source_error!(invalid_interpolated_string_error(source_text)); + + if let Some((message, start, end, unclosed)) = bracket_syntax_error(source_text) { + return Some( + NormalizedParseDiagnostic::other(source_file, message, start, end) + .with_unclosed_bracket(unclosed), + ); + } + + if matches!( + &error.error, + parser::ParseErrorType::Lexical(parser::LexicalErrorType::LineContinuationError) + ) { + // Only a backslash at the end of the source is an EOF error. + let terminal_backslash = source_text.len().checked_sub(1); + if !matches!(mode, Mode::Eval) + && terminal_backslash == Some(error.location.start().to_usize()) + { + let loc = source_line_end_location(source_file, error.location.start()); + return Some(NormalizedParseDiagnostic::new( + parser::ParseErrorType::OtherError("unexpected EOF while parsing".to_owned()), + loc, + loc, + )); + } + let loc = source_location(source_file, error.location.start() + TextSize::from(1)); + return Some(NormalizedParseDiagnostic::new( + error.error.clone(), + loc, + loc, + )); + } + + if let Some((message, start, end)) = unterminated_string_error(source_text) { + // The scanner reports quote positions, which are UTF-8 character boundaries. + return Some(NormalizedParseDiagnostic::other_in_code_points( + source_file, + message, + start, + end, + )); + } + source_error!(expected_indented_block_error(error, source_text)); + + if matches!( + &error.error, + parser::ParseErrorType::Lexical(parser::LexicalErrorType::Eof) + ) { + return Some(eof_parse_diagnostic(error, source_file)); + } + + source_error!(invalid_type_param_error(source_text)); + source_error!(invalid_comprehension_error(source_text)); + source_error!(invalid_parameter_star_annotation_error(source_text)); + source_error!(invalid_parameter_list_error(source_text)); + source_error!(invalid_call_argument_error(source_text)); + + if is_missing_comma_between_literals(error) { + let (loc, end_loc) = adjusted_error_locations(source_file, error.location); + let msg = "invalid syntax. Perhaps you forgot a comma?".into(); + return Some(NormalizedParseDiagnostic::new( + parser::ParseErrorType::OtherError(msg), + loc, + end_loc, + )); + } + + source_error!(invalid_dict_error(source_text)); + source_error!(invalid_collection_assignment_error(source_text)); + source_error!(invalid_group_error(source_text)); + source_error!(invalid_def_type_params_error(source_text)); + source_error!(invalid_expression_error(source_text)); + source_error!(invalid_named_expression_error(source_text)); + source_error!(invalid_plain_assignment_error(source_text)); + source_error!(expression_assignment_error(source_text)); + source_error!(invalid_annotation_target_error(source_text)); + source_error!(invalid_assignment_target_error(source_text)); + source_error!(invalid_augassign_target_error(source_text)); + source_error!(invalid_for_target_error(source_text)); + source_error!(invalid_with_target_error(source_text)); + source_error!(invalid_delete_target_error(source_text)); + source_error!(invalid_standalone_except_error(source_text)); + source_error!(invalid_import_statement_error(source_text)); + source_error!(invalid_import_target_error(source_text)); + source_error!(invalid_except_as_target_error(source_text)); + source_error!(invalid_match_mapping_rest_wildcard_error(source_text)); + source_error!(invalid_match_as_target_error(source_text)); + source_error!(invalid_for_if_clause_error(source_text)); + source_error!(invalid_if_expression_statement_error(source_text)); + source_error!(invalid_else_elif_error(source_text)); + source_error!(mixed_except_handlers_error(source_text)); + + if matches!( + &error.error, + parser::ParseErrorType::Lexical(parser::LexicalErrorType::IndentationError) + ) { + let end_loc = source_line_end_location(source_file, error.location.start()); + return Some(NormalizedParseDiagnostic::new( + error.error.clone(), + end_loc, + end_loc, + )); + } + + if matches!( + &error.error, + parser::ParseErrorType::InvalidAssignmentTarget + ) { + return Some(invalid_assignment_target_diagnostic(error, source_file)); + } + + if matches!( + &error.error, + parser::ParseErrorType::InvalidNamedAssignmentTarget + ) { + let (loc, end_loc) = adjusted_error_locations(source_file, error.location); + let target = source_file.source_text().slice(error.location); + let msg = format!("cannot use assignment expressions with {target}"); + return Some(NormalizedParseDiagnostic::new( + parser::ParseErrorType::OtherError(msg), + loc, + end_loc, + )); + } + + None +} + +fn eof_parse_diagnostic( + error: &parser::ParseError, + source_file: &SourceFile, +) -> NormalizedParseDiagnostic { + let source_text = source_file.source_text(); + if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) { + let loc = source_location(source_file, TextSize::new(bracket_offset as u32)); + let end_loc = SourceLocation { + line: loc.line, + character_offset: loc.character_offset.saturating_add(1), + }; + let msg = format!("'{bracket_char}' was never closed"); + NormalizedParseDiagnostic::new(parser::ParseErrorType::OtherError(msg), loc, end_loc) + .with_unclosed_bracket(true) + } else { + let end_loc = source_line_end_location(source_file, error.location.start()); + NormalizedParseDiagnostic::new(error.error.clone(), end_loc, end_loc) + } +} + +fn invalid_assignment_target_diagnostic( + error: &parser::ParseError, + source_file: &SourceFile, +) -> NormalizedParseDiagnostic { + let (loc, end_loc) = adjusted_error_locations(source_file, error.location); + let expr_str = source_file.source_text().slice(error.location); + + let msg = parser::parse_expression(expr_str).map_or_else( + |_| match expr_str { + "yield" => "assignment to yield expression not possible".into(), + _ => format!("cannot assign to {expr_str}"), + }, + |parsed| match *parsed.syntax().body { + ast::Expr::Call(_) => "cannot assign to function call".into(), + ast::Expr::BinOp(_) => "cannot assign to expression".into(), + ast::Expr::If(_) => "cannot assign to conditional expression".into(), + ast::Expr::Generator(_) => "cannot assign to generator expression".into(), + ast::Expr::FString(_) => "invalid syntax".into(), + ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::NumberLiteral(_) => { + "cannot assign to literal here. Maybe you meant '==' instead of '='?".into() + } + ast::Expr::EllipsisLiteral(_) => { + "cannot assign to ellipsis here. Maybe you meant '==' instead of '='?".into() + } + _ => format!("cannot assign to {expr_str}"), + }, + ); + + NormalizedParseDiagnostic::new(parser::ParseErrorType::OtherError(msg), loc, end_loc) +} + +fn default_parse_diagnostic( + error: parser::ParseError, + source_file: &SourceFile, +) -> NormalizedParseDiagnostic { + let (loc, end_loc) = adjusted_error_locations(source_file, error.location); + NormalizedParseDiagnostic::new(error.error, loc, end_loc) +} + +fn adjusted_error_locations( + source_file: &SourceFile, + range: ruff_text_size::TextRange, +) -> (SourceLocation, SourceLocation) { + let mut locations = source_locations(source_file, range.start(), range.end()); + if locations.1.character_offset.get() == 1 && locations.1.line > locations.0.line { + locations.1 = source_location(source_file, range.end() - TextSize::from(1)); + locations.1.character_offset = locations.1.character_offset.saturating_add(1); + } + locations +} + +fn source_line_end_location(source_file: &SourceFile, offset: TextSize) -> SourceLocation { + let loc = source_location(source_file, offset); + let line_idx = loc.line.to_zero_indexed(); + let line = source_file + .source_text() + .split('\n') + .nth(line_idx) + .unwrap_or(""); + let line_end_col = line.chars().count() + 1; + SourceLocation { + line: loc.line, + character_offset: ruff_source_file::OneIndexed::new(line_end_col) + .unwrap_or(loc.character_offset), + } +} + +fn is_missing_comma_between_literals(error: &parser::ParseError) -> bool { + matches!( + &error.error, + parser::ParseErrorType::ExpectedToken { expected, found } + if matches!((expected, found), (TokenKind::Comma, TokenKind::Int)) + ) +} + +fn is_ascii_identifier_char(byte: u8) -> bool { + byte == b'_' || byte.is_ascii_alphanumeric() +} + +fn numeric_keyword_suffix(rest: &[u8]) -> bool { + rest.starts_with(b"and") + || rest.starts_with(b"else") + || rest.starts_with(b"for") + || rest.starts_with(b"if") + || rest.starts_with(b"in") + || rest.starts_with(b"is") + || rest.starts_with(b"or") + || rest.starts_with(b"not") +} + +fn consume_decimal_digits(bytes: &[u8], mut index: usize) -> usize { + while index < bytes.len() { + match bytes[index] { + b'0'..=b'9' => index += 1, + b'_' if bytes + .get(index + 1) + .is_some_and(|byte| byte.is_ascii_digit()) => + { + index += 2; + } + _ => break, + } + } + index +} + +fn consume_radix_digits(bytes: &[u8], mut index: usize, is_digit: impl Fn(u8) -> bool) -> usize { + while index < bytes.len() { + if is_digit(bytes[index]) { + index += 1; + } else if bytes.get(index) == Some(&b'_') + && bytes.get(index + 1).is_some_and(|&byte| is_digit(byte)) + { + index += 2; + } else { + break; + } + } + index +} + +fn invalid_radix_literal_error( + bytes: &[u8], + start: usize, + kind: &'static str, + is_digit: impl Fn(u8) -> bool, +) -> Option<(String, usize)> { + let mut index = start + 2; + let mut has_digit = false; + loop { + let Some(&byte) = bytes.get(index) else { + return Some((format!("invalid {kind} literal"), start + 1)); + }; + if byte == b'_' { + let Some(&next) = bytes.get(index + 1) else { + return Some((format!("invalid {kind} literal"), index)); + }; + if is_digit(next) { + has_digit = true; + index += 2; + continue; + } + if next.is_ascii_digit() && matches!(kind, "binary" | "octal") { + return Some(( + format!("invalid digit '{}' in {kind} literal", next as char), + index + 1, + )); + } + return Some((format!("invalid {kind} literal"), index)); + } + if is_digit(byte) { + has_digit = true; + index += 1; + continue; + } + if byte.is_ascii_digit() && matches!(kind, "binary" | "octal") { + return Some(( + format!("invalid digit '{}' in {kind} literal", byte as char), + index, + )); + } + if has_digit { + return None; + } + return Some((format!("invalid {kind} literal"), start + 1)); + } +} + +fn decimal_tail_error(bytes: &[u8], mut index: usize) -> Option { + loop { + while bytes.get(index).is_some_and(|byte| byte.is_ascii_digit()) { + index += 1; + } + if bytes.get(index) != Some(&b'_') { + return None; + } + let underscore = index; + index += 1; + if !bytes.get(index).is_some_and(|byte| byte.is_ascii_digit()) { + return Some(underscore); + } + } +} + +fn decimal_tail_end(bytes: &[u8], mut index: usize) -> usize { + loop { + while bytes.get(index).is_some_and(|byte| byte.is_ascii_digit()) { + index += 1; + } + if bytes.get(index) == Some(&b'_') + && bytes + .get(index + 1) + .is_some_and(|byte| byte.is_ascii_digit()) + { + index += 2; + } else { + return index; + } + } +} + +fn invalid_decimal_literal_error(bytes: &[u8], start: usize) -> Option<(String, usize)> { + if bytes.get(start) == Some(&b'.') { + return None; + } + let message = "invalid decimal literal".to_owned(); + if let Some(offset) = decimal_tail_error(bytes, start) { + return Some((message, offset)); + } + + let mut index = decimal_tail_end(bytes, start); + if bytes.get(index) == Some(&b'.') { + if bytes.get(index + 1) == Some(&b'_') { + return Some((message, index)); + } + if let Some(offset) = decimal_tail_error(bytes, index + 1) { + return Some((message, offset)); + } + index = decimal_tail_end(bytes, index + 1); + } + if matches!(bytes.get(index), Some(b'e' | b'E')) { + let exponent = index; + index += 1; + let sign = if matches!(bytes.get(index), Some(b'+' | b'-')) { + let sign = index; + index += 1; + Some(sign) + } else { + None + }; + if !bytes.get(index).is_some_and(|byte| byte.is_ascii_digit()) { + return Some((message, sign.unwrap_or(exponent))); + } + if let Some(offset) = decimal_tail_error(bytes, index) { + return Some((message, offset)); + } + } + None +} + +fn leading_zero_decimal_literal_error(bytes: &[u8], start: usize) -> Option<(String, usize)> { + if bytes.get(start) != Some(&b'0') { + return None; + } + let mut index = start; + loop { + match bytes.get(index) { + Some(b'0') => index += 1, + Some(b'_') + if bytes + .get(index + 1) + .is_some_and(|byte| byte.is_ascii_digit()) => + { + index += 1; + } + _ => break, + } + } + if bytes.get(index).is_some_and(|byte| byte.is_ascii_digit()) { + let after_digits = decimal_tail_end(bytes, index); + if !matches!( + bytes.get(after_digits), + Some(b'.' | b'e' | b'E' | b'j' | b'J') + ) { + return Some(( + "leading zeros in decimal integer literals are not permitted; use an 0o prefix for octal integers".to_owned(), + start, + )); + } + } + None +} + +fn invalid_numeric_literal_error(bytes: &[u8], start: usize) -> Option<(String, usize)> { + if bytes.get(start) == Some(&b'0') { + match bytes.get(start + 1) { + Some(b'x' | b'X') => { + return invalid_radix_literal_error(bytes, start, "hexadecimal", |byte| { + byte.is_ascii_hexdigit() + }); + } + Some(b'o' | b'O') => { + return invalid_radix_literal_error(bytes, start, "octal", |byte| { + matches!(byte, b'0'..=b'7') + }); + } + Some(b'b' | b'B') => { + return invalid_radix_literal_error(bytes, start, "binary", |byte| { + matches!(byte, b'0' | b'1') + }); + } + _ => {} + } + if let Some(err) = leading_zero_decimal_literal_error(bytes, start) { + return Some(err); + } + } + invalid_decimal_literal_error(bytes, start) +} + +fn consume_exponent(bytes: &[u8], index: usize) -> usize { + if !matches!(bytes.get(index), Some(b'e' | b'E')) { + return index; + } + let mut cursor = index + 1; + if matches!(bytes.get(cursor), Some(b'+' | b'-')) { + cursor += 1; + } + if bytes.get(cursor).is_some_and(|byte| byte.is_ascii_digit()) { + consume_decimal_digits(bytes, cursor) + } else { + index + } +} + +fn number_literal_end(bytes: &[u8], start: usize) -> Option<(&'static str, usize)> { + if bytes.get(start) == Some(&b'.') { + if !bytes + .get(start + 1) + .is_some_and(|byte| byte.is_ascii_digit()) + { + return None; + } + let mut index = consume_decimal_digits(bytes, start + 1); + index = consume_exponent(bytes, index); + if matches!(bytes.get(index), Some(b'j' | b'J')) { + return Some(("imaginary", index + 1)); + } + return Some(("decimal", index)); + } + + if !bytes.get(start).is_some_and(|byte| byte.is_ascii_digit()) { + return None; + } + + if bytes.get(start) == Some(&b'0') { + match bytes.get(start + 1) { + Some(b'x' | b'X') => { + let end = consume_radix_digits(bytes, start + 2, |byte| byte.is_ascii_hexdigit()); + return Some(("hexadecimal", end)); + } + Some(b'o' | b'O') => { + let end = + consume_radix_digits(bytes, start + 2, |byte| matches!(byte, b'0'..=b'7')); + return Some(("octal", end)); + } + Some(b'b' | b'B') => { + let end = + consume_radix_digits(bytes, start + 2, |byte| matches!(byte, b'0' | b'1')); + return Some(("binary", end)); + } + _ => {} + } + } + + let mut index = consume_decimal_digits(bytes, start); + if bytes.get(index) == Some(&b'.') { + index = consume_decimal_digits(bytes, index + 1); + } + index = consume_exponent(bytes, index); + if matches!(bytes.get(index), Some(b'j' | b'J')) { + return Some(("imaginary", index + 1)); + } + Some(("decimal", index)) +} + +fn skip_quoted_string(bytes: &[u8], mut index: usize) -> usize { + let quote = bytes[index]; + let triple = bytes.get(index + 1) == Some("e) && bytes.get(index + 2) == Some("e); + let quote_len = if triple { 3 } else { 1 }; + index += quote_len; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else if triple + && bytes.get(index) == Some("e) + && bytes.get(index + 1) == Some("e) + && bytes.get(index + 2) == Some("e) + { + return index + 3; + } else if !triple && bytes[index] == quote { + return index + 1; + } else { + index += 1; + } + } + index +} + +fn invalid_number_literal_error(source: &str) -> Option<(String, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + byte if byte >= 0x80 || byte == b'_' || byte.is_ascii_alphabetic() => { + index += 1; + while index < bytes.len() + && (bytes[index] >= 0x80 || is_ascii_identifier_char(bytes[index])) + { + index += 1; + } + } + b'.' | b'0'..=b'9' => { + if let Some(err) = invalid_numeric_literal_error(bytes, index) { + return Some(err); + } + let Some((kind, end)) = number_literal_end(bytes, index) else { + index += 1; + continue; + }; + if end > index { + if source[end..].starts_with('⁄') { + return Some(("invalid character '⁄' (U+2044)".to_owned(), end)); + } + if bytes + .get(end) + .is_some_and(|byte| *byte < 128 && is_ascii_identifier_char(*byte)) + && !numeric_keyword_suffix(&bytes[end..]) + { + return Some((format!("invalid {kind} literal"), end.saturating_sub(1))); + } + } + index = end.max(index + 1); + } + _ => index += 1, + } + } + None +} + +fn cpython_indented_block_clause(message: &str) -> Option<&'static str> { + let clause = message.strip_prefix("Expected an indented block after ")?; + Some(match clause { + "`if` statement" => "'if' statement", + "`elif` clause" => "'elif' statement", + "`else` clause" => "'else' statement", + "`for` statement" => "'for' statement", + "`with` statement" => "'with' statement", + "`while` statement" => "'while' statement", + "`try` statement" => "'try' statement", + "`except` clause" => "'except' statement", + "`finally` clause" => "'finally' statement", + "`match` statement" => "'match' statement", + "`case` block" => "'case' statement", + "`class` definition" => "class definition", + "function definition" => "function definition", + _ => return None, + }) +} + +fn previous_non_empty_line_number(source: &str, offset: usize) -> Option { + let bytes = source.as_bytes(); + let mut index = offset.min(bytes.len()); + while index > 0 { + let line_end = index; + while index > 0 && bytes[index - 1] != b'\n' { + index -= 1; + } + let line_start = index; + let content_start = skip_horizontal_whitespace(bytes, line_start); + let mut content_end = line_end; + while content_end > content_start + && matches!( + bytes.get(content_end - 1), + Some(b' ' | b'\t' | b'\r' | b'\x0c') + ) + { + content_end -= 1; + } + if content_start < content_end { + return Some( + source[..line_start] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1, + ); + } + index = line_start.saturating_sub(1); + } + None +} + +fn expected_indented_block_error( + error: &parser::ParseError, + source: &str, +) -> Option<(String, usize, usize)> { + let parser::ParseErrorType::OtherError(message) = &error.error else { + return None; + }; + let mut clause = cpython_indented_block_clause(message)?; + let start = error.location.start().to_usize(); + let end = error.location.end().to_usize(); + let line = previous_non_empty_line_number(source, start)?; + if clause == "'except' statement" + && let Some(previous_line) = previous_non_empty_line(source, start) + && matches!( + previous_line.trim_start(), + line if line.starts_with("except*") || line.starts_with("except *") + ) + { + clause = "'except*' statement"; + } + Some(( + format!("expected an indented block after {clause} on line {line}"), + start, + end, + )) +} + +fn previous_non_empty_line(source: &str, offset: usize) -> Option<&str> { + let bytes = source.as_bytes(); + let mut index = offset.min(bytes.len()); + while index > 0 { + let line_end = index; + while index > 0 && bytes[index - 1] != b'\n' { + index -= 1; + } + let line_start = index; + let mut content_start = line_start; + while content_start < line_end + && matches!(bytes[content_start], b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') + { + content_start += 1; + } + let mut content_end = line_end; + while content_end > content_start + && matches!(bytes[content_end - 1], b' ' | b'\t' | b'\r' | b'\x0c') + { + content_end -= 1; + } + if content_start < content_end { + return source.get(line_start..line_end); + } + index = line_start.saturating_sub(1); + } + None +} + +fn starts_identifier(bytes: &[u8], index: usize, word: &[u8]) -> bool { + bytes.get(index..index + word.len()) == Some(word) + && index + .checked_sub(1) + .and_then(|before| bytes.get(before)) + .is_none_or(|byte| !is_ascii_identifier_char(*byte)) + && bytes + .get(index + word.len()) + .is_none_or(|byte| !is_ascii_identifier_char(*byte)) +} + +fn is_plain_assignment_operator(bytes: &[u8], index: usize) -> bool { + bytes.get(index) == Some(&b'=') + && bytes.get(index + 1) != Some(&b'=') + && !matches!( + index.checked_sub(1).and_then(|before| bytes.get(before)), + Some(b'=' | b'!' | b'<' | b'>' | b':') + ) +} + +fn is_simple_keyword_name(bytes: &[u8], mut start: usize, mut end: usize) -> bool { + while matches!( + bytes.get(start), + Some(b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') + ) { + start += 1; + } + while end > start + && matches!( + bytes.get(end - 1), + Some(b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') + ) + { + end -= 1; + } + let Some(&first) = bytes.get(start) else { + return false; + }; + if !(first == b'_' || first.is_ascii_alphabetic() || first >= 0x80) { + return false; + } + let mut index = start + 1; + while index < end { + if bytes[index] < 0x80 && !is_ascii_identifier_char(bytes[index]) { + return false; + } + index += 1; + } + true +} + +fn is_function_parameter_list(bytes: &[u8], paren: usize) -> bool { + let mut cursor = paren; + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + if cursor > 0 && bytes.get(cursor - 1) == Some(&b']') { + let mut bracket = cursor; + let mut level = 0usize; + while bracket > 0 { + bracket -= 1; + match bytes[bracket] { + b']' => level += 1, + b'[' => { + level = level.saturating_sub(1); + if level == 0 { + cursor = bracket; + break; + } + } + _ => {} + } + } + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + } + while cursor > 0 + && bytes + .get(cursor - 1) + .is_some_and(|byte| *byte >= 0x80 || is_ascii_identifier_char(*byte)) + { + cursor -= 1; + } + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + cursor >= 3 + && starts_identifier(bytes, cursor - 3, b"def") + && cursor + .checked_sub(4) + .and_then(|before| bytes.get(before)) + .is_none_or(|byte| !is_ascii_identifier_char(*byte)) +} + +#[derive(Clone, Copy)] +enum ParameterListKind { + Function, + Lambda, +} + +fn matching_delimiter(bytes: &[u8], open: usize, close: u8) -> Option { + let mut index = open; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + byte if byte == close => { + level = level.saturating_sub(1); + if level == 0 { + return Some(index); + } + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ => index += 1, + } + } + None +} + +fn find_lambda_parameter_end(bytes: &[u8], mut index: usize) -> Option { + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b':' if level == 0 => return Some(index), + _ => index += 1, + } + } + None +} + +fn top_level_byte(bytes: &[u8], mut index: usize, end: usize, needle: u8) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + byte if level == 0 && byte == needle => return Some(index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ => index += 1, + } + } + None +} + +fn identifier_end(bytes: &[u8], mut index: usize, end: usize) -> usize { + if !bytes + .get(index) + .is_some_and(|byte| *byte >= 0x80 || *byte == b'_' || byte.is_ascii_alphabetic()) + { + return index; + } + index += 1; + while index < end + && bytes + .get(index) + .is_some_and(|byte| *byte >= 0x80 || is_ascii_identifier_char(*byte)) + { + index += 1; + } + index +} + +fn expression_slice_is_tuple(source: &str, start: usize, end: usize) -> bool { + let bytes = source.as_bytes(); + let (start, end) = trim_target_range(bytes, start, end); + if start >= end { + return false; + } + let Ok(parsed) = parser::parse(&source[start..end], parser::Mode::Expression.into()) else { + return false; + }; + matches!(parsed.into_syntax(), ast::Mod::Expression(expression) if matches!(*expression.body, ast::Expr::Tuple(_))) +} + +fn type_param_list_open(bytes: &[u8], open: usize) -> bool { + let mut cursor = open; + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + while cursor > 0 + && bytes + .get(cursor - 1) + .is_some_and(|byte| *byte >= 0x80 || is_ascii_identifier_char(*byte)) + { + cursor -= 1; + } + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + (cursor >= 3 && starts_identifier(bytes, cursor - 3, b"def")) + || (cursor >= 5 && starts_identifier(bytes, cursor - 5, b"class")) + || (cursor >= 4 && starts_identifier(bytes, cursor - 4, b"type")) +} + +fn invalid_type_param_item_error( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (start, end) = trim_target_range(bytes, start, end); + if start >= end || bytes.get(start) != Some(&b'*') { + return None; + } + let is_param_spec = bytes.get(start + 1) == Some(&b'*'); + let name_start = start + if is_param_spec { 2 } else { 1 }; + let name_end = identifier_end(bytes, name_start, end); + if name_start == name_end { + return None; + } + let colon = next_non_horizontal_whitespace(bytes, name_end); + if colon >= end || bytes.get(colon) != Some(&b':') { + return None; + } + let has_constraints = expression_slice_is_tuple(source, colon + 1, end); + let message = match (is_param_spec, has_constraints) { + (false, false) => "cannot use bound with TypeVarTuple", + (false, true) => "cannot use constraints with TypeVarTuple", + (true, false) => "cannot use bound with ParamSpec", + (true, true) => "cannot use constraints with ParamSpec", + }; + Some((message.to_owned(), colon, colon + 1)) +} + +fn invalid_type_param_list_error( + source: &str, + open: usize, + close: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut item_start = open + 1; + let mut index = item_start; + let mut level = 0usize; + while index <= close { + if index == close || (level == 0 && bytes.get(index) == Some(&b',')) { + if let Some(error) = invalid_type_param_item_error(source, item_start, index) { + return Some(error); + } + item_start = index + 1; + index += 1; + continue; + } + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_type_param_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'[' if type_param_list_open(bytes, index) => { + let Some(close) = matching_delimiter(bytes, index, b']') else { + index += 1; + continue; + }; + if let Some(error) = invalid_type_param_list_error(source, index, close) { + return Some(error); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_comprehension_in_slice( + bytes: &[u8], + open: usize, + close: usize, +) -> Option<(String, usize, usize)> { + let for_index = find_keyword_at_level(bytes, open + 1, close, b"for")?; + let item_start = next_non_horizontal_whitespace(bytes, open + 1); + if item_start >= for_index { + return None; + } + if bytes.get(item_start..item_start + 2) == Some(b"**") && bytes.get(open) == Some(&b'{') { + return Some(( + "dict unpacking cannot be used in dict comprehension".to_owned(), + item_start, + item_start + 2, + )); + } + if bytes.get(item_start..item_start + 2) == Some(b"**") && bytes.get(open) == Some(&b'(') { + return Some(("invalid syntax".to_owned(), for_index, for_index + 3)); + } + if bytes.get(item_start) == Some(&b'*') { + return Some(( + "iterable unpacking cannot be used in comprehension".to_owned(), + item_start, + item_start + 1, + )); + } + if !matches!(bytes.get(open), Some(b'[' | b'{')) { + return None; + } + if top_level_colon(bytes, open + 1, for_index).is_none() + && let Some(comma) = top_level_byte(bytes, open + 1, for_index, b',') + { + let (start, _) = trim_target_range(bytes, open + 1, comma); + return Some(( + "did you forget parentheses around the comprehension target?".to_owned(), + start, + comma + 1, + )); + } + None +} + +fn invalid_comprehension_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + let close_byte = match bytes[index] { + b'(' => b')', + b'[' => b']', + _ => b'}', + }; + let Some(close) = matching_delimiter(bytes, index, close_byte) else { + index += 1; + continue; + }; + if let Some(error) = invalid_comprehension_in_slice(bytes, index, close) { + return Some(error); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_group_in_slice( + bytes: &[u8], + open: usize, + close: usize, +) -> Option<(String, usize, usize)> { + let (item_start, item_end) = trim_target_range(bytes, open + 1, close); + if item_start >= item_end + || top_level_byte(bytes, item_start, item_end, b',').is_some() + || top_level_colon(bytes, item_start, item_end).is_some() + || find_keyword_at_level(bytes, item_start, item_end, b"for").is_some() + { + return None; + } + if bytes.get(item_start..item_start + 2) == Some(b"**") { + return Some(( + "cannot use double starred expression here".to_owned(), + item_start, + item_start + 2, + )); + } + if bytes.get(item_start) == Some(&b'*') { + return Some(( + "cannot use starred expression here".to_owned(), + item_start, + item_start + 1, + )); + } + None +} + +fn invalid_group_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' => { + let Some(close) = matching_delimiter(bytes, index, b')') else { + index += 1; + continue; + }; + if let Some(error) = invalid_group_in_slice(bytes, index, close) { + return Some(error); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_parameter_star_annotation_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' => { + let Some(close) = matching_delimiter(bytes, index, b')') else { + index += 1; + continue; + }; + let mut param_start = index + 1; + while param_start < close { + let param_end = + find_byte_at_level(bytes, param_start, close, b',').unwrap_or(close); + if let Some(colon) = top_level_colon(bytes, param_start, param_end) { + let value_start = next_non_horizontal_whitespace(bytes, colon + 1); + if bytes.get(value_start) == Some(&b'*') { + return Some(( + "invalid syntax".to_owned(), + value_start, + value_start + 1, + )); + } + } + param_start = param_end.saturating_add(1); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_def_type_params_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + _ if starts_identifier(bytes, index, b"def") => { + let name_start = skip_horizontal_whitespace(bytes, index + 3); + let name_end = identifier_end(bytes, name_start, bytes.len()); + let bracket = skip_horizontal_whitespace(bytes, name_end); + if bytes.get(bracket) == Some(&b'[') { + let Some(close) = matching_delimiter(bytes, bracket, b']') else { + index = bracket + 1; + continue; + }; + let after_close = skip_horizontal_whitespace(bytes, close + 1); + if bytes.get(after_close) == Some(&b'(') + && type_param_list_is_malformed(bytes, bracket + 1, close) + { + return Some(("expected '('".to_owned(), bracket, bracket + 1)); + } + } + index = name_end.max(index + 3); + } + _ => index += 1, + } + } + None +} + +fn type_param_list_is_malformed(bytes: &[u8], start: usize, end: usize) -> bool { + let mut index = start; + let mut expect_item = true; + while index < end { + index = skip_horizontal_whitespace(bytes, index); + if index >= end { + break; + } + if bytes[index] == b',' { + if expect_item { + return true; + } + expect_item = true; + index += 1; + continue; + } + if !expect_item { + return true; + } + if bytes.get(index..index + 2) == Some(b"**") { + index += 2; + } else if bytes.get(index) == Some(&b'*') { + index += 1; + } + let item_start = skip_horizontal_whitespace(bytes, index); + let item_end = identifier_end(bytes, item_start, end); + if item_end == item_start { + return true; + } + index = item_end; + if bytes.get(skip_horizontal_whitespace(bytes, index)) == Some(&b':') { + index = skip_horizontal_whitespace(bytes, index) + 1; + while index < end && bytes[index] != b',' { + index = match bytes[index] { + b'\'' | b'"' => skip_quoted_string(bytes, index), + _ => index + 1, + }; + } + } + expect_item = false; + } + false +} + +fn invalid_parameter_list_slice_error( + source: &str, + start: usize, + end: usize, + kind: ParameterListKind, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = start; + let mut level = 0usize; + let mut default_seen = false; + let mut keyword_only = false; + let mut slash_seen = false; + let mut var_keyword_seen = false; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + _ if level == 0 + && var_keyword_seen + && bytes.get(index).is_some_and(|byte| { + *byte >= 0x80 || *byte == b'_' || byte.is_ascii_alphabetic() + }) => + { + let name_end = identifier_end(bytes, index, end); + return Some(( + "arguments cannot follow var-keyword argument".to_owned(), + index, + name_end, + )); + } + _ if level == 0 + && !keyword_only + && bytes.get(index).is_some_and(|byte| { + *byte >= 0x80 || *byte == b'_' || byte.is_ascii_alphabetic() + }) => + { + let param_end = find_byte_at_level(bytes, index, end, b',') + .or_else(|| top_level_byte(bytes, index, end, b')')) + .or_else(|| { + matches!(kind, ParameterListKind::Lambda) + .then(|| top_level_byte(bytes, index, end, b':')) + .flatten() + }) + .unwrap_or(end); + let name_end = identifier_end(bytes, index, param_end); + if top_level_byte(bytes, index, param_end, b'=').is_some() { + default_seen = true; + } else if default_seen { + return Some(( + "parameter without a default follows parameter with a default".to_owned(), + index, + name_end, + )); + } + index = name_end; + } + b'(' if level == 0 => { + let close = matching_delimiter(bytes, index, b')') + .filter(|close| *close <= end) + .unwrap_or(index + 1); + let message = match kind { + ParameterListKind::Function => "Function parameters cannot be parenthesized", + ParameterListKind::Lambda => { + "Lambda expression parameters cannot be parenthesized" + } + }; + return Some((message.to_owned(), index, close + 1)); + } + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b'/' if level == 0 => { + if var_keyword_seen { + return Some(( + "arguments cannot follow var-keyword argument".to_owned(), + index, + index + 1, + )); + } + if slash_seen { + return Some(("/ may appear only once".to_owned(), index, index + 1)); + } + slash_seen = true; + let next = next_non_horizontal_whitespace(bytes, index + 1); + if bytes.get(next) == Some(&b'*') { + return Some(("expected comma between / and *".to_owned(), next, next + 1)); + } + index += 1; + } + b'*' if level == 0 => { + if var_keyword_seen { + return Some(( + "arguments cannot follow var-keyword argument".to_owned(), + index, + index + 1, + )); + } + keyword_only = true; + let stars = usize::from(bytes.get(index + 1) == Some(&b'*')) + 1; + let name_start = next_non_horizontal_whitespace(bytes, index + stars); + for keyword in [b"True".as_slice(), b"False".as_slice(), b"None".as_slice()] { + if starts_identifier(bytes, name_start, keyword) { + return Some(( + "invalid syntax".to_owned(), + name_start, + name_start + keyword.len(), + )); + } + } + let param_end = find_byte_at_level(bytes, name_start, end, b',') + .or_else(|| top_level_byte(bytes, name_start, end, b')')) + .or_else(|| { + matches!(kind, ParameterListKind::Lambda) + .then(|| top_level_byte(bytes, name_start, end, b':')) + .flatten() + }) + .unwrap_or(end); + if stars == 1 && matches!(bytes.get(name_start), Some(b')' | b',' | b':')) { + return Some(( + "named arguments must follow bare *".to_owned(), + index, + index + 1, + )); + } + if stars == 1 && top_level_byte(bytes, name_start, param_end, b'=').is_some() { + return Some(( + "var-positional argument cannot have default value".to_owned(), + index, + index + 1, + )); + } + if stars == 2 && top_level_byte(bytes, name_start, param_end, b'=').is_some() { + return Some(( + "var-keyword argument cannot have default value".to_owned(), + index, + index + 2, + )); + } + if stars == 2 { + var_keyword_seen = true; + index = param_end; + continue; + } + index += stars; + } + b'=' if level == 0 => { + let value_start = next_non_horizontal_whitespace(bytes, index + 1); + if value_start >= end || matches!(bytes.get(value_start), Some(b',' | b')' | b':')) + { + if matches!(kind, ParameterListKind::Lambda) + && matches!(bytes.get(value_start), Some(b':')) + { + return Some(("invalid syntax".to_owned(), index, index + 1)); + } + return Some(( + "expected default value expression".to_owned(), + index, + index + 1, + )); + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_parameter_list_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + _ if starts_identifier(bytes, index, b"def") => { + let Some(paren) = top_level_byte(bytes, index + 3, bytes.len(), b'(') else { + index += 3; + continue; + }; + let Some(close) = matching_delimiter(bytes, paren, b')') else { + index = paren + 1; + continue; + }; + if let Some(error) = invalid_parameter_list_slice_error( + source, + paren + 1, + close, + ParameterListKind::Function, + ) { + return Some(error); + } + index = close + 1; + } + _ if starts_identifier(bytes, index, b"lambda") => { + let params_start = index + 6; + let Some(params_end) = find_lambda_parameter_end(bytes, params_start) else { + index = params_start; + continue; + }; + if let Some(error) = invalid_parameter_list_slice_error( + source, + params_start, + params_end, + ParameterListKind::Lambda, + ) { + return Some(error); + } + index = params_end + 1; + } + _ => index += 1, + } + } + None +} + +#[derive(Clone, Copy)] +struct CallArgFrame { + level: usize, + arg_start: Option, + in_call: bool, +} + +fn next_non_horizontal_whitespace(bytes: &[u8], mut index: usize) -> usize { + while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\x0c')) { + index += 1; + } + index +} + +fn invalid_call_argument_assignment_error( + source: &str, + arg_start: usize, + equal: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let start = bytes[arg_start..equal] + .iter() + .rposition(|byte| *byte == b'\n') + .map_or(arg_start, |newline| arg_start + newline + 1); + let (target_start, target_end) = trim_target_range(bytes, start, equal); + if target_start >= target_end { + return None; + } + let value_start = next_non_horizontal_whitespace(bytes, equal + 1); + if matches!(bytes.get(value_start), None | Some(b',' | b')')) { + return Some(( + "expected argument value expression".to_owned(), + target_start, + equal + 1, + )); + } + if bytes.get(target_start..target_start + 2) == Some(b"**") { + return Some(( + "cannot assign to keyword argument unpacking".to_owned(), + target_start, + value_start, + )); + } + if bytes.get(target_start) == Some(&b'*') { + return Some(( + "cannot assign to iterable argument unpacking".to_owned(), + target_start, + value_start, + )); + } + for keyword in [b"True".as_slice(), b"False".as_slice(), b"None".as_slice()] { + if bytes.get(target_start..target_end) == Some(keyword) { + let keyword = ::core::str::from_utf8(keyword).ok()?; + return Some(( + format!("cannot assign to {keyword}"), + target_start, + target_end, + )); + } + } + if is_simple_keyword_name(bytes, target_start, target_end) { + return None; + } + Some(( + "expression cannot contain assignment, perhaps you meant \"==\"?".to_owned(), + target_start, + equal, + )) +} + +fn invalid_call_star_expression_error( + bytes: &[u8], + arg_start: usize, + index: usize, +) -> Option<(String, usize, usize)> { + let start = next_non_horizontal_whitespace(bytes, arg_start); + if start != index || bytes.get(index) != Some(&b'*') { + return None; + } + let after_star = next_non_horizontal_whitespace(bytes, index + 1); + if matches!(bytes.get(after_star), None | Some(b',' | b')' | b':')) { + return Some(( + "Invalid star expression".to_owned(), + index, + (index + 1).min(bytes.len()), + )); + } + None +} + +fn invalid_call_argument_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + let mut level = 0usize; + let mut frames: Vec = Vec::new(); + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + _ if starts_identifier(bytes, index, b"lambda") => { + let params_start = index + 6; + if let Some(params_end) = find_lambda_parameter_end(bytes, params_start) { + index = params_end + 1; + } else { + index = params_start; + } + } + b'(' => { + level += 1; + let in_call = opening_paren_is_call(bytes, index) + || frames.last().is_some_and(|frame| frame.in_call); + frames.push(CallArgFrame { + level, + arg_start: (in_call && !is_function_parameter_list(bytes, index)) + .then_some(index + 1), + in_call, + }); + index += 1; + } + b')' => { + if matches!(frames.last(), Some(frame) if frame.level == level) { + frames.pop(); + } + level = level.saturating_sub(1); + index += 1; + } + b'[' | b'{' => { + level += 1; + index += 1; + } + b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b',' => { + if let Some(frame) = frames.last_mut() + && frame.level == level + && frame.arg_start.is_some() + { + frame.arg_start = Some(index + 1); + } + index += 1; + } + b'*' => { + if let Some(CallArgFrame { + level: frame_level, + arg_start: Some(arg_start), + in_call: true, + }) = frames.last().copied() + && frame_level == level + && let Some(error) = invalid_call_star_expression_error(bytes, arg_start, index) + { + return Some(error); + } + index += 1; + } + b'=' if is_plain_assignment_operator(bytes, index) => { + if let Some(CallArgFrame { + level: frame_level, + arg_start: Some(arg_start), + in_call: true, + }) = frames.last().copied() + && frame_level == level + && let Some(error) = + invalid_call_argument_assignment_error(source, arg_start, index) + { + return Some(error); + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn top_level_colon(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b':' if level == 0 => return Some(index), + _ => index += 1, + } + } + None +} + +fn expression_slice_is_valid(source: &str, start: usize, end: usize) -> bool { + let bytes = source.as_bytes(); + let (start, end) = trim_target_range(bytes, start, end); + start < end + && parser::parse(&source[start..end], parser::Mode::Expression.into()) + .is_ok_and(|parsed| matches!(parsed.into_syntax(), ast::Mod::Expression(_))) +} + +fn invalid_dict_entry_error( + source: &str, + item_start: usize, + item_end: usize, + colon: Option, + saw_dict_item: bool, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (item_start, item_end) = trim_target_range(bytes, item_start, item_end); + if item_start >= item_end { + return None; + } + if let Some(colon) = colon { + let value_start = next_non_horizontal_whitespace(bytes, colon + 1); + if value_start >= item_end { + return Some(( + "expression expected after dictionary key and ':'".to_owned(), + colon, + colon + 1, + )); + } + if bytes.get(value_start) == Some(&b'*') { + return Some(( + "cannot use a starred expression in a dictionary value".to_owned(), + value_start, + value_start + 1, + )); + } + if !expression_slice_is_valid(source, value_start, item_end) { + return Some(("invalid syntax".to_owned(), value_start, value_start)); + } + } else if saw_dict_item { + return Some(( + "':' expected after dictionary key".to_owned(), + item_end.saturating_sub(1), + item_end, + )); + } + None +} + +fn invalid_dict_literal_error( + source: &str, + open: usize, + close: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut item_start = open + 1; + let mut index = item_start; + let mut level = 0usize; + let mut saw_dict_item = false; + let mut item_colon = None; + while index <= close { + if index == close || (level == 0 && bytes.get(index) == Some(&b',')) { + if let Some(error) = + invalid_dict_entry_error(source, item_start, index, item_colon, saw_dict_item) + { + return Some(error); + } + saw_dict_item |= item_colon.is_some(); + item_start = index + 1; + item_colon = None; + index += 1; + continue; + } + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b':' if level == 0 && item_colon.is_none() => { + item_colon = Some(index); + saw_dict_item = true; + index += 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_dict_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'{' => { + let Some(close) = matching_delimiter(bytes, index, b'}') else { + index += 1; + continue; + }; + if top_level_colon(bytes, index + 1, close).is_some() + && let Some(error) = invalid_dict_literal_error(source, index, close) + { + return Some(error); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn collection_open_is_call(bytes: &[u8], open: usize) -> bool { + if bytes.get(open) != Some(&b'(') { + return false; + } + let mut cursor = open; + while cursor > 0 && matches!(bytes.get(cursor - 1), Some(b' ' | b'\t' | b'\x0c')) { + cursor -= 1; + } + matches!( + cursor.checked_sub(1).and_then(|before| bytes.get(before)), + Some(b')' | b']' | b'_' | b'a'..=b'z' | b'A'..=b'Z' | 0x80..=0xff) + ) +} + +fn invalid_collection_assignment_in_slice( + source: &str, + bytes: &[u8], + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + let mut item_start = start; + let mut index = start; + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b',' if level == 0 => { + item_start = index + 1; + index += 1; + } + b'=' if level == 0 && is_plain_assignment_operator(bytes, index) => { + if top_level_colon(bytes, item_start, index).is_none() { + let start = next_non_horizontal_whitespace(bytes, item_start); + let target_end = trim_end_horizontal_whitespace(bytes, start, index); + if start < target_end + && let Some((expr_name, expr_start, expr_end, _)) = + expression_name_and_range(&source[start..target_end]) + { + if matches!(expr_name, "list" | "tuple") { + return None; + } + if matches!(expr_name, "expression" | "attribute" | "subscript") { + return Some(( + format!( + "cannot assign to {expr_name} here. Maybe you meant '==' instead of '='?" + ), + start + expr_start, + start + expr_end, + )); + } + } + return Some(( + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned(), + start, + index + 1, + )); + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_collection_assignment_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + let close_byte = match bytes[index] { + b'(' => b')', + b'[' => b']', + _ => b'}', + }; + let Some(close) = matching_delimiter(bytes, index, close_byte) else { + index += 1; + continue; + }; + if !collection_open_is_call(bytes, index) + && let Some(error) = + invalid_collection_assignment_in_slice(source, bytes, index + 1, close) + { + return Some(error); + } + index = close + 1; + } + _ => index += 1, + } + } + None +} + +fn expression_assignment_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + let mut paren_arg_starts: Vec<(Option, bool)> = Vec::new(); + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + _ if starts_identifier(bytes, index, b"lambda") => { + let params_start = index + 6; + if let Some(params_end) = find_lambda_parameter_end(bytes, params_start) { + index = params_end + 1; + } else { + index = params_start; + } + } + b'(' => { + let in_call_context = opening_paren_is_call(bytes, index) + || paren_arg_starts.last().is_some_and(|(_, in_call)| *in_call); + paren_arg_starts.push(( + (!is_function_parameter_list(bytes, index)).then_some(index + 1), + in_call_context, + )); + index += 1; + } + b')' => { + paren_arg_starts.pop(); + index += 1; + } + b',' => { + if let Some((start, _)) = paren_arg_starts.last_mut() + && start.is_some() + { + *start = Some(index + 1); + } + index += 1; + } + b'=' if is_plain_assignment_operator(bytes, index) => { + if let Some((Some(start), true)) = paren_arg_starts.last().copied() + && !is_simple_keyword_name(bytes, start, index) + { + let mut expr_start = start; + while matches!(bytes.get(expr_start), Some(b' ' | b'\t' | b'\x0c')) { + expr_start += 1; + } + return Some(( + "expression cannot contain assignment, perhaps you meant \"==\"?" + .to_owned(), + expr_start, + index, + )); + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn invalid_named_expression_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index + 1 < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b':' if bytes.get(index + 1) == Some(&b'=') => { + let target_start = named_expression_target_start(bytes, index); + let target_end = trim_end_horizontal_whitespace(bytes, target_start, index); + if target_start < target_end + && let Some((expr_name, start, end, is_name)) = + expression_name_and_range(&source[target_start..target_end]) + && !is_name + { + return Some(( + format!("cannot use assignment expressions with {expr_name}"), + target_start + start, + target_start + end, + )); + } + index += 2; + } + _ => index += 1, + } + } + None +} + +#[derive(Clone, Copy)] +struct AssignmentContext { + start: usize, + call: bool, +} + +fn invalid_plain_assignment_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut stack: Vec = Vec::new(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + stack.push(AssignmentContext { + start: index + 1, + call: bytes[index] == b'(' && opening_paren_is_call(bytes, index), + }); + index += 1; + } + b')' | b']' | b'}' => { + stack.pop(); + index += 1; + } + b',' => { + if let Some(context) = stack.last_mut() + && !context.call + { + context.start = index + 1; + } + index += 1; + } + b'=' if is_plain_assignment_operator(bytes, index) => { + if let Some(context) = stack.last().copied() + && !context.call + { + let target_start = skip_horizontal_whitespace(bytes, context.start); + let target_end = trim_end_horizontal_whitespace(bytes, target_start, index); + if target_start < target_end + && let Some((expr_name, start, end, _)) = + expression_name_and_range(&source[target_start..target_end]) + && matches!(expr_name, "expression" | "attribute" | "subscript") + { + return Some(( + format!( + "cannot assign to {expr_name} here. Maybe you meant '==' instead of '='?" + ), + target_start + start, + target_start + end, + )); + } + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn opening_paren_is_call(bytes: &[u8], paren: usize) -> bool { + let mut cursor = paren; + while cursor > 0 && matches!(bytes[cursor - 1], b' ' | b'\t' | b'\x0c') { + cursor -= 1; + } + cursor > 0 + && (bytes[cursor - 1] >= 0x80 + || is_ascii_identifier_char(bytes[cursor - 1]) + || matches!(bytes[cursor - 1], b')' | b']')) +} + +fn named_expression_target_start(bytes: &[u8], walrus: usize) -> usize { + let mut index = walrus; + let mut level = 0usize; + while index > 0 { + index -= 1; + match bytes[index] { + b')' | b']' | b'}' => level += 1, + b'(' | b'[' | b'{' if level > 0 => level -= 1, + b'(' | b'[' | b'{' if level == 0 => return index + 1, + b',' | b'\n' | b';' if level == 0 => return index + 1, + _ => {} + } + } + 0 +} + +fn trim_end_horizontal_whitespace(bytes: &[u8], start: usize, mut end: usize) -> usize { + while end > start && matches!(bytes[end - 1], b' ' | b'\t' | b'\x0c') { + end -= 1; + } + end +} + +fn annotation_target_error_for_slice( + source: &str, + start: usize, + colon: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, colon); + if target_start >= target_end { + return None; + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + return None; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + match expression.body.as_ref() { + ast::Expr::Name(_) | ast::Expr::Attribute(_) | ast::Expr::Subscript(_) => None, + ast::Expr::List(_) => Some(( + "only single target (not list) can be annotated".to_owned(), + target_start, + target_end, + )), + ast::Expr::Tuple(_) => Some(( + "only single target (not tuple) can be annotated".to_owned(), + target_start, + target_end, + )), + _ => Some(( + "illegal target for annotation".to_owned(), + target_start, + target_end, + )), + } +} + +fn invalid_annotation_line_start(bytes: &[u8], line_start: usize) -> bool { + let column = skip_horizontal_whitespace(bytes, line_start); + for keyword in [ + b"async".as_slice(), + b"case", + b"class", + b"def", + b"elif", + b"else", + b"except", + b"finally", + b"for", + b"if", + b"match", + b"try", + b"while", + b"with", + ] { + if starts_identifier(bytes, column, keyword) { + return false; + } + } + true +} + +fn invalid_annotation_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + if invalid_annotation_line_start(bytes, line_start) + && let Some(colon) = find_byte_at_level(bytes, line_start, line_end, b':') + && bytes.get(colon + 1) != Some(&b'=') + && colon.checked_sub(1).and_then(|before| bytes.get(before)) != Some(&b':') + && let Some(error) = annotation_target_error_for_slice(source, line_start, colon) + { + return Some(error); + } + line_start = line_end; + } + None +} + +fn statement_target_end(bytes: &[u8], mut index: usize) -> usize { + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => return index, + b'\n' | b';' if level == 0 => return index, + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ => index += 1, + } + } + index +} + +fn invalid_assignment_target(expression: &ast::Expr) -> Option<&ast::Expr> { + match expression { + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + elts.iter().find_map(invalid_assignment_target) + } + ast::Expr::Starred(ast::ExprStarred { value, .. }) => invalid_assignment_target(value), + ast::Expr::Name(_) | ast::Expr::Subscript(_) | ast::Expr::Attribute(_) => None, + _ => Some(expression), + } +} + +fn invalid_for_target(expression: &ast::Expr) -> Option<&ast::Expr> { + match expression { + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().find_map(invalid_for_target), + ast::Expr::Starred(ast::ExprStarred { value, .. }) => invalid_for_target(value), + ast::Expr::Compare(ast::ExprCompare { left, ops, .. }) => { + if matches!(ops.first(), Some(ast::CmpOp::In)) { + invalid_for_target(left) + } else { + None + } + } + ast::Expr::Name(_) | ast::Expr::Subscript(_) | ast::Expr::Attribute(_) => None, + _ => Some(expression), + } +} + +fn invalid_delete_target(expression: &ast::Expr) -> Option<&ast::Expr> { + match expression { + ast::Expr::List(ast::ExprList { elts, .. }) + | ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => { + elts.iter().find_map(invalid_delete_target) + } + ast::Expr::Name(_) | ast::Expr::Subscript(_) | ast::Expr::Attribute(_) => None, + ast::Expr::Starred(_) => Some(expression), + ast::Expr::Compare(_) => Some(expression), + _ => Some(expression), + } +} + +fn delete_target_expr_name(expression: &ast::Expr) -> &'static str { + match expression { + ast::Expr::Attribute(_) => "attribute", + ast::Expr::Subscript(_) => "subscript", + ast::Expr::Starred(_) => "starred", + ast::Expr::Name(_) => "name", + ast::Expr::List(_) => "list", + ast::Expr::Tuple(_) => "tuple", + ast::Expr::Lambda(_) => "lambda", + ast::Expr::Call(_) => "function call", + ast::Expr::BoolOp(_) | ast::Expr::BinOp(_) | ast::Expr::UnaryOp(_) => "expression", + ast::Expr::Generator(_) => "generator expression", + ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => "yield expression", + ast::Expr::Await(_) => "await expression", + ast::Expr::ListComp(_) => "list comprehension", + ast::Expr::SetComp(_) => "set comprehension", + ast::Expr::DictComp(_) => "dict comprehension", + ast::Expr::Dict(_) => "dict literal", + ast::Expr::Set(_) => "set display", + ast::Expr::FString(_) => "f-string expression", + ast::Expr::TString(_) => "t-string expression", + ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => { + "literal" + } + ast::Expr::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", + }, + ast::Expr::BooleanLiteral(boolean) => { + if boolean.value { + "True" + } else { + "False" + } + } + ast::Expr::NoneLiteral(_) => "None", + ast::Expr::EllipsisLiteral(_) => "ellipsis", + ast::Expr::Compare(_) => "comparison", + ast::Expr::If(_) => "conditional expression", + ast::Expr::Named(_) => "named expression", + ast::Expr::Slice(_) | ast::Expr::IpyEscapeCommand(_) => "expression", + } +} + +fn parenthesized_single_starred_delete_target(bytes: &[u8], start: usize, end: usize) -> bool { + let mut cursor = start; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + if bytes.get(cursor) != Some(&b'(') { + return false; + } + cursor += 1; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + if bytes.get(cursor) != Some(&b'*') { + return false; + } + let mut level = 1usize; + cursor += 1; + while cursor < end { + match bytes[cursor] { + b'\'' | b'"' => { + cursor = skip_quoted_string(bytes, cursor); + } + b'(' | b'[' | b'{' => { + level += 1; + cursor += 1; + } + b')' => { + level = level.saturating_sub(1); + if level == 0 { + cursor += 1; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + return cursor == end; + } + cursor += 1; + } + b',' if level == 1 => return false, + b']' | b'}' => { + level = level.saturating_sub(1); + cursor += 1; + } + _ => cursor += 1, + } + } + false +} + +fn trim_target_range(bytes: &[u8], mut start: usize, mut end: usize) -> (usize, usize) { + while start < end + && matches!( + bytes.get(start), + Some(b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') + ) + { + start += 1; + } + while end > start + && matches!( + bytes.get(end - 1), + Some(b' ' | b'\t' | b'\n' | b'\r' | b'\x0c') + ) + { + end -= 1; + } + (start, end) +} + +fn invalid_assignment_message(name: &'static str, top_level_bitwise: bool) -> String { + if top_level_bitwise { + format!("cannot assign to {name} here. Maybe you meant '==' instead of '='?") + } else { + format!("cannot assign to {name}") + } +} + +fn assignment_target_error_for_slice( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, end); + if target_start >= target_end { + return None; + } + if starts_identifier(bytes, target_start, b"yield") { + return Some(( + "assignment to yield expression not possible".to_owned(), + target_start, + target_start + 5, + )); + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + return None; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let invalid_target = invalid_assignment_target(&expression.body)?; + let invalid_start = target_start + invalid_target.range().start().to_usize(); + let invalid_end = target_start + invalid_target.range().end().to_usize(); + if matches!(invalid_target, ast::Expr::FString(_)) { + return Some(("invalid syntax".to_owned(), invalid_start, invalid_end)); + } + let name = delete_target_expr_name(invalid_target); + let top_level = invalid_target.range() == expression.body.range(); + let bitwise_like = matches!( + invalid_target, + ast::Expr::Call(_) + | ast::Expr::BoolOp(_) + | ast::Expr::BinOp(_) + | ast::Expr::UnaryOp(_) + | ast::Expr::NumberLiteral(_) + | ast::Expr::StringLiteral(_) + | ast::Expr::BytesLiteral(_) + | ast::Expr::EllipsisLiteral(_) + ); + Some(( + invalid_assignment_message(name, top_level && bitwise_like), + invalid_start, + invalid_end, + )) +} + +fn star_target_error_for_slice( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + invalid_target_error_for_slice(source, start, end, invalid_assignment_target) +} + +fn for_target_error_for_slice( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + invalid_target_error_for_slice(source, start, end, invalid_for_target) +} + +fn invalid_target_error_for_slice( + source: &str, + start: usize, + end: usize, + invalid_target: for<'a> fn(&'a ast::Expr) -> Option<&'a ast::Expr>, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, end); + if target_start >= target_end { + return None; + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + return None; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let invalid_target = invalid_target(&expression.body)?; + let name = delete_target_expr_name(invalid_target); + let invalid_start = target_start + invalid_target.range().start().to_usize(); + let invalid_end = target_start + invalid_target.range().end().to_usize(); + Some(( + format!("cannot assign to {name}"), + invalid_start, + invalid_end, + )) +} + +fn first_compare_operator_at_level(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b'<' | b'>' if level == 0 => return Some(index), + b'=' if level == 0 && bytes.get(index + 1) == Some(&b'=') => return Some(index), + b'!' if level == 0 && bytes.get(index + 1) == Some(&b'=') => return Some(index), + _ if level == 0 && starts_identifier(bytes, index, b"is") => return Some(index), + _ if level == 0 && starts_identifier(bytes, index, b"not") => return Some(index), + _ => index += 1, + } + } + None +} + +fn non_in_compare_for_target_error( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, end); + if target_start >= target_end { + return None; + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + return None; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let ast::Expr::Compare(ast::ExprCompare { ops, .. }) = expression.body.as_ref() else { + return None; + }; + if matches!(ops.first(), Some(ast::CmpOp::In)) { + return None; + } + let operator = first_compare_operator_at_level(bytes, target_start, target_end)?; + Some(( + "invalid syntax".to_owned(), + operator, + (operator + 1).min(target_end), + )) +} + +fn top_level_plain_assignment_offsets(bytes: &[u8]) -> Vec { + let mut offsets = Vec::new(); + let mut index = 0usize; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b'=' if level == 0 && is_plain_assignment_operator(bytes, index) => { + offsets.push(index); + index += 1; + } + _ => index += 1, + } + } + offsets +} + +fn invalid_assignment_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let offsets = top_level_plain_assignment_offsets(bytes); + if offsets.is_empty() { + return None; + } + let mut start = 0usize; + for offset in offsets { + if let Some(error) = assignment_target_error_for_slice(source, start, offset) { + return Some(error); + } + start = offset + 1; + } + None +} + +fn top_level_augassign_offset(bytes: &[u8]) -> Option<(usize, usize)> { + let mut index = 0usize; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b'+' | b'-' | b'*' | b'@' | b'/' | b'%' | b'&' | b'|' | b'^' + if level == 0 && bytes.get(index + 1) == Some(&b'=') => + { + return Some((index, 2)); + } + b'<' | b'>' + if level == 0 + && bytes.get(index + 1) == Some(&bytes[index]) + && bytes.get(index + 2) == Some(&b'=') => + { + return Some((index, 3)); + } + b'*' if level == 0 + && bytes.get(index + 1) == Some(&b'*') + && bytes.get(index + 2) == Some(&b'=') => + { + return Some((index, 3)); + } + b'/' if level == 0 + && bytes.get(index + 1) == Some(&b'/') + && bytes.get(index + 2) == Some(&b'=') => + { + return Some((index, 3)); + } + _ => index += 1, + } + } + None +} + +fn invalid_augassign_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (operator, _) = top_level_augassign_offset(bytes)?; + let (target_start, target_end) = trim_target_range(bytes, 0, operator); + if target_start >= target_end { + return None; + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + return None; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let name = delete_target_expr_name(&expression.body); + Some(( + format!("'{name}' is an illegal expression for augmented assignment"), + target_start, + target_end, + )) +} + +fn find_for_target_delimiter(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ if level == 0 && starts_identifier(bytes, index, b"in") => return Some(index), + b':' if level == 0 => return Some(index), + _ => index += 1, + } + } + None +} + +fn invalid_for_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + _ if starts_identifier(bytes, index, b"for") => { + let target_start = skip_horizontal_whitespace(bytes, index + 3); + let line_end = source[index..] + .find('\n') + .map_or(bytes.len(), |newline| index + newline); + if let Some(target_end) = find_for_target_delimiter(bytes, target_start, line_end) { + if let Some(error) = + for_target_error_for_slice(source, target_start, target_end) + { + return Some(error); + } + if let Some(error) = + non_in_compare_for_target_error(source, target_start, target_end) + { + return Some(error); + } + } + index = target_start.max(index + 3); + } + _ => index += 1, + } + } + None +} + +fn find_with_target_delimiter(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' if level == 0 => return Some(index), + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b',' | b':' if level == 0 => return Some(index), + _ => index += 1, + } + } + None +} + +fn invalid_with_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let mut column = skip_horizontal_whitespace(bytes, line_start); + if starts_identifier(bytes, column, b"async") { + column = skip_horizontal_whitespace(bytes, column + 5); + } + if !starts_identifier(bytes, column, b"with") { + line_start = line_end; + continue; + } + let mut index = column + 4; + while let Some(as_index) = find_keyword_at_level(bytes, index, line_end, b"as") { + let target_start = skip_horizontal_whitespace(bytes, as_index + 2); + if let Some(target_end) = find_with_target_delimiter(bytes, target_start, line_end) { + if let Some(error) = star_target_error_for_slice(source, target_start, target_end) { + return Some(error); + } + index = target_end.saturating_add(1); + } else { + break; + } + } + line_start = line_end; + } + None +} + +fn find_missing_in_if_keyword(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' if level == 0 => return None, + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ if level == 0 && starts_identifier(bytes, index, b"in") => return None, + _ if level == 0 && starts_identifier(bytes, index, b"if") => return Some(index), + _ => index += 1, + } + } + None +} + +fn invalid_for_if_clause_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0usize; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ if level > 0 && starts_identifier(bytes, index, b"for") => { + let target_start = skip_horizontal_whitespace(bytes, index + 3); + let line_end = source[index..] + .find('\n') + .map_or(bytes.len(), |newline| index + newline); + if let Some(if_index) = find_missing_in_if_keyword(bytes, target_start, line_end) { + return Some(( + "'in' expected after for-loop variables".to_owned(), + if_index, + (if_index + 2).min(line_end), + )); + } + index = target_start.max(index + 3); + } + _ => index += 1, + } + } + None +} + +fn invalid_delete_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'd' if starts_identifier(bytes, index, b"del") => { + let mut target_start = index + 3; + if !matches!(bytes.get(target_start), Some(b' ' | b'\t' | b'\x0c')) { + index += 3; + continue; + } + while matches!(bytes.get(target_start), Some(b' ' | b'\t' | b'\x0c')) { + target_start += 1; + } + let mut target_end = statement_target_end(bytes, target_start); + while target_end > target_start + && matches!(bytes.get(target_end - 1), Some(b' ' | b'\t' | b'\x0c')) + { + target_end -= 1; + } + if target_start >= target_end { + index = target_end.max(index + 3); + continue; + } + if parenthesized_single_starred_delete_target(bytes, target_start, target_end) { + return Some(( + "cannot use starred expression here".to_owned(), + target_start, + target_end, + )); + } + if bytes.get(target_start) == Some(&b'*') { + return Some(( + "cannot delete starred".to_owned(), + target_start, + (target_start + 1).min(target_end), + )); + } + let target_text = &source[target_start..target_end]; + let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { + index = target_end; + continue; + }; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + index = target_end; + continue; + }; + let Some(invalid_target) = invalid_delete_target(&expression.body) else { + index = target_end; + continue; + }; + let start = target_start + invalid_target.range().start().to_usize(); + let end = target_start + invalid_target.range().end().to_usize(); + if matches!(invalid_target, ast::Expr::FString(_)) { + return Some(("invalid syntax".to_owned(), start, end)); + } + let name = delete_target_expr_name(invalid_target); + return Some((format!("cannot delete {name}"), start, end)); + } + _ => index += 1, + } + } + None +} + +fn skip_horizontal_whitespace(bytes: &[u8], mut index: usize) -> usize { + while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\x0c')) { + index += 1; + } + index +} + +fn find_keyword_at_level( + bytes: &[u8], + mut index: usize, + end: usize, + keyword: &[u8], +) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ if level == 0 && starts_identifier(bytes, index, keyword) => return Some(index), + _ => index += 1, + } + } + None +} + +fn find_byte_at_level(bytes: &[u8], mut index: usize, end: usize, needle: u8) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'#' if level == 0 => return None, + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + byte if level == 0 && byte == needle => return Some(index), + _ => index += 1, + } + } + None +} + +fn expression_name_and_range(source: &str) -> Option<(&'static str, usize, usize, bool)> { + let parsed = parser::parse(source, parser::Mode::Expression.into()).ok()?; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let is_name = matches!(expression.body.as_ref(), ast::Expr::Name(_)); + Some(( + delete_target_expr_name(&expression.body), + expression.body.range().start().to_usize(), + expression.body.range().end().to_usize(), + is_name, + )) +} + +fn invalid_standalone_except_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + let mut seen_try = false; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let column = skip_horizontal_whitespace(bytes, line_start); + if column >= line_end { + line_start = line_end; + continue; + } + if starts_identifier(bytes, column, b"try") { + seen_try = true; + } else if (bytes.get(column..column + 7) == Some(b"except*") + || starts_identifier(bytes, column, b"except")) + && !seen_try + { + let end = if bytes.get(column..column + 7) == Some(b"except*") { + column + 7 + } else { + column + 6 + }; + return Some(("invalid syntax".to_owned(), column, end)); + } + line_start = line_end; + } + None +} + +fn invalid_import_statement_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let column = skip_horizontal_whitespace(bytes, line_start); + if column < line_end + && starts_identifier(bytes, column, b"import") + && find_keyword_at_level(bytes, column + 6, line_end, b"from").is_some() + { + return Some(( + "Did you mean to use 'from ... import ...' instead?".to_owned(), + column, + column + 6, + )); + } + line_start = line_end; + } + None +} + +fn import_as_target_end(bytes: &[u8], mut index: usize) -> usize { + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => return index, + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' if level == 0 => return index, + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + b',' | b';' | b'\n' if level == 0 => return index, + _ => index += 1, + } + } + index +} + +fn valid_import_alias_name(bytes: &[u8], mut start: usize, end: usize) -> bool { + start = skip_horizontal_whitespace(bytes, start); + let Some(&first) = bytes.get(start) else { + return false; + }; + if !(first == b'_' || first.is_ascii_alphabetic() || first >= 0x80) { + return false; + } + let mut index = start + 1; + while index < end { + match bytes[index] { + b' ' | b'\t' | b'\x0c' => break, + byte if byte >= 0x80 || is_ascii_identifier_char(byte) => index += 1, + _ => return false, + } + } + let index = skip_horizontal_whitespace(bytes, index); + matches!( + bytes.get(index), + None | Some(b',' | b')' | b';' | b'\n' | b'\r') + ) +} + +fn import_target_error_for_slice( + source: &str, + start: usize, + end: usize, +) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, end); + if target_start >= target_end || valid_import_alias_name(bytes, target_start, target_end) { + return None; + } + let parsed = parser::parse( + &source[target_start..target_end], + parser::Mode::Expression.into(), + ) + .ok()?; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + return None; + }; + let name = delete_target_expr_name(&expression.body); + let start = target_start + expression.body.range().start().to_usize(); + let end = target_start + expression.body.range().end().to_usize(); + Some((format!("cannot use {name} as import target"), start, end)) +} + +fn statement_starts_import(bytes: &[u8], line_start: usize, line_end: usize) -> bool { + let column = skip_horizontal_whitespace(bytes, line_start); + if starts_identifier(bytes, column, b"import") { + return true; + } + starts_identifier(bytes, column, b"from") + && find_keyword_at_level(bytes, column + 4, line_end, b"import").is_some() +} + +fn invalid_import_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + let mut in_parenthesized_from_import = false; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let starts_import = statement_starts_import(bytes, line_start, line_end); + if starts_import && bytes[line_start..line_end].contains(&b'(') { + in_parenthesized_from_import = true; + } + if starts_import || in_parenthesized_from_import { + let mut index = line_start; + while index < line_end { + if starts_identifier(bytes, index, b"as") { + let target_start = skip_horizontal_whitespace(bytes, index + 2); + let target_end = import_as_target_end(bytes, target_start); + if let Some(error) = + import_target_error_for_slice(source, target_start, target_end) + { + return Some(error); + } + index = target_end.max(index + 2); + } else { + index += 1; + } + } + } + if in_parenthesized_from_import && bytes[line_start..line_end].contains(&b')') { + in_parenthesized_from_import = false; + } + line_start = line_end; + } + None +} + +fn invalid_except_as_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + let mut seen_try = false; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let mut column = skip_horizontal_whitespace(bytes, line_start); + if column >= line_end { + line_start = line_end; + continue; + } + if starts_identifier(bytes, column, b"try") { + seen_try = true; + line_start = line_end; + continue; + } + let (keyword_len, starred) = if bytes.get(column..column + 7) == Some(b"except*") { + (7, true) + } else if starts_identifier(bytes, column, b"except") { + (6, false) + } else { + line_start = line_end; + continue; + }; + if !seen_try { + line_start = line_end; + continue; + } + column += keyword_len; + let Some(as_index) = find_keyword_at_level(bytes, column, line_end, b"as") else { + line_start = line_end; + continue; + }; + let target_start = skip_horizontal_whitespace(bytes, as_index + 2); + let Some(delimiter) = find_byte_at_level(bytes, target_start, line_end, b':') + .into_iter() + .chain(find_byte_at_level(bytes, target_start, line_end, b',')) + .min() + else { + line_start = line_end; + continue; + }; + let mut target_end = delimiter; + while target_end > target_start + && matches!(bytes.get(target_end - 1), Some(b' ' | b'\t' | b'\x0c')) + { + target_end -= 1; + } + let Some((expr_name, start, end, is_name)) = + expression_name_and_range(&source[target_start..target_end]) + else { + line_start = line_end; + continue; + }; + if !is_name { + let statement = if starred { "except*" } else { "except" }; + return Some(( + format!("cannot use {statement} statement with {expr_name}"), + target_start + start, + target_start + end, + )); + } + line_start = line_end; + } + None +} + +fn invalid_match_as_target_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let quoted_ranges = quoted_string_ranges(bytes); + let mut quoted_range = 0usize; + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let mut column = skip_horizontal_whitespace(bytes, line_start); + if column >= line_end + || offset_in_ranges("ed_ranges, &mut quoted_range, column) + || !starts_identifier(bytes, column, b"case") + { + line_start = line_end; + continue; + } + column += 4; + let Some(as_index) = find_keyword_at_level(bytes, column, line_end, b"as") else { + line_start = line_end; + continue; + }; + let target_start = skip_horizontal_whitespace(bytes, as_index + 2); + let Some(delimiter) = find_byte_at_level(bytes, target_start, line_end, b':') + .into_iter() + .chain(find_byte_at_level(bytes, target_start, line_end, b',')) + .min() + else { + line_start = line_end; + continue; + }; + let mut target_end = delimiter; + while target_end > target_start + && matches!(bytes.get(target_end - 1), Some(b' ' | b'\t' | b'\x0c')) + { + target_end -= 1; + } + if source[target_start..target_end].trim() == "_" { + return Some(( + "cannot use '_' as a target".to_owned(), + target_start, + target_end, + )); + } + let Some((expr_name, start, end, is_name)) = + expression_name_and_range(&source[target_start..target_end]) + else { + line_start = line_end; + continue; + }; + if !is_name { + if matches!(expr_name, "expression" | "subscript") { + line_start = line_end; + continue; + } + return Some(( + format!("cannot use {expr_name} as pattern target"), + target_start + start, + target_start + end, + )); + } + line_start = line_end; + } + None +} + +fn quoted_string_ranges(bytes: &[u8]) -> Vec<(usize, usize)> { + let mut ranges = Vec::new(); + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + let end = skip_quoted_string(bytes, index); + ranges.push((index, end)); + index = end; + } + _ => index += 1, + } + } + ranges +} + +fn offset_in_ranges(ranges: &[(usize, usize)], range_index: &mut usize, offset: usize) -> bool { + while ranges + .get(*range_index) + .is_some_and(|(_, end)| *end <= offset) + { + *range_index += 1; + } + ranges + .get(*range_index) + .is_some_and(|(start, end)| *start <= offset && offset < *end) +} + +fn invalid_match_mapping_rest_wildcard_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let next_line_end = |line_start: usize| { + line_start + + bytes[line_start..] + .iter() + .position(|byte| *byte == b'\n') + .unwrap_or(bytes.len() - line_start) + }; + let mut index = 0usize; + let mut line_start = 0usize; + let mut line_end = next_line_end(line_start); + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'\n' => { + index += 1; + line_start = index; + line_end = next_line_end(line_start); + } + _ => { + let column = skip_horizontal_whitespace(bytes, line_start); + if index != column + || column >= line_end + || !starts_identifier(bytes, column, b"case") + { + index += 1; + continue; + } + let mut cursor = column + 4; + while cursor < line_end { + match bytes[cursor] { + b'#' => break, + b'\'' | b'"' => cursor = skip_quoted_string(bytes, cursor), + b'{' => { + let rest = next_non_horizontal_whitespace(bytes, cursor + 1); + if bytes.get(rest..rest + 2) == Some(b"**") { + let name_start = next_non_horizontal_whitespace(bytes, rest + 2); + let name_end = identifier_end(bytes, name_start, line_end); + if source.get(name_start..name_end) == Some("_") { + return Some(( + "invalid syntax".to_owned(), + name_start, + name_end, + )); + } + } + cursor += 1; + } + _ => cursor += 1, + } + } + index = line_end; + } + } + } + None +} + +fn invalid_if_expression_statement_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + if let Some(if_index) = find_keyword_at_level(bytes, line_start, line_end, b"if") + && let Some((start, end)) = statement_before_if_expression(bytes, line_start, if_index) + && find_keyword_at_level(bytes, if_index + 2, line_end, b"else").is_some() + { + return Some(( + "expected expression before 'if', but statement is given".to_owned(), + start, + end, + )); + } + if let Some(else_index) = find_keyword_at_level(bytes, line_start, line_end, b"else") + && find_keyword_at_level(bytes, line_start, else_index, b"if").is_some() + && let Some((start, end)) = + statement_after_else_expression(bytes, else_index + 4, line_end) + { + return Some(( + "expected expression after 'else', but statement is given".to_owned(), + start, + end, + )); + } + line_start = line_end; + } + None +} + +fn statement_before_if_expression( + bytes: &[u8], + line_start: usize, + if_index: usize, +) -> Option<(usize, usize)> { + let mut start = if_index; + while start > line_start && matches!(bytes.get(start - 1), Some(b' ' | b'\t' | b'\x0c')) { + start -= 1; + } + while start > line_start + && !matches!( + bytes.get(start - 1), + Some(b'=' | b':' | b',' | b'(' | b'[' | b'{') + ) + { + start -= 1; + } + start = skip_horizontal_whitespace(bytes, start); + for keyword in [b"pass".as_slice(), b"break", b"continue"] { + if starts_identifier(bytes, start, keyword) { + return Some((start, start + keyword.len())); + } + } + None +} + +fn statement_after_else_expression( + bytes: &[u8], + else_end: usize, + line_end: usize, +) -> Option<(usize, usize)> { + let start = skip_horizontal_whitespace(bytes, else_end); + for keyword in [ + b"pass".as_slice(), + b"return", + b"raise", + b"del", + b"yield", + b"assert", + b"break", + b"continue", + b"import", + b"from", + ] { + if starts_identifier(bytes, start, keyword) { + let end = statement_target_end(bytes, start).min(line_end); + return Some((start, end.max(start + keyword.len()))); + } + } + None +} + +fn invalid_else_elif_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut line_start = 0usize; + let mut else_indents: Vec = Vec::new(); + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let column = skip_horizontal_whitespace(bytes, line_start); + let line_column = column.saturating_sub(line_start); + if column >= line_end { + line_start = line_end; + continue; + } + while else_indents + .last() + .is_some_and(|indent| line_column < *indent) + { + else_indents.pop(); + } + if starts_identifier(bytes, column, b"else") + && find_byte_at_level(bytes, column + 4, line_end, b':').is_some() + { + else_indents.push(line_column); + } else if starts_identifier(bytes, column, b"elif") && else_indents.contains(&line_column) { + return Some(( + "'elif' block follows an 'else' block".to_owned(), + column, + column + 4, + )); + } + line_start = line_end; + } + None +} + +fn mixed_except_handlers_error(source: &str) -> Option<(String, usize, usize)> { + let message = "cannot have both 'except' and 'except*' on the same 'try'".to_owned(); + let mut seen_except = false; + let mut seen_except_star = false; + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let bytes = line.as_bytes(); + let mut column = 0usize; + while matches!(bytes.get(column), Some(b' ' | b'\t' | b'\x0c')) { + column += 1; + } + let token_start = line_start + column; + if bytes.get(column..column + 7) == Some(b"except*") { + if seen_except { + return Some((message, token_start, token_start + 7)); + } + seen_except_star = true; + } else if starts_identifier(bytes, column, b"except") { + if seen_except_star { + return Some((message, token_start, token_start + 6)); + } + seen_except = true; + } + line_start += line.len(); + } + None +} + +fn non_printable_character_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + byte if byte.is_ascii_control() && !matches!(byte, b'\t' | b'\n' | b'\r' | b'\x0c') => { + return Some(( + format!("invalid non-printable character U+{byte:04X}"), + index, + index + 1, + )); + } + byte if byte >= 0x80 => { + let ch = source[index..].chars().next()?; + if ch.is_control() { + return Some(( + format!("invalid non-printable character U+{:04X}", ch as u32), + index, + index + ch.len_utf8(), + )); + } + index += ch.len_utf8(); + } + _ => index += 1, + } + } + None +} + +fn unterminated_string_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + let mut line = 1usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\n' => { + line += 1; + index += 1; + } + quote @ (b'\'' | b'"') => { + let start = index; + let start_line = line; + let quote_size = if bytes.get(index + 1) == Some("e) + && bytes.get(index + 2) == Some("e) + { + 3 + } else { + 1 + }; + index += quote_size; + let mut has_escaped_quote = false; + let mut closed = false; + while index < bytes.len() { + let c = bytes[index]; + if c == b'\n' { + if quote_size == 1 { + return Some(( + unterminated_string_message(line, false, has_escaped_quote), + start, + start + 1, + )); + } + line += 1; + index += 1; + } else if c == quote { + if quote_size == 3 { + if bytes.get(index + 1) == Some("e) + && bytes.get(index + 2) == Some("e) + { + index += 3; + closed = true; + break; + } + index += 1; + } else { + index += 1; + closed = true; + break; + } + } else if c == b'\\' { + if bytes.get(index + 1) == Some("e) { + has_escaped_quote = true; + } + index = (index + 2).min(bytes.len()); + } else { + index += 1; + } + } + if !closed { + let detected_line = if quote_size == 3 { line } else { start_line }; + return Some(( + unterminated_string_message( + detected_line, + quote_size == 3, + has_escaped_quote, + ), + start, + start + 1, + )); + } + } + _ => index += 1, + } + } + None +} + +fn invalid_interpolated_string_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + quote @ (b'\'' | b'"') => { + let Some(prefix) = interpolated_string_prefix(bytes, index) else { + index = skip_quoted_string(bytes, index); + continue; + }; + if let Some(error) = + single_quoted_format_spec_newline_error(bytes, index, quote, prefix) + { + return Some(error); + } + let Some((content_start, content_end)) = + quoted_string_content_range(bytes, index, quote) + else { + index = skip_quoted_string(bytes, index); + continue; + }; + if let Some(error) = + invalid_replacement_field_error(bytes, content_start, content_end, prefix) + { + return Some(error); + } + index = skip_quoted_string(bytes, index); + } + _ => index += 1, + } + } + None +} + +fn single_quoted_format_spec_newline_error( + bytes: &[u8], + quote_index: usize, + quote: u8, + prefix: &str, +) -> Option<(String, usize, usize)> { + if bytes.get(quote_index + 1) == Some("e) && bytes.get(quote_index + 2) == Some("e) { + return None; + } + + let (content_start, content_end) = quoted_string_content_range(bytes, quote_index, quote)?; + let mut index = content_start; + while index < content_end { + match bytes[index] { + b'{' if bytes.get(index + 1) == Some(&b'{') => index += 2, + b'}' if bytes.get(index + 1) == Some(&b'}') => index += 2, + b'{' => { + let expr_start = skip_ascii_whitespace(bytes, index + 1, content_end); + if let Some(separator) = replacement_field_separator(bytes, expr_start, content_end) + && bytes[separator] == b':' + { + let format_end = + replacement_field_closing_brace(bytes, separator + 1, content_end) + .unwrap_or(content_end); + if bytes[separator + 1..format_end].contains(&b'\n') { + return Some(( + format!( + "{prefix}: newlines are not allowed in format specifiers for single quoted {prefix}s" + ), + quote_index, + quote_index + 1, + )); + } + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn interpolated_string_prefix(bytes: &[u8], quote: usize) -> Option<&'static str> { + let prev = quote.checked_sub(1).and_then(|index| bytes.get(index))?; + let lower_prev = prev.to_ascii_lowercase(); + let (prefix_start, marker) = if matches!(lower_prev, b'f' | b't') { + if quote >= 2 && bytes[quote - 2].eq_ignore_ascii_case(&b'r') { + (quote - 2, lower_prev) + } else { + (quote - 1, lower_prev) + } + } else if lower_prev == b'r' + && quote >= 2 + && matches!(bytes[quote - 2].to_ascii_lowercase(), b'f' | b't') + { + (quote - 2, bytes[quote - 2].to_ascii_lowercase()) + } else { + return None; + }; + + if prefix_start > 0 && is_ascii_identifier_char(bytes[prefix_start - 1]) { + return None; + } + + Some(if marker == b'f' { + "f-string" + } else { + "t-string" + }) +} + +fn quoted_string_content_range( + bytes: &[u8], + quote_index: usize, + quote: u8, +) -> Option<(usize, usize)> { + let triple = + bytes.get(quote_index + 1) == Some("e) && bytes.get(quote_index + 2) == Some("e); + let quote_len = if triple { 3 } else { 1 }; + let content_start = quote_index + quote_len; + let mut index = content_start; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else if (triple + && bytes.get(index) == Some("e) + && bytes.get(index + 1) == Some("e) + && bytes.get(index + 2) == Some("e)) + || (!triple && bytes[index] == quote) + { + return Some((content_start, index)); + } else { + index += 1; + } + } + None +} + +fn invalid_replacement_field_error( + bytes: &[u8], + start: usize, + end: usize, + prefix: &str, +) -> Option<(String, usize, usize)> { + let mut index = start; + while index < end { + match bytes[index] { + b'{' if bytes.get(index + 1) == Some(&b'{') => index += 2, + b'}' if bytes.get(index + 1) == Some(&b'}') => index += 2, + b'{' => { + if let Some(error) = replacement_field_error(bytes, index, end, prefix) { + return Some(error); + } + index += 1; + } + _ => index += 1, + } + } + None +} + +fn replacement_field_error( + bytes: &[u8], + open: usize, + end: usize, + prefix: &str, +) -> Option<(String, usize, usize)> { + let expr_start = skip_ascii_whitespace(bytes, open + 1, end); + if let Some(backslash) = replacement_field_line_continuation(bytes, expr_start, end) { + return Some(( + "unexpected character after line continuation character".to_owned(), + backslash + 1, + (backslash + 2).min(end), + )); + } + if let Some(quote) = unterminated_string_in_replacement_field(bytes, expr_start, end) { + return Some(( + unterminated_string_message(1, false, false), + quote, + quote + 1, + )); + } + match bytes.get(expr_start).copied() { + Some(marker @ (b'=' | b'!' | b':' | b'}')) => { + return Some(( + format!( + "{prefix}: valid expression required before '{}'", + marker as char + ), + expr_start, + expr_start + 1, + )); + } + Some(_) => {} + None => { + return Some(( + format!("{prefix}: expecting a valid expression after '{{'"), + open, + open + 1, + )); + } + } + + if starts_identifier(bytes, expr_start, b"lambda") { + return Some(( + format!("{prefix}: lambda expressions are not allowed without parentheses"), + expr_start, + expr_start + b"lambda".len(), + )); + } + + if invalid_replacement_expression_start(bytes, expr_start, end) { + return Some(( + format!("{prefix}: expecting a valid expression after '{{'"), + open, + open + 1, + )); + } + + let Some(separator) = replacement_field_separator(bytes, expr_start, end) else { + return Some((format!("{prefix}: expecting '}}'"), open, open + 1)); + }; + + if bytes[separator] == b':' + && replacement_expression_has_parse_error(bytes, expr_start, separator) + { + return Some(("invalid syntax".to_owned(), expr_start, separator)); + } + + match bytes[separator] { + b'=' => invalid_debug_expression_error(bytes, separator, end, prefix), + b'!' => invalid_conversion_error(bytes, separator, end, prefix), + b':' => invalid_format_spec_error(bytes, separator, end, prefix), + b'}' => None, + _ => unreachable!(), + } +} + +fn replacement_field_line_continuation( + bytes: &[u8], + mut index: usize, + end: usize, +) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'\\' => return Some(index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' if level > 0 => { + level -= 1; + index += 1; + } + b'=' | b'!' | b':' | b'}' if level == 0 => return None, + _ => index += 1, + } + } + None +} + +fn unterminated_string_in_replacement_field( + bytes: &[u8], + mut index: usize, + end: usize, +) -> Option { + while index < end { + match bytes[index] { + quote @ (b'\'' | b'"') => { + let string_end = skip_quoted_string(bytes, index); + if string_end >= end && !bytes[index + 1..end].contains("e) { + return Some(index); + } + index = string_end; + } + _ => index += 1, + } + } + None +} + +fn replacement_expression_has_parse_error(bytes: &[u8], start: usize, end: usize) -> bool { + let Ok(expression) = ::core::str::from_utf8(&bytes[start..end]) else { + return false; + }; + parser::parse_expression(expression).is_err() +} + +fn invalid_replacement_expression_start(bytes: &[u8], index: usize, end: usize) -> bool { + if index >= end { + return true; + } + + if matches!( + bytes[index], + b'.' | b',' | b'*' | b'/' | b'%' | b'&' | b'|' | b'^' | b'<' | b'>' | b'@' + ) { + return true; + } + + if matches!(bytes[index], b'+' | b'-' | b'~') { + let operand = skip_ascii_whitespace(bytes, index + 1, end); + return !bytes.get(operand).is_some_and(|byte| { + *byte >= 0x80 + || *byte == b'_' + || byte.is_ascii_alphabetic() + || byte.is_ascii_digit() + || matches!(*byte, b'\'' | b'"' | b'(' | b'[' | b'{') + }); + } + + [ + b"and".as_slice(), + b"as".as_slice(), + b"else".as_slice(), + b"for".as_slice(), + b"if".as_slice(), + b"in".as_slice(), + b"is".as_slice(), + b"or".as_slice(), + ] + .iter() + .any(|keyword| starts_identifier(bytes, index, keyword)) +} + +fn replacement_field_separator(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' if level > 0 => { + level -= 1; + index += 1; + } + b'=' | b'!' | b':' | b'}' if level == 0 => return Some(index), + _ => index += 1, + } + } + None +} + +fn invalid_debug_expression_error( + bytes: &[u8], + equals: usize, + end: usize, + prefix: &str, +) -> Option<(String, usize, usize)> { + let next = equals + 1; + if next >= end || matches!(bytes[next], b'!' | b':' | b'}') { + return None; + } + Some(( + format!("{prefix}: expecting '!', or ':', or '}}'"), + next, + next.saturating_add(1).min(end), + )) +} + +fn invalid_conversion_error( + bytes: &[u8], + bang: usize, + end: usize, + prefix: &str, +) -> Option<(String, usize, usize)> { + let next = bang + 1; + if next >= end { + return Some((format!("{prefix}: expecting '}}'"), bang, bang + 1)); + } + + if bytes[next].is_ascii_whitespace() { + let following = skip_ascii_whitespace(bytes, next, end); + let message = if bytes + .get(following) + .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_') + { + "conversion type must come right after the exclamation mark" + } else { + "missing conversion character" + }; + return Some((format!("{prefix}: {message}"), next, next + 1)); + } + + if matches!(bytes[next], b':' | b'}') { + return Some(( + format!("{prefix}: missing conversion character"), + next, + next + 1, + )); + } + + if !bytes[next].is_ascii_alphabetic() && bytes[next] != b'_' { + return Some(( + format!("{prefix}: invalid conversion character"), + next, + next + 1, + )); + } + + let conversion_end = identifier_end(bytes, next, end); + let conversion = &bytes[next..conversion_end]; + if !matches!(conversion, b"s" | b"r" | b"a") { + let conversion = ::core::str::from_utf8(conversion).unwrap_or(""); + return Some(( + format!( + "{prefix}: invalid conversion character '{conversion}': expected 's', 'r', or 'a'" + ), + next, + conversion_end, + )); + } + + if conversion_end >= end || matches!(bytes[conversion_end], b':' | b'}') { + return None; + } + + Some(( + format!("{prefix}: expecting ':' or '}}'"), + conversion_end, + conversion_end + 1, + )) +} + +fn invalid_format_spec_error( + bytes: &[u8], + colon: usize, + end: usize, + prefix: &str, +) -> Option<(String, usize, usize)> { + if replacement_field_closing_brace(bytes, colon + 1, end).is_some() { + return None; + } + Some(( + format!("{prefix}: expecting '}}', or format specs"), + colon, + colon + 1, + )) +} + +fn replacement_field_closing_brace(bytes: &[u8], mut index: usize, end: usize) -> Option { + let mut level = 0usize; + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'{' => { + level += 1; + index += 1; + } + b'}' if level > 0 => { + level -= 1; + index += 1; + } + b'}' => return Some(index), + _ => index += 1, + } + } + None +} + +fn skip_ascii_whitespace(bytes: &[u8], mut index: usize, end: usize) -> usize { + while index < end && matches!(bytes[index], b' ' | b'\t' | b'\r' | b'\n' | 0x0c) { + index += 1; + } + index +} + +fn string_literal_end_at(bytes: &[u8], index: usize) -> Option { + match bytes.get(index).copied()? { + b'\'' | b'"' => Some(skip_quoted_string(bytes, index)), + first if first.is_ascii_alphabetic() => { + if matches!(bytes.get(index + 1), Some(b'\'' | b'"')) { + return string_literal_prefix(bytes, index, index + 1) + .then(|| skip_quoted_string(bytes, index + 1)); + } + if matches!(bytes.get(index + 2), Some(b'\'' | b'"')) { + return string_literal_prefix(bytes, index, index + 2) + .then(|| skip_quoted_string(bytes, index + 2)); + } + None + } + _ => None, + } +} + +fn string_literal_prefix(bytes: &[u8], start: usize, quote: usize) -> bool { + let prefix = &bytes[start..quote]; + let valid = matches!( + prefix, + b"b" | b"B" + | b"r" + | b"R" + | b"u" + | b"U" + | b"f" + | b"F" + | b"t" + | b"T" + | b"br" + | b"bR" + | b"Br" + | b"BR" + | b"rb" + | b"rB" + | b"Rb" + | b"RB" + | b"fr" + | b"fR" + | b"Fr" + | b"FR" + | b"rf" + | b"rF" + | b"Rf" + | b"RF" + | b"tr" + | b"tR" + | b"Tr" + | b"TR" + | b"rt" + | b"rT" + | b"Rt" + | b"RT" + ); + valid && (start == 0 || !is_ascii_identifier_char(bytes[start - 1])) +} + +fn invalid_expression_error(source: &str) -> Option<(String, usize, usize)> { + invalid_string_expression_error(source).or_else(|| missing_comma_expression_error(source)) +} + +fn invalid_string_expression_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + if let Some(first_string_end) = string_literal_end_at(bytes, index) { + let expr_start = skip_ascii_whitespace(bytes, first_string_end, bytes.len()); + if expression_atom_start(bytes, expr_start) + && let Some(expr_end) = adjacent_atom_end(bytes, expr_start) + { + let next = skip_ascii_whitespace(bytes, expr_end, bytes.len()); + if string_literal_end_at(bytes, next).is_some() { + return Some(( + "invalid syntax. Is this intended to be part of the string?".to_owned(), + expr_start, + expr_end, + )); + } + } + index = first_string_end; + } else { + index += 1; + } + } + None +} + +fn missing_comma_expression_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut stack: Vec = Vec::new(); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'#' { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } else if let Some(string_end) = string_literal_end_at(bytes, index) { + index = string_end; + } else { + match bytes[index] { + b'(' | b'[' | b'{' => { + if bytes[index] == b'[' && opening_bracket_is_class_type_params(bytes, index) { + let Some(close) = matching_delimiter(bytes, index, b']') else { + index += 1; + continue; + }; + index = close + 1; + continue; + } + stack.push(bytes[index]); + index += 1; + } + b')' | b']' | b'}' => { + stack.pop(); + index += 1; + } + _ if !stack.is_empty() && expression_continuation_keyword(bytes, index) => { + index = identifier_end(bytes, index, bytes.len()); + } + byte if !stack.is_empty() && expression_atom_start_byte(byte) => { + let atom_end = adjacent_atom_end(bytes, index).unwrap_or(index + 1); + let next = skip_ascii_whitespace(bytes, atom_end, bytes.len()); + if next > atom_end + && expression_atom_start(bytes, next) + && !expression_continuation_keyword(bytes, next) + { + return Some(( + "invalid syntax. Perhaps you forgot a comma?".to_owned(), + index, + next + 1, + )); + } + index = atom_end; + } + _ => index += 1, + } + } + } + None +} + +fn opening_bracket_is_class_type_params(bytes: &[u8], bracket: usize) -> bool { + let mut cursor = bracket; + while cursor > 0 && matches!(bytes[cursor - 1], b' ' | b'\t' | b'\x0c') { + cursor -= 1; + } + while cursor > 0 + && bytes + .get(cursor - 1) + .is_some_and(|byte| *byte >= 0x80 || is_ascii_identifier_char(*byte)) + { + cursor -= 1; + } + while cursor > 0 && matches!(bytes[cursor - 1], b' ' | b'\t' | b'\x0c') { + cursor -= 1; + } + cursor >= 5 && starts_identifier(bytes, cursor - 5, b"class") +} + +fn expression_continuation_keyword(bytes: &[u8], index: usize) -> bool { + [ + b"and".as_slice(), + b"else".as_slice(), + b"for".as_slice(), + b"if".as_slice(), + b"in".as_slice(), + b"is".as_slice(), + b"not".as_slice(), + b"or".as_slice(), + ] + .iter() + .any(|keyword| starts_identifier(bytes, index, keyword)) +} + +fn expression_atom_start(bytes: &[u8], index: usize) -> bool { + bytes + .get(index) + .is_some_and(|byte| expression_atom_start_byte(*byte)) + || string_literal_end_at(bytes, index).is_some() +} + +fn expression_atom_start_byte(byte: u8) -> bool { + byte >= 0x80 + || byte == b'_' + || byte.is_ascii_alphabetic() + || byte.is_ascii_digit() + || matches!(byte, b'\'' | b'"' | b'(' | b'[' | b'{') } -#[derive(Error, Debug)] -pub struct ParseError { - #[source] - pub error: ParseErrorType, - pub raw_location: ruff_text_size::TextRange, - pub location: SourceLocation, - pub end_location: SourceLocation, - pub source_path: String, - /// Set when the error is an unclosed bracket (converted from EOF). - pub is_unclosed_bracket: bool, +fn adjacent_atom_end(bytes: &[u8], index: usize) -> Option { + if let Some(string_end) = string_literal_end_at(bytes, index) { + return Some(string_end); + } + match bytes.get(index).copied()? { + byte if byte >= 0x80 || byte == b'_' || byte.is_ascii_alphabetic() => { + Some(identifier_end(bytes, index, bytes.len())) + } + byte if byte.is_ascii_digit() => { + let mut end = index + 1; + while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') { + end += 1; + } + Some(end) + } + b'(' | b'[' | b'{' => Some(index + 1), + _ => None, + } } -impl ::core::fmt::Display for ParseError { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - self.error.fmt(f) +fn unterminated_string_message( + detected_line: usize, + triple: bool, + has_escaped_quote: bool, +) -> String { + if triple { + format!("unterminated triple-quoted string literal (detected at line {detected_line})") + } else if has_escaped_quote { + format!( + "unterminated string literal (detected at line {detected_line}); perhaps you escaped the end quote?" + ) + } else { + format!("unterminated string literal (detected at line {detected_line})") } } -#[derive(Error, Debug)] -pub enum CompileError { - #[error(transparent)] - Codegen(#[from] codegen::error::CodegenError), - #[error(transparent)] - Parse(#[from] ParseError), +fn expected_opening_bracket(closing: char) -> char { + match closing { + ')' => '(', + ']' => '[', + '}' => '{', + _ => unreachable!(), + } } -impl CompileError { - #[must_use] - pub fn from_ruff_parse_error(error: parser::ParseError, source_file: &SourceFile) -> Self { - let source_code = source_file.to_source_code(); - let source_text = source_file.source_text(); - - // For EOF errors (unclosed brackets), find the unclosed bracket position - // and adjust both the error location and message - let mut is_unclosed_bracket = false; - let (error_type, location, end_location) = match &error.error { - ParseErrorType::Lexical(LexicalErrorType::Eof) => { - if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) { - let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32); - let loc = - source_code.source_location(bracket_text_size, PositionEncoding::Utf8); - let end_loc = SourceLocation { - line: loc.line, - character_offset: loc.character_offset.saturating_add(1), - }; - let msg = format!("'{bracket_char}' was never closed"); - is_unclosed_bracket = true; - (ParseErrorType::OtherError(msg), loc, end_loc) - } else { - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let end_loc = - source_code.source_location(error.location.end(), PositionEncoding::Utf8); - (error.error, loc, end_loc) - } - } - - ParseErrorType::Lexical(LexicalErrorType::IndentationError) => { - // For IndentationError, point the offset to the end of the line content - // instead of the beginning - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let line_idx = loc.line.to_zero_indexed(); - let line = source_text.split('\n').nth(line_idx).unwrap_or(""); - let line_end_col = line.chars().count() + 1; // 1-indexed, past last char - let end_loc = SourceLocation { - line: loc.line, - character_offset: ruff_source_file::OneIndexed::new(line_end_col) - .unwrap_or(loc.character_offset), - }; - (error.error, end_loc, end_loc) - } - ParseErrorType::ExpectedToken { expected, found } - if matches!((expected, found), (TokenKind::Comma, TokenKind::Int)) => - { - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let mut end_loc = - source_code.source_location(error.location.end(), PositionEncoding::Utf8); - - // If the error range ends at the start of a new line (column 1), - // adjust it to the end of the previous line - if end_loc.character_offset.get() == 1 && end_loc.line > loc.line { - let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); - end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); - end_loc.character_offset = end_loc.character_offset.saturating_add(1); - } - let msg = "invalid syntax. Perhaps you forgot a comma?".into(); - (ParseErrorType::OtherError(msg), loc, end_loc) - } - - ParseErrorType::InvalidAssignmentTarget => { - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let mut end_loc = - source_code.source_location(error.location.end(), PositionEncoding::Utf8); - - // If the error range ends at the start of a new line (column 1), - // adjust it to the end of the previous line - if end_loc.character_offset.get() == 1 && end_loc.line > loc.line { - let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); - end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); - end_loc.character_offset = end_loc.character_offset.saturating_add(1); - } - - let expr_str = source_file.source_text().slice(error.location); - - let msg = parser::parse_expression(expr_str).map_or_else( - |_| match expr_str { - "yield" => "assignment to yield expression not possible".into(), - _ => format!("cannot assign to {expr_str}"), - }, - |parsed| match *parsed.syntax().body { - ast::Expr::Call(_) => "cannot assign to function call".into(), - ast::Expr::BinOp(_) => "cannot assign to expression".into(), - ast::Expr::If(_) => "cannot assign to conditional expression".into(), - ast::Expr::Generator(_) => "cannot assign to generator expression".into(), - ast::Expr::StringLiteral(_) - | ast::Expr::BytesLiteral(_) - | ast::Expr::NumberLiteral(_) => { - "cannot assign to literal here. Maybe you meant '==' instead of '='?" - .into() - } - ast::Expr::EllipsisLiteral(_) => { - "cannot assign to ellipsis here. Maybe you meant '==' instead of '='?" - .into() - } - _ => format!("cannot assign to {expr_str}"), - }, - ); +fn bracket_syntax_error(source: &str) -> Option<(String, usize, usize, bool)> { + let mut stack: Vec<(char, usize, usize)> = Vec::new(); + let mut in_string = false; + let mut string_quote = '\0'; + let mut triple_quote = false; + let mut escape_next = false; + let mut is_raw_string = false; + let mut line = 1usize; + + let chars: Vec<(usize, char)> = source.char_indices().collect(); + let mut index = 0; + while index < chars.len() { + let (byte_offset, ch) = chars[index]; + + if ch == '\n' { + line += 1; + } + + if escape_next { + escape_next = false; + index += 1; + continue; + } - (ParseErrorType::OtherError(msg), loc, end_loc) + if in_string { + if ch == '\\' && !is_raw_string { + escape_next = true; + } else if triple_quote { + if ch == string_quote + && index + 2 < chars.len() + && chars[index + 1].1 == string_quote + && chars[index + 2].1 == string_quote + { + in_string = false; + index += 3; + continue; + } + } else if ch == string_quote { + in_string = false; } + index += 1; + continue; + } - ParseErrorType::InvalidNamedAssignmentTarget => { - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let mut end_loc = - source_code.source_location(error.location.end(), PositionEncoding::Utf8); + if ch == '#' { + while index < chars.len() && chars[index].1 != '\n' { + index += 1; + } + continue; + } - // If the error range ends at the start of a new line (column 1), - // adjust it to the end of the previous line - if end_loc.character_offset.get() == 1 && end_loc.line > loc.line { - let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); - end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); - end_loc.character_offset = end_loc.character_offset.saturating_add(1); + if ch == '\'' || ch == '"' { + is_raw_string = false; + for look_back in 1..=2.min(index) { + let prev = chars[index - look_back].1; + if matches!(prev, 'r' | 'R') { + is_raw_string = true; + break; } + if !matches!(prev, 'b' | 'B' | 'f' | 'F' | 'u' | 'U') { + break; + } + } + string_quote = ch; + if index + 2 < chars.len() && chars[index + 1].1 == ch && chars[index + 2].1 == ch { + triple_quote = true; + in_string = true; + index += 3; + continue; + } + triple_quote = false; + in_string = true; + index += 1; + continue; + } - let target = source_file.source_text().slice(error.location); - let msg = format!("cannot use assignment expressions with {target}"); - (ParseErrorType::OtherError(msg), loc, end_loc) + match ch { + '(' | '[' | '{' => stack.push((ch, byte_offset, line)), + ')' | ']' | '}' => { + let expected = expected_opening_bracket(ch); + let Some(&(opening, _, opening_line)) = stack.last() else { + return Some((format!("unmatched '{ch}'"), byte_offset, byte_offset, false)); + }; + if opening == expected { + stack.pop(); + } else { + let suffix = if opening_line != line { + format!(" on line {opening_line}") + } else { + String::new() + }; + return Some(( + format!( + "closing parenthesis '{ch}' does not match opening parenthesis '{opening}'{suffix}" + ), + byte_offset, + byte_offset, + false, + )); + } } + _ => {} + } - _ => { - let loc = - source_code.source_location(error.location.start(), PositionEncoding::Utf8); - let mut end_loc = - source_code.source_location(error.location.end(), PositionEncoding::Utf8); + index += 1; + } + + stack.last().map(|(opening, byte_offset, _)| { + ( + format!("'{opening}' was never closed"), + *byte_offset, + *byte_offset, + true, + ) + }) +} + +fn is_legacy_statement_expression_start(byte: u8) -> bool { + byte >= 0x80 + || byte == b'_' + || byte.is_ascii_alphabetic() + || byte.is_ascii_digit() + || matches!(byte, b'\'' | b'"' | b'{' | b'[') +} - // If the error range ends at the start of a new line (column 1), - // adjust it to the end of the previous line - if end_loc.character_offset.get() == 1 && end_loc.line > loc.line { - let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1); - end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8); - end_loc.character_offset = end_loc.character_offset.saturating_add(1); +fn legacy_statement_container_has_invalid_attribute(bytes: &[u8], start: usize) -> bool { + let Some(&opening) = bytes.get(start) else { + return false; + }; + if !matches!(opening, b'{' | b'[') { + return false; + } + + let mut index = start; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\n' | b';' if level == 0 => return false, + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + if level == 0 { + return false; + } + } + b'.' => { + let mut cursor = index + 1; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + if matches!(bytes.get(cursor), Some(b')' | b']' | b'}')) { + return true; } + index += 1; + } + _ => index += 1, + } + } + false +} - (error.error, loc, end_loc) +fn invalid_legacy_statement_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } } - }; + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'p' | b'e' => { + let keyword = if starts_identifier(bytes, index, b"print") { + Some("print") + } else if starts_identifier(bytes, index, b"exec") { + Some("exec") + } else { + None + }; + let Some(keyword) = keyword else { + index += 1; + continue; + }; + let after_keyword = index + keyword.len(); + if !matches!(bytes.get(after_keyword), Some(b' ' | b'\t' | b'\x0c')) { + index = after_keyword; + continue; + } + let mut cursor = after_keyword; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + if legacy_statement_container_has_invalid_attribute(bytes, cursor) { + index = after_keyword; + continue; + } + if bytes.get(cursor).is_some_and(|byte| { + *byte != b'(' && is_legacy_statement_expression_start(*byte) + }) { + return Some(( + format!( + "Missing parentheses in call to '{keyword}'. Did you mean {keyword}(...)?" + ), + index, + after_keyword, + )); + } + index = after_keyword; + } + _ => index += 1, + } + } + None +} - Self::Parse(ParseError { - error: error_type, - raw_location: error.location, - location, - end_location, - source_path: source_file.name().to_owned(), - is_unclosed_bracket, - }) +/// Return the syntax error for a decimal integer literal exceeding the configured limit. +/// +/// The parser has already distinguished integer literals from strings, comments, floats, and +/// complex numbers. Inspecting its tokens keeps the limit consistent for every source parsing +/// entry point without reimplementing Python's lexer here. +#[must_use] +pub fn long_decimal_integer_literal_error( + source_file: &SourceFile, + tokens: &Tokens, + max_str_digits: usize, +) -> Option { + if max_str_digits == 0 { + return None; } + tokens.iter().find_map(|token| { + if token.kind() != TokenKind::Int { + return None; + } + let literal = source_file.source_text().slice(token.range()); + if literal + .as_bytes() + .get(..2) + .is_some_and(|prefix| matches!(prefix, b"0x" | b"0X" | b"0o" | b"0O" | b"0b" | b"0B")) + { + return None; + } + let digits = literal.bytes().filter(u8::is_ascii_digit).count(); + (digits > max_str_digits).then(|| { + let start = token.range().start().to_usize(); + CompileError::from_source_error( + source_file, + format!( + "Exceeds the limit ({max_str_digits} digits) for integer string conversion: value has {digits} digits; use sys.set_int_max_str_digits() to increase the limit - Consider hexadecimal for huge integer literals to avoid decimal conversion limits." + ), + start, + start, + ) + }) + }) +} - #[must_use] - pub const fn location(&self) -> Option { - match self { - Self::Codegen(codegen_error) => codegen_error.location, - Self::Parse(parse_error) => Some(parse_error.location), +fn invalid_parenthesized_import_star_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'f' if starts_identifier(bytes, index, b"from") => { + let mut cursor = index + 4; + while cursor < bytes.len() && !matches!(bytes[cursor], b'\n' | b';') { + if starts_identifier(bytes, cursor, b"import") { + cursor += 6; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\r')) { + cursor += 1; + } + if bytes.get(cursor) == Some(&b'(') { + cursor += 1; + while cursor < bytes.len() + && !matches!(bytes[cursor], b')' | b'\n' | b';') + { + if bytes[cursor] == b'*' { + return Some(("invalid syntax".to_owned(), cursor, cursor + 1)); + } + cursor += 1; + } + } + break; + } + cursor += 1; + } + index = cursor; + } + _ => index += 1, } } + None +} - #[must_use] - pub const fn python_location(&self) -> (usize, usize) { - if let Some(location) = self.location() { - (location.line.get(), location.character_offset.get()) - } else { - (0, 0) +fn too_many_nested_parentheses_error(source: &str) -> Option<(String, usize, usize)> { + const MAXLEVEL: usize = 200; + + let bytes = source.as_bytes(); + let mut index = 0; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b'(' | b'[' | b'{' => { + if level >= MAXLEVEL { + return Some(("too many nested parentheses".to_owned(), index, index + 1)); + } + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ => index += 1, } } + None +} - #[must_use] - pub fn python_end_location(&self) -> Option<(usize, usize)> { - match self { - Self::Codegen(_) => None, - Self::Parse(parse_error) => Some(( - parse_error.end_location.line.get(), - parse_error.end_location.character_offset.get(), - )), +fn invalid_unparenthesized_yield_after_comma_error(source: &str) -> Option<(String, usize, usize)> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + b',' => { + let mut cursor = index + 1; + while matches!(bytes.get(cursor), Some(b' ' | b'\t' | b'\x0c')) { + cursor += 1; + } + if starts_identifier(bytes, cursor, b"yield") { + return Some(("invalid syntax".to_owned(), cursor, cursor + 5)); + } + index += 1; + } + _ => index += 1, } } + None +} - #[must_use] - pub fn source_path(&self) -> &str { - match self { - Self::Codegen(codegen_error) => &codegen_error.source_path, - Self::Parse(parse_error) => &parse_error.source_path, +fn post_parse_source_error( + source_file: &SourceFile, + tokens: &Tokens, + opts: &CompileOpts, +) -> Option { + if let Some((message, start, end)) = + too_many_nested_parentheses_error(source_file.source_text()) + { + return Some(CompileError::from_source_error( + source_file, + message, + start, + end, + )); + } + if let Some(error) = + long_decimal_integer_literal_error(source_file, tokens, opts.int_max_str_digits) + { + return Some(error); + } + invalid_call_argument_error(source_file.source_text()) + .or_else(|| invalid_match_mapping_rest_wildcard_error(source_file.source_text())) + .or_else(|| invalid_match_as_target_error(source_file.source_text())) + .or_else(|| invalid_unparenthesized_yield_after_comma_error(source_file.source_text())) + .or_else(|| invalid_parenthesized_import_star_error(source_file.source_text())) + .map(|(message, start, end)| { + CompileError::from_source_error(source_file, message, start, end) + }) +} + +fn is_compound_stmt(stmt: &ast::Stmt) -> bool { + matches!( + stmt, + ast::Stmt::FunctionDef(_) + | ast::Stmt::ClassDef(_) + | ast::Stmt::If(_) + | ast::Stmt::For(_) + | ast::Stmt::While(_) + | ast::Stmt::With(_) + | ast::Stmt::Try(_) + | ast::Stmt::Match(_) + ) +} + +fn single_mode_body_error(body: &[ast::Stmt], source_file: &SourceFile) -> Option { + let first = body.first()?; + let source_code = source_file.to_source_code(); + let first_start = source_code.source_location(first.range().start(), PositionEncoding::Utf8); + let first_end = source_code.source_location(first.range().end(), PositionEncoding::Utf8); + + if body.iter().skip(1).any(|stmt| { + source_code + .source_location(stmt.range().start(), PositionEncoding::Utf8) + .line + > first_start.line + }) { + return Some(CompileError::from_source_error( + source_file, + "multiple statements found while compiling a single statement".to_owned(), + first.range().end().to_usize(), + first.range().end().to_usize(), + )); + } + + if is_compound_stmt(first) + && first_start.line == first_end.line + && !ends_with_line_break(source_file.source_text()) + { + return Some(CompileError::from_source_error( + source_file, + "invalid syntax".to_owned(), + first.range().start().to_usize(), + first.range().start().to_usize(), + )); + } + None +} + +fn single_mode_source_error(ast: &ast::Mod, source_file: &SourceFile) -> Option { + let ast::Mod::Module(module) = ast else { + return None; + }; + single_mode_body_error(&module.body, source_file) +} + +fn ends_with_line_break(source: &str) -> bool { + source.ends_with('\n') || source.ends_with('\r') +} + +fn ends_with_implied_dedent(source: &str) -> bool { + let mut lexer = parser::lexer::lex(source, parser::Mode::Module); + let mut last_kind = TokenKind::EndOfFile; + loop { + let kind = lexer.next_token(); + if kind.is_eof() { + break; } + last_kind = kind; + } + matches!(last_kind, TokenKind::Dedent) +} + +/// Detect input that only parses because Ruff's lexer closes indentation at EOF. +/// +/// `PyCF_DONT_IMPLY_DEDENT` is used by `codeop` and interactive compile +/// paths to keep an indented block incomplete until a terminating newline is seen. +#[must_use] +pub fn dont_imply_dedent_source_error(source_file: &SourceFile) -> Option { + let source = source_file.source_text(); + if ends_with_line_break(source) || !ends_with_implied_dedent(source) { + return None; } + let eof = source.len(); + Some(CompileError::from_source_error( + source_file, + "incomplete input".to_owned(), + eof, + eof, + )) } /// Find the last unclosed opening bracket in source code. @@ -378,6 +5204,15 @@ fn _compile( source_file: SourceFile, mode: Mode, opts: CompileOpts, +) -> Result { + _compile_with_syntax_warning_handler(source_file, mode, opts, None) +} + +fn _compile_with_syntax_warning_handler<'a>( + source_file: SourceFile, + mode: Mode, + opts: CompileOpts, + syntax_warning_handler: Option<&'a mut compile::SyntaxWarningHandler<'a>>, ) -> Result { let parser_mode = match mode { Mode::Exec => parser::Mode::Module, @@ -386,10 +5221,49 @@ fn _compile( // since these are only different in terms of compilation Mode::Single | Mode::BlockExpr => parser::Mode::Module, }; - let parsed = parser::parse(source_file.source_text(), parser_mode.into()) - .map_err(|err| CompileError::from_ruff_parse_error(err, &source_file))?; + let parser_options = parser::ParseOptions::from(parser_mode); + let parsed = parser::parse(source_file.source_text(), parser_options) + .map_err(|err| CompileError::from_ruff_parse_error(err, &source_file, mode))?; + if opts.dont_imply_dedent + && matches!(mode, Mode::Single) + && let Some(error) = dont_imply_dedent_source_error(&source_file) + { + return Err(error); + } + if let Some(error) = post_parse_source_error(&source_file, parsed.tokens(), &opts) { + return Err(error); + } let ast = parsed.into_syntax(); - compile::compile_top(ast, source_file, mode, opts).map_err(|e| e.into()) + let single_mode_error = matches!(mode, Mode::Single) + .then(|| single_mode_source_error(&ast, &source_file)) + .flatten(); + let code = compile::compile_top_with_syntax_warning_handler( + ast, + source_file, + mode, + opts, + syntax_warning_handler, + ) + .map_err(CompileError::from)?; + if let Some(error) = single_mode_error { + return Err(error); + } + Ok(code) +} + +pub fn compile_with_syntax_warning_handler<'a>( + source: &str, + mode: Mode, + source_path: &str, + opts: CompileOpts, + syntax_warning_handler: &'a mut compile::SyntaxWarningHandler<'a>, +) -> Result { + let source = source.replace("\r\n", "\n"); + #[cfg(windows)] + let source = source.as_str(); + + let source_file = SourceFileBuilder::new(source_path, source).finish(); + _compile_with_syntax_warning_handler(source_file, mode, opts, Some(syntax_warning_handler)) } pub fn compile_symtable( @@ -408,15 +5282,31 @@ pub fn _compile_symtable( let res = match mode { Mode::Exec | Mode::Single | Mode::BlockExpr => { let ast = ruff_python_parser::parse_module(source_file.source_text()) - .map_err(|e| CompileError::from_ruff_parse_error(e, &source_file))?; - symboltable::SymbolTable::scan_program(&ast.into_syntax(), source_file.clone()) + .map_err(|e| CompileError::from_ruff_parse_error(e, &source_file, mode))?; + if let Some(error) = + post_parse_source_error(&source_file, ast.tokens(), &CompileOpts::default()) + { + return Err(error); + } + let ast = ast.into_syntax(); + if matches!(mode, Mode::Single) + && let Some(error) = single_mode_body_error(&ast.body, &source_file) + { + return Err(error); + } + symboltable::SymbolTable::scan_program(&ast, source_file.clone()) } Mode::Eval => { let ast = ruff_python_parser::parse( source_file.source_text(), parser::Mode::Expression.into(), ) - .map_err(|e| CompileError::from_ruff_parse_error(e, &source_file))?; + .map_err(|e| CompileError::from_ruff_parse_error(e, &source_file, mode))?; + if let Some(error) = + post_parse_source_error(&source_file, ast.tokens(), &CompileOpts::default()) + { + return Err(error); + } symboltable::SymbolTable::scan_expr( &ast.into_syntax().expect_expression(), source_file.clone(), @@ -437,6 +5327,21 @@ mod tests { dbg!(compiled.expect("compile error")); } + #[test] + fn dont_imply_dedent_requires_terminating_newline() { + let code = "if True:\n pass"; + + let opts = CompileOpts { + dont_imply_dedent: true, + ..CompileOpts::default() + }; + let err = compile(code, Mode::Single, "<>", opts.clone()).expect_err("compile succeeded"); + assert_eq!(err.to_string(), "incomplete input"); + + compile("if True:\n pass\n", Mode::Single, "<>", opts).expect("compile error"); + compile(code, Mode::Single, "<>", CompileOpts::default()).expect("compile error"); + } + #[test] fn compile_phello() { let code = r#" @@ -501,6 +5406,20 @@ def f(): dbg!(compiled.expect("compile error")); } + #[test] + fn compile_call_arg_lambda_default() { + let code = "signature((lambda a=10: a))"; + let compiled = compile(code, Mode::Exec, "<>", CompileOpts::default()); + dbg!(compiled.expect("compile error")); + } + + #[test] + fn compile_generic_function_parameter_default() { + let code = "def __repr__[T: str](self, default: T = '') -> str: pass"; + let compiled = compile(code, Mode::Exec, "<>", CompileOpts::default()); + dbg!(compiled.expect("compile error")); + } + #[test] fn compile_int() { let code = r#" diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index d5bae7eebed..94bb445fec6 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -796,8 +796,15 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: &Item) -> Resul quote! {} }; + // Forward a `traverse` option to the generated `#[pyclass]` so exception + // payloads with a manual `Traverse` impl are GC-tracked and traversed. + let traverse_attr = match class_meta.inner()._optional_str("traverse").ok().flatten() { + Some(value) => quote! { , traverse = #value }, + None => quote! {}, + }; + let ret = quote! { - #[pyclass(module = false, name = #class_name, base = #base_class_name)] + #[pyclass(module = false, name = #class_name, base = #base_class_name #traverse_attr)] #item #impl_pyclass }; @@ -1155,13 +1162,16 @@ where let slot_ident = Ident::new(&slot_ident.to_string().to_lowercase(), slot_ident.span()); let slot_name = slot_ident.to_string(); let tokens = { - const NON_ATOMIC_SLOTS: &[&str] = &["as_buffer"]; const POINTER_SLOTS: &[&str] = &["as_sequence", "as_mapping"]; const STATIC_GEN_SLOTS: &[&str] = &["as_number"]; - if NON_ATOMIC_SLOTS.contains(&slot_name.as_str()) { + if slot_name == "as_buffer" { + // bf_releasebuffer is not a separate function in RustPython; the + // exporter's BufferMethods already release. Only its presence is + // observable, and AsBuffer declares that. quote_spanned! { span => - slots.#slot_ident = Some(Self::#ident as _); + slots.#slot_ident.store(Some(Self::#ident as _)); + slots.has_release_buffer.store(Self::RELEASE_BUFFER); } } else if POINTER_SLOTS.contains(&slot_name.as_str()) { quote_spanned! { span => @@ -1433,6 +1443,11 @@ impl GetSetNursery { fn validate(&mut self) -> Result<()> { let mut errors = Vec::new(); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for ((name, _cfgs), (getter, setter)) in &self.map { if getter.is_none() { errors.push(err_span!( @@ -1442,6 +1457,7 @@ impl GetSetNursery { )); }; } + errors.into_result()?; self.validated = true; Ok(()) @@ -1525,6 +1541,11 @@ impl MemberNursery { fn validate(&mut self) -> Result<()> { let mut errors = Vec::new(); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (name, entry) in &self.map { if entry.getter.is_none() { errors.push(err_span!( @@ -1534,6 +1555,7 @@ impl MemberNursery { )); }; } + errors.into_result()?; self.validated = true; Ok(()) diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 60b2296cea7..1ee878c1313 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -63,7 +63,7 @@ impl ItemNursery { if !inserted { return Err(syn::Error::new( item.attr_name.span(), - format!("Duplicated #[py*] attribute found for {:?}", &item.py_names), + format!("Duplicated #[py*] attribute found for {:?}", item.py_names), )); } } @@ -465,8 +465,15 @@ impl ClassItemMeta { pub(crate) struct ExceptionItemMeta(ClassItemMeta); impl ItemMeta for ExceptionItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = - &["module", "name", "base", "unhashable", "ctx", "impl"]; + const ALLOWED_NAMES: &'static [&'static str] = &[ + "module", + "name", + "base", + "unhashable", + "ctx", + "impl", + "traverse", + ]; fn from_inner(inner: ItemMetaInner) -> Self { Self(ClassItemMeta(inner)) diff --git a/crates/host_env/Cargo.toml b/crates/host_env/Cargo.toml index 718e3f41aab..e26fdeafe0b 100644 --- a/crates/host_env/Cargo.toml +++ b/crates/host_env/Cargo.toml @@ -17,14 +17,27 @@ libc = { workspace = true } num-traits = { workspace = true } parking_lot = { workspace = true } paste = { workspace = true } +widestring = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } + +[target.'cfg(any(unix, target_os = "macos", target_os = "redox", target_os = "wasi"))'.dependencies] rustix = { workspace = true } [target.'cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))'.dependencies] num_cpus = "1.17.0" +[target.'cfg(not(any(target_os = "ios", target_os = "android", target_os = "windows", target_arch = "wasm32", target_os = "redox")))'.dependencies] +mac_address = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dns-lookup = { workspace = true } +gethostname = { workspace = true } +rustyline = { workspace = true } +socket2 = { workspace = true, features = ["all"] } +which = { workspace = true } + [target.'cfg(all(unix, not(target_os = "ios"), not(target_os = "redox")))'.dependencies] termios = { workspace = true } @@ -35,10 +48,13 @@ libloading = "0.9" [target.'cfg(all(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "android"), not(any(target_env = "musl", target_env = "sgx"))))'.dependencies] libffi = { workspace = true, features = ["system"] } +[target.'cfg(target_os = "macos")'.dependencies] +system-configuration = { workspace = true } + [target.'cfg(windows)'.dependencies] +memchr.workspace = true junction = { workspace = true } schannel = { workspace = true } -widestring = { workspace = true } windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Globalization", @@ -69,5 +85,8 @@ windows-sys = { workspace = true, features = [ "Win32_UI_WindowsAndMessaging", ] } +[build-dependencies] +cc = "1" + [lints] workspace = true diff --git a/crates/host_env/build.rs b/crates/host_env/build.rs new file mode 100644 index 00000000000..d04d48f1621 --- /dev/null +++ b/crates/host_env/build.rs @@ -0,0 +1,72 @@ +//! Like CPython's `HAVE_ALTZONE`, it detects the presence of `altzone` in `time.h` at build time. + +#![allow( + clippy::disallowed_methods, + reason = "build scripts cannot use rustpython-host_env" +)] + +use std::{env, fs, path::PathBuf}; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rustc-check-cfg=cfg(has_altzone)"); + + let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + + if target_env == "msvc" || target_arch == "wasm32" { + return; + } + + let host = env::var("HOST").unwrap_or_default(); + let target = env::var("TARGET").unwrap_or_default(); + // cc::Build resolves CC_, CC_, TARGET_CC, then CC. + if host != target && !has_target_c_compiler(&target) { + return; + } + + if probe_altzone() { + println!("cargo:rustc-cfg=has_altzone"); + } +} + +/// Whether any compiler env var that `cc::Build` would consult for the target is set. +fn has_target_c_compiler(target: &str) -> bool { + let underscored = target.replace(['-', '.'], "_"); + env::var_os(format!("CC_{target}")).is_some() + || env::var_os(format!("CC_{underscored}")).is_some() + || env::var_os("TARGET_CC").is_some() + || env::var_os("CC").is_some() +} + +/// Check corresponding to `AC_TRY_COMPILE(... altzone ...)` in CPython's `configure`. +fn probe_altzone() -> bool { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + let src = out_dir.join("probe_altzone.c"); + let obj = out_dir.join("probe_altzone.o"); + + if fs::write( + &src, + "#include \nint main(void) { return (int)altzone; }\n", + ) + .is_err() + { + return false; + } + + let Ok(compiler) = cc::Build::new().try_get_compiler() else { + return false; + }; + + let mut cmd = compiler.to_command(); + if compiler.is_like_msvc() { + cmd.arg("/c").arg(&src).arg(format!("/Fo{}", obj.display())); + } else { + cmd.arg("-c").arg(&src).arg("-o").arg(&obj); + } + + match cmd.output() { + Ok(output) => output.status.success(), + Err(_) => false, + } +} diff --git a/crates/host_env/src/crt_fd.rs b/crates/host_env/src/crt_fd.rs index c49d661b37a..f06c5aad984 100644 --- a/crates/host_env/src/crt_fd.rs +++ b/crates/host_env/src/crt_fd.rs @@ -5,7 +5,7 @@ use alloc::fmt; use core::cmp; use std::{ffi, io}; -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] use std::os::fd::AsFd; #[cfg(not(windows))] use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}; @@ -112,7 +112,7 @@ mod win { } #[inline] - pub(super) fn as_raw_fd(&self) -> Raw { + pub(super) fn as_raw_fd(self) -> Raw { self.fd } } @@ -140,12 +140,13 @@ pub struct Borrowed<'fd> { inner: BorrowedInner<'fd>, } -impl<'fd> PartialEq for Borrowed<'fd> { +impl PartialEq for Borrowed<'_> { fn eq(&self, other: &Self) -> bool { self.as_raw() == other.as_raw() } } -impl<'fd> Eq for Borrowed<'fd> {} + +impl Eq for Borrowed<'_> {} impl fmt::Debug for Borrowed<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -208,49 +209,49 @@ impl Owned { } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl From for OwnedFd { fn from(fd: Owned) -> Self { fd.inner } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl From for Owned { fn from(fd: OwnedFd) -> Self { Self { inner: fd } } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl AsFd for Owned { fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl AsRawFd for Owned { fn as_raw_fd(&self) -> RawFd { self.as_raw() } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl FromRawFd for Owned { unsafe fn from_raw_fd(fd: RawFd) -> Self { unsafe { Self::from_raw(fd) } } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl IntoRawFd for Owned { fn into_raw_fd(self) -> RawFd { self.into_raw() } } -impl<'fd> Borrowed<'fd> { +impl Borrowed<'_> { /// Create a `crt_fd::Borrowed` from a raw file descriptor. /// /// # Safety @@ -286,28 +287,28 @@ impl<'fd> Borrowed<'fd> { } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl<'fd> From> for BorrowedFd<'fd> { fn from(fd: Borrowed<'fd>) -> Self { fd.inner } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl<'fd> From> for Borrowed<'fd> { fn from(fd: BorrowedFd<'fd>) -> Self { Self { inner: fd } } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl AsFd for Borrowed<'_> { fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl AsRawFd for Borrowed<'_> { fn as_raw_fd(&self) -> RawFd { self.as_raw() @@ -354,9 +355,8 @@ pub fn ftruncate(fd: Borrowed<'_>, len: Offset) -> io::Result<()> { cfg_select! { windows => { if ret != 0 { - // _chsize_s returns errno directly, convert to Windows error code - let winerror = crate::os::errno_to_winerror(ret); - return Err(io::Error::from_raw_os_error(winerror)); + // _chsize_s returns errno directly; preserve it exactly. + return Err(crate::os::io_error_from_errno(ret)); } } _ => cvt(ret)?, diff --git a/crates/host_env/src/ctypes.rs b/crates/host_env/src/ctypes.rs index 9bfd41c2818..a038e7a6d49 100644 --- a/crates/host_env/src/ctypes.rs +++ b/crates/host_env/src/ctypes.rs @@ -3,6 +3,7 @@ use core::ffi::{ CStr, c_char, c_double, c_float, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort, c_void, }; +use core::ptr::NonNull; #[cfg(all( any( target_os = "linux", @@ -24,7 +25,7 @@ use libffi::middle::Type; ))] use libffi::{ low, - middle::{Arg, Cif, Closure, CodePtr}, + middle::{Arg, Cif, Closure, CodePtr, Ret}, }; #[cfg(any(unix, windows))] use libloading::Library; @@ -36,6 +37,7 @@ use rustpython_wtf8::Wtf8; use rustpython_wtf8::Wtf8Buf; #[cfg(any(unix, windows))] use std::{collections::HashMap, ffi::OsStr, sync::OnceLock}; +use widestring::WideCStr; #[cfg(all( any( @@ -392,33 +394,11 @@ pub fn dyld_shared_cache_contains_path(path: &str) -> Result usize { - #[cfg(any(unix, windows, target_os = "wasi"))] - { - unsafe { libc::strlen(ptr) } - } - #[cfg(not(any(unix, windows, target_os = "wasi")))] - { - let mut len = 0; - while unsafe { *ptr.add(len) } != 0 { - len += 1; - } - len - } -} - /// # Safety /// /// `ptr` must be valid to read until the first NUL wide character. -pub unsafe fn wcslen(ptr: *const WChar) -> usize { - let mut len = 0; - while unsafe { *ptr.add(len) } != 0 as WChar { - len += 1; - } - len +pub unsafe fn wcslen(ptr: NonNull) -> usize { + unsafe { WideCStr::from_ptr_str(ptr.as_ptr().cast()).len() } } /// # Safety @@ -440,7 +420,7 @@ pub fn read_pointer_from_buffer(buffer: &[u8]) -> usize { pub const WCHAR_SIZE: usize = core::mem::size_of::(); #[inline] -pub fn wchar_from_bytes(bytes: &[u8]) -> Option { +pub const fn wchar_from_bytes(bytes: &[u8]) -> Option { if bytes.len() < WCHAR_SIZE { return None; } @@ -524,20 +504,17 @@ pub fn encode_wtf8_to_wchar_padded(s: &Wtf8, size: usize) -> Vec { } pub fn wchar_null_terminated_bytes(s: &Wtf8) -> Vec { - let wchars: Vec = s - .code_points() - .map(|cp| cp.to_u32() as WChar) - .chain(core::iter::once(0)) - .collect(); - vec_into_bytes(wchars) -} - -pub fn vec_into_bytes(vec: Vec) -> Vec { - let len = vec.len() * core::mem::size_of::(); - let cap = vec.capacity() * core::mem::size_of::(); - let ptr = vec.as_ptr() as *mut u8; - core::mem::forget(vec); - unsafe { Vec::from_raw_parts(ptr, len, cap) } + if size_of::() == 2 { + // We can't cast u32 to WChar because it would truncate the value on platforms where WChar + // is two bytes. Wtf8::encode_wide does all of the hard work for us, so all we have to do + // is split the bytes. + utf16z_bytes(s) + } else { + s.code_points() + .flat_map(|cp| (cp.to_u32() as WChar).to_ne_bytes()) + .chain((0 as WChar).to_ne_bytes()) + .collect() + } } pub enum IntegerValue { @@ -652,21 +629,6 @@ pub enum FfiValue { Pointer(usize), } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub enum CallResult { - Void, - Pointer(usize), - Value(low::ffi_arg), -} - #[cfg(all( any( target_os = "linux", @@ -1123,7 +1085,10 @@ pub fn simple_storage_value_to_bytes_endian( } pub fn utf16z_bytes(s: &Wtf8) -> Vec { - vec_into_bytes::(s.encode_wide().chain(core::iter::once(0)).collect()) + s.encode_wide() + .flat_map(|cp| cp.to_ne_bytes()) + .chain(0u16.to_ne_bytes()) + .collect() } pub fn null_terminated_bytes(bytes: &[u8]) -> Vec { @@ -1324,10 +1289,10 @@ pub unsafe fn callback_arg_value(type_code: Option<&str>, ptr: *const c_void) -> } Some("Z") => { let wstr_ptr = unsafe { *(ptr as *const *const WChar) }; - if wstr_ptr.is_null() { - DecodedValue::None - } else { + if let Some(wstr_ptr) = NonNull::new(wstr_ptr.cast_mut()) { DecodedValue::String(unsafe { read_wide_string(wstr_ptr) }.to_string()) + } else { + DecodedValue::None } } Some("P") => DecodedValue::Pointer(unsafe { *(ptr as *const usize) }), @@ -1537,78 +1502,6 @@ pub fn ffi_type_from_code(ty: &str) -> Option { } } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_from_tag(tag: u8) -> Type { - match tag { - b'c' | b'b' => Type::i8(), - b'B' | b'?' => Type::u8(), - b'h' | b'v' => Type::i16(), - b'H' => Type::u16(), - b'i' => Type::i32(), - b'I' => Type::u32(), - b'l' => { - if core::mem::size_of::() == 8 { - Type::i64() - } else { - Type::i32() - } - } - b'L' => { - if core::mem::size_of::() == 8 { - Type::u64() - } else { - Type::u32() - } - } - b'q' => Type::i64(), - b'Q' => Type::u64(), - b'f' => Type::f32(), - b'd' | b'g' => Type::f64(), - b'u' => { - if core::mem::size_of::() == 2 { - Type::u16() - } else { - Type::u32() - } - } - _ => Type::pointer(), - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_from_format(fmt: &str) -> Type { - match fmt.trim_start_matches(['<', '>', '!', '@', '=']) { - "b" => Type::i8(), - "B" => Type::u8(), - "h" => Type::i16(), - "H" => Type::u16(), - "i" | "l" => Type::i32(), - "I" | "L" => Type::u32(), - "q" => Type::i64(), - "Q" => Type::u64(), - "f" => Type::f32(), - "d" => Type::f64(), - "P" | "z" | "Z" | "O" => Type::pointer(), - _ => Type::u8(), - } -} - #[cfg(all( any( target_os = "linux", @@ -1687,119 +1580,6 @@ pub fn ffi_void_type() -> Type { Type::void() } -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_for_return_size(size: usize) -> Type { - if size <= 4 { - Type::i32() - } else if size <= 8 { - Type::i64() - } else { - Type::pointer() - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CTypeParamKind { - Structure, - Union, - Array, - Pointer, - Simple, -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn ffi_type_for_layout( - kind: CTypeParamKind, - ffi_field_types: &[Type], - size: usize, - length: usize, - format: Option<&str>, -) -> Type { - const MAX_FFI_STRUCT_SIZE: usize = 1024 * 1024; - - match kind { - CTypeParamKind::Structure | CTypeParamKind::Union => { - if !ffi_field_types.is_empty() { - Type::structure(ffi_field_types.iter().cloned()) - } else if size <= MAX_FFI_STRUCT_SIZE { - ffi_byte_struct(size) - } else { - ffi_pointer_type() - } - } - CTypeParamKind::Array => { - if size > MAX_FFI_STRUCT_SIZE || length > MAX_FFI_STRUCT_SIZE { - ffi_pointer_type() - } else if let Some(fmt) = format { - ffi_repeat_type(ffi_type_from_format(fmt), length) - } else { - ffi_byte_struct(size) - } - } - CTypeParamKind::Pointer => ffi_pointer_type(), - CTypeParamKind::Simple => { - if let Some(fmt) = format { - ffi_type_from_format(fmt) - } else { - Type::u8() - } - } - } -} - -#[cfg(all( - any( - target_os = "linux", - target_os = "macos", - target_os = "windows", - target_os = "android" - ), - not(any(target_env = "musl", target_env = "sgx")) -))] -pub fn callproc( - code_ptr: CodePtr, - ffi_arg_types: Vec, - ffi_return_type: Type, - ffi_args: &[Arg<'_>], - restype_is_none: bool, - is_pointer_return: bool, -) -> CallResult { - let cif = Cif::new(ffi_arg_types, ffi_return_type); - if restype_is_none { - unsafe { cif.call::<()>(code_ptr, ffi_args) }; - CallResult::Void - } else if is_pointer_return { - CallResult::Pointer(unsafe { cif.call::(code_ptr, ffi_args) }) - } else { - CallResult::Value(unsafe { cif.call::(code_ptr, ffi_args) }) - } -} - #[cfg(all( any( target_os = "linux", @@ -2052,6 +1832,63 @@ impl Drop for CallbackThunk { } } +/// Type codes whose value is a pointer (drives pointer-return decoding and +/// TYPEFLAG_ISPOINTER). +pub fn simple_type_is_pointer(code: &str) -> bool { + matches!(code, "z" | "Z" | "P" | "s" | "X" | "O") +} + +/// All valid ctypes simple type codes on this platform. +// +// TODO: the vm's `SIMPLE_TYPE_CHARS` const (crates/vm/src/stdlib/_ctypes/simple.rs) +// should adopt this as the single source of truth. +pub fn simple_type_chars() -> &'static str { + #[cfg(windows)] + { + // spell-checker: disable-next-line + "cbBhHiIlLdfuzZqQPXOv?g" + } + #[cfg(not(windows))] + { + // spell-checker: disable-next-line + "cbBhHiIlLdfuzZqQPOv?g" + } +} + +/// Recursive layout of a ctypes type: memory shape independent of any object +/// model, used to lower by-value aggregate arguments and aggregate returns for +/// [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CTypeLayout { + /// Simple type identified by its ctypes code ('i', 'd', 'P', 'u', ...). + Simple(char), + /// Any pointer-kind field (`POINTER(T)`, `c_void_p`/`z`/`Z`, function pointer). + Pointer, + /// Struct with per-field layouts in declaration order; `size` is the total + /// size including trailing padding. + Struct { fields: Vec, size: usize }, + /// Union, lowered to a size-matched byte struct (libffi has no union kind, so + /// register classification of float-only unions is approximate). + Union { fields: Vec, size: usize }, + /// Fixed-length array; only meaningful nested inside an aggregate. + Array { + element: Box, + length: usize, + size: usize, + }, + /// No field information available: a size-matched byte struct fallback. + Opaque { size: usize }, +} + #[cfg(all( any( target_os = "linux", @@ -2061,20 +1898,325 @@ impl Drop for CallbackThunk { ), not(any(target_env = "musl", target_env = "sgx")) ))] -pub fn call_result_bytes(raw_result: &CallResult) -> Option<(Vec, usize)> { - match raw_result { - CallResult::Void => None, - CallResult::Pointer(ptr) => { - let bytes = ptr.to_ne_bytes(); - Some((bytes.to_vec(), core::mem::size_of::())) +impl CTypeLayout { + /// Total size in bytes. + pub fn size(&self) -> usize { + match self { + Self::Simple(code) => { + let mut buf = [0u8; 4]; + simple_type_size(code.encode_utf8(&mut buf)).unwrap_or(0) + } + Self::Pointer => POINTER_SIZE, + Self::Struct { size, .. } + | Self::Union { size, .. } + | Self::Array { size, .. } + | Self::Opaque { size } => *size, } - CallResult::Value(val) => { - let bytes = val.to_ne_bytes(); - Some((bytes.to_vec(), core::mem::size_of_val(val))) + } + + /// Lower to a libffi type. `Err` if a simple code is unrecognized. + fn to_ffi_type(&self) -> Result { + match self { + Self::Simple(code) => { + let mut buf = [0u8; 4]; + let code = code.encode_utf8(&mut buf); + ffi_type_from_code(code).ok_or_else(|| CallError::UnknownTypeCode(code.to_string())) + } + Self::Pointer => Ok(ffi_pointer_type()), + Self::Struct { fields, .. } => { + let mut ffi_fields = Vec::with_capacity(fields.len()); + for field in fields { + ffi_fields.push(field.to_ffi_type()?); + } + Ok(Type::structure(ffi_fields)) + } + Self::Array { + element, length, .. + } => Ok(ffi_repeat_type(element.to_ffi_type()?, *length)), + Self::Union { size, .. } | Self::Opaque { size } => Ok(ffi_byte_struct(*size)), } } } +/// One argument of a foreign call. Buffers are borrowed and must outlive the +/// call; keeping their owners alive is the caller's responsibility. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy)] +pub enum CallArg<'a> { + /// A value typed by a ctypes simple type code, as its raw native-endian + /// buffer (at least `simple_type_size(code)` bytes relevant). + Typed { code: &'a str, buffer: &'a [u8] }, + /// Untyped Python int (ConvParam default: C int). + Int(i32), + /// Untyped Python float (ConvParam default: C double). + Double(f64), + /// Address-valued argument (pointer decay, byref, bytes/str copies, NULL = 0). + Pointer(usize), + /// By-value aggregate: layout plus its raw bytes (`buffer.len() >= layout.size()`). + Aggregate { + layout: &'a CTypeLayout, + buffer: &'a [u8], + }, +} + +/// Return-type selector for [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy)] +pub enum CallRet<'a> { + /// restype is None: the call returns void. + Void, + /// A ctypes simple type code. Pointer-kind codes (`simple_type_is_pointer`) + /// yield [`CallValue::Pointer`]; everything else [`CallValue::Scalar`]. + Code(&'a str), + /// A pointer-typed return without a driving code (`POINTER(T)`, function + /// pointer). + Pointer, + /// A by-value aggregate return. + Aggregate(&'a CTypeLayout), +} + +/// Per-call error-swapping options. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, Copy, Default)] +pub struct CallOptions { + /// Swap the ctypes-local errno around the raw call (unix; ignored on windows). + pub use_errno: bool, + /// Swap the ctypes-local last error around the raw call (windows; ignored + /// elsewhere). + pub use_last_error: bool, +} + +/// Result of a foreign call. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug)] +pub enum CallValue { + /// Void return. + Void, + /// Raw return-register image, native endian (register-sized: 8 bytes on + /// 64-bit). Decode with [`decode_type_code`]. + Scalar(Vec), + /// Pointer-valued return. + Pointer(usize), + /// Exactly `layout.size()` bytes of a returned aggregate. + Aggregate(Vec), +} + +/// Errors from [`call`]. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallError { + NullFunctionPointer, + UnknownTypeCode(String), + /// An aggregate argument's buffer was shorter than its layout size. + BufferTooSmall { + expected: usize, + got: usize, + }, +} + +/// Perform a foreign call: handles scalar, pointer, and by-value aggregate +/// arguments, and void / scalar / pointer / aggregate returns. +#[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) +))] +pub fn call( + addr: usize, + args: &[CallArg<'_>], + ret: CallRet<'_>, + options: CallOptions, +) -> Result { + enum Lowered<'a> { + Scalar(FfiValue), + Aggregate(&'a [u8]), + } + + let code_ptr = code_ptr_from_addr(addr).ok_or(CallError::NullFunctionPointer)?; + + // Pass 1: argument types + owned scalar values / borrowed aggregate buffers. + let mut ffi_arg_types: Vec = Vec::with_capacity(args.len()); + let mut lowered: Vec> = Vec::with_capacity(args.len()); + for arg in args { + match arg { + CallArg::Typed { code, buffer } => { + let ty = ffi_type_from_code(code) + .ok_or_else(|| CallError::UnknownTypeCode((*code).to_string()))?; + ffi_arg_types.push(ty); + lowered.push(Lowered::Scalar(ffi_value_from_type_code(code, buffer))); + } + CallArg::Int(value) => { + ffi_arg_types.push(ffi_i32_type()); + lowered.push(Lowered::Scalar(FfiValue::I32(*value))); + } + CallArg::Double(value) => { + ffi_arg_types.push(ffi_f64_type()); + lowered.push(Lowered::Scalar(FfiValue::F64(*value))); + } + CallArg::Pointer(value) => { + ffi_arg_types.push(ffi_pointer_type()); + lowered.push(Lowered::Scalar(FfiValue::Pointer(*value))); + } + CallArg::Aggregate { layout, buffer } => { + let expected = layout.size(); + if buffer.len() < expected { + return Err(CallError::BufferTooSmall { + expected, + got: buffer.len(), + }); + } + ffi_arg_types.push(layout.to_ffi_type()?); + lowered.push(Lowered::Aggregate(buffer)); + } + } + } + + let ffi_return_type = match ret { + CallRet::Void => ffi_void_type(), + CallRet::Code(code) => { + ffi_type_from_code(code).ok_or_else(|| CallError::UnknownTypeCode(code.to_string()))? + } + CallRet::Pointer => ffi_pointer_type(), + CallRet::Aggregate(layout) => layout.to_ffi_type()?, + }; + + // Pass 2: borrow the completed `lowered` as libffi Args. No reallocation can + // now invalidate the scalar borrows; aggregate Args point at caller buffers. + let ffi_args: Vec> = lowered + .iter() + .map(|arg| match arg { + Lowered::Scalar(value) => ffi_arg_from_value(value), + // `.first()` avoids indexing an empty buffer (a zero-sized by-value + // aggregate); libffi reads nothing for a zero-size type. + Lowered::Aggregate(buffer) => Arg::new(buffer.first().unwrap_or(&0u8)), + }) + .collect(); + + let cif = Cif::new(ffi_arg_types, ffi_return_type); + + // Allocate the aggregate return buffer outside the error-swap window so no + // allocation runs between the raw call and the errno/last-error capture. + // libffi requires this buffer be at least `ffi_arg`-sized and suitably + // aligned; a `u64` slice guarantees both. + let mut aggregate_buffer: Vec = match ret { + CallRet::Aggregate(layout) => vec![0u64; core::cmp::max(layout.size(), 8).div_ceil(8)], + _ => Vec::new(), + }; + + enum RawResult { + Void, + Pointer(usize), + Scalar(u64), + Aggregate, + } + + let mut invoke = || -> RawResult { + match ret { + CallRet::Void => { + unsafe { cif.call::<()>(code_ptr, &ffi_args) }; + RawResult::Void + } + CallRet::Code(code) if simple_type_is_pointer(code) => { + RawResult::Pointer(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Code(_) => { + // Capture a full register (`u64`), not `low::ffi_arg`: the + // `libffi_sys` binding types `ffi_arg` as `c_ulong`, which is + // 4 bytes under LLP64 (Windows x64) and would truncate 8-byte + // returns (`q`/`Q`/`d`). `decode_type_code` reads the leading + // bytes the type code needs. + RawResult::Scalar(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Pointer => { + RawResult::Pointer(unsafe { cif.call::(code_ptr, &ffi_args) }) + } + CallRet::Aggregate(_) => { + unsafe { + cif.call_return_into(code_ptr, &ffi_args, Ret::new(&mut aggregate_buffer[..])); + } + RawResult::Aggregate + } + } + }; + + #[cfg(not(windows))] + let raw = if options.use_errno { + with_swapped_errno(invoke) + } else { + invoke() + }; + + #[cfg(windows)] + let raw = if options.use_last_error { + with_swapped_last_error(invoke) + } else { + invoke() + }; + + let result = match raw { + RawResult::Void => CallValue::Void, + RawResult::Pointer(ptr) => CallValue::Pointer(ptr), + RawResult::Scalar(value) => CallValue::Scalar(value.to_ne_bytes().to_vec()), + RawResult::Aggregate => { + let size = match ret { + CallRet::Aggregate(layout) => layout.size(), + _ => 0, + }; + let bytes: Vec = aggregate_buffer + .iter() + .flat_map(|word| word.to_ne_bytes()) + .collect(); + CallValue::Aggregate(bytes[..size].to_vec()) + } + }; + + Ok(result) +} + /// # Safety /// /// `ptr` must point to `len` readable bytes. @@ -2110,8 +2252,7 @@ pub unsafe fn borrowed_slice_as_mut(slice: &[u8]) -> &mut [u8] { pub fn wide_chars_to_wtf8(wchars: &[WChar]) -> Wtf8Buf { #[cfg(windows)] { - let wide: Vec = wchars.to_vec(); - Wtf8Buf::from_wide(&wide) + Wtf8Buf::from_wide(wchars) } #[cfg(not(windows))] { @@ -2130,10 +2271,10 @@ pub fn wide_chars_to_wtf8(wchars: &[WChar]) -> Wtf8Buf { /// # Safety /// /// `ptr` must be a valid NUL-terminated wide C string. -pub unsafe fn read_wide_string(ptr: *const WChar) -> Wtf8Buf { - let len = unsafe { wcslen(ptr) }; - let wchars = unsafe { core::slice::from_raw_parts(ptr, len) }; - wide_chars_to_wtf8(wchars) +pub unsafe fn read_wide_string(ptr: NonNull) -> Wtf8Buf { + // SAFETY: WideCStr does not assume an encoding. + let wchars = unsafe { WideCStr::from_ptr_str(ptr.as_ptr().cast()) }; + Wtf8Buf::from_string(wchars.to_string_lossy()) } /// # Safety @@ -2151,18 +2292,15 @@ pub unsafe fn read_c_string_from_address(addr: usize) -> Option> { /// /// `addr` must either be zero or a valid NUL-terminated wide C string pointer. pub unsafe fn read_wide_string_from_address(addr: usize) -> Option { - if addr == 0 { - None - } else { - Some(unsafe { read_wide_string(addr as *const WChar) }) - } + let ptr = NonNull::new(addr as *mut WChar)?; + Some(unsafe { read_wide_string(ptr) }) } /// # Safety /// /// `ptr` must point to `len` readable wide characters. -pub unsafe fn read_wide_string_with_len(ptr: *const WChar, len: usize) -> Wtf8Buf { - let wchars = unsafe { core::slice::from_raw_parts(ptr, len) }; +pub unsafe fn read_wide_string_with_len(ptr: NonNull, len: usize) -> Wtf8Buf { + let wchars = unsafe { core::slice::from_raw_parts(ptr.as_ptr(), len) }; wide_chars_to_wtf8(wchars) } @@ -2186,13 +2324,12 @@ pub fn string_at(ptr: usize, size: isize) -> Result, StringAtError> { } pub fn wstring_at(ptr: usize, size: isize) -> Result { - if ptr == 0 { + let Some(ptr) = NonNull::new(ptr as *mut WChar) else { return Err(StringAtError::NullPointer); - } - let w_ptr = ptr as *const WChar; + }; if size < 0 { // SAFETY: caller passed a non-null NUL-terminated wide string pointer. - return Ok(unsafe { read_wide_string(w_ptr) }); + return Ok(unsafe { read_wide_string(ptr) }); } let len = { let size_usize = size as usize; @@ -2202,7 +2339,7 @@ pub fn wstring_at(ptr: usize, size: isize) -> Result { size_usize }; // SAFETY: caller requested exactly `len` readable wide characters from non-null pointer. - Ok(unsafe { read_wide_string_with_len(w_ptr, len) }) + Ok(unsafe { read_wide_string_with_len(ptr, len) }) } /// # Safety @@ -2252,14 +2389,14 @@ pub unsafe fn read_pointer_char_slice( /// # Safety /// /// `start` must be valid to read `len` wide characters following `step`. -pub unsafe fn read_wide_string_strided(start: *const WChar, len: usize, step: isize) -> Wtf8Buf { +pub unsafe fn read_wide_string_strided(start: NonNull, len: usize, step: isize) -> Wtf8Buf { if step == 1 { return unsafe { read_wide_string_with_len(start, len) }; } let mut wchars = Vec::with_capacity(len); let mut cur = start; for _ in 0..len { - wchars.push(unsafe { *cur }); + wchars.push(unsafe { cur.read() }); cur = unsafe { cur.offset(step) }; } wide_chars_to_wtf8(&wchars) @@ -2274,10 +2411,9 @@ pub unsafe fn read_pointer_wchar_slice( start: isize, len: usize, step: isize, -) -> Wtf8Buf { - let wchar_size = core::mem::size_of::(); - let start_addr = (ptr_value as isize + start * wchar_size as isize) as *const WChar; - unsafe { read_wide_string_strided(start_addr, len, step) } +) -> Option { + let start_addr = unsafe { NonNull::new(ptr_value as *mut WChar)?.offset(start) }; + Some(unsafe { read_wide_string_strided(start_addr, len, step) }) } /// # Safety @@ -2718,3 +2854,690 @@ pub fn dlsym_checked(_handle: usize, symbol_name: &CStr) -> Result<*mut c_void, symbol_name.to_string_lossy() )) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_type_is_pointer_classifies_codes() { + assert!(simple_type_is_pointer("z")); + assert!(simple_type_is_pointer("Z")); + assert!(simple_type_is_pointer("P")); + assert!(simple_type_is_pointer("O")); + assert!(!simple_type_is_pointer("i")); + assert!(!simple_type_is_pointer("d")); + assert!(!simple_type_is_pointer("")); + } + + #[test] + fn simple_type_chars_contains_expected_codes() { + let chars = simple_type_chars(); + assert!(chars.contains('i')); + assert!(chars.contains('d')); + assert!(chars.contains('P')); + // junk / non-code characters are excluded + assert!(!chars.contains('@')); + assert!(!chars.contains(' ')); + assert!(!chars.contains('1')); + } + + #[cfg(all( + any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "android" + ), + not(any(target_env = "musl", target_env = "sgx")) + ))] + mod call_tests { + use super::*; + + extern "C" fn abs_i32(x: i32) -> i32 { + x.abs() + } + + extern "C" fn add_i32(a: i32, b: i32) -> i32 { + a + b + } + + extern "C" fn sqrt_f64(x: f64) -> f64 { + x.sqrt() + } + + extern "C" fn noop() {} + + #[repr(C)] + struct PairI32 { + a: i32, + b: i32, + } + extern "C" fn sum_pair(p: PairI32) -> i32 { + p.a + p.b + } + extern "C" fn ret_pair() -> PairI32 { + PairI32 { a: 10, b: 20 } + } + + #[repr(C)] + struct PairF32 { + x: f32, + y: f32, + } + extern "C" fn sum_pair_f32(p: PairF32) -> f32 { + p.x + p.y + } + + #[repr(C)] + struct Inner { + a: i32, + b: i32, + } + #[repr(C)] + struct Outer { + inner: Inner, + c: i32, + } + extern "C" fn sum_outer(o: Outer) -> i32 { + o.inner.a + o.inner.b + o.c + } + + #[repr(C)] + struct ArrStruct { + arr: [i32; 3], + tag: i32, + } + extern "C" fn sum_arr_struct(s: ArrStruct) -> i32 { + s.arr[0] + s.arr[1] + s.arr[2] + s.tag + } + + #[repr(C)] + struct Big { + a: i64, + b: i64, + c: i64, + } + extern "C" fn sum_big(v: Big) -> i64 { + v.a + v.b + v.c + } + extern "C" fn ret_big() -> Big { + Big { a: 1, b: 2, c: 3 } + } + + #[allow(dead_code)] + #[repr(C)] + struct S3 { + a: u8, + b: u8, + c: u8, + } + extern "C" fn ret_s3() -> S3 { + S3 { a: 1, b: 2, c: 3 } + } + + #[allow(dead_code)] + #[repr(C)] + struct S5 { + a: u8, + b: u8, + c: u8, + d: u8, + e: u8, + } + extern "C" fn ret_s5() -> S5 { + S5 { + a: 1, + b: 2, + c: 3, + d: 4, + e: 5, + } + } + + #[allow(dead_code)] + #[repr(C)] + struct S12 { + a: i32, + b: i32, + c: i32, + } + extern "C" fn ret_s12() -> S12 { + S12 { + a: 100, + b: 200, + c: 300, + } + } + + fn addr_of(f: extern "C" fn() -> ()) -> usize { + f as *const () as usize + } + + fn scalar_bytes(value: &CallValue) -> &[u8] { + match value { + CallValue::Scalar(bytes) => bytes, + other => panic!("expected Scalar, got {other:?}"), + } + } + + fn aggregate_bytes(value: &CallValue) -> &[u8] { + match value { + CallValue::Aggregate(bytes) => bytes, + other => panic!("expected Aggregate, got {other:?}"), + } + } + + fn i32_bytes(values: &[i32]) -> Vec { + values.iter().flat_map(|v| v.to_ne_bytes()).collect() + } + + // --- scalar parity ----------------------------------------------------- + + #[test] + fn calls_f64_scalar() { + let addr = sqrt_f64 as *const () as usize; + let result = call( + addr, + &[CallArg::Double(2.0)], + CallRet::Code("d"), + CallOptions::default(), + ) + .unwrap(); + match decode_type_code("d", scalar_bytes(&result)) { + DecodedValue::Float(v) => { + assert!((v - core::f64::consts::SQRT_2).abs() < 1e-12) + } + _ => panic!("expected Float return"), + } + } + + #[test] + fn typed_scalar_arg_from_buffer() { + let addr = abs_i32 as *const () as usize; + let buffer = (-5i32).to_ne_bytes(); + let result = call( + addr, + &[CallArg::Typed { + code: "i", + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(5) + )); + } + + #[test] + fn typed_two_scalar_args() { + let addr = add_i32 as *const () as usize; + let a = 2i32.to_ne_bytes(); + let b = 3i32.to_ne_bytes(); + let result = call( + addr, + &[ + CallArg::Typed { + code: "i", + buffer: &a, + }, + CallArg::Typed { + code: "i", + buffer: &b, + }, + ], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(5) + )); + } + + #[test] + fn void_return_is_void() { + let result = call(addr_of(noop), &[], CallRet::Void, CallOptions::default()).unwrap(); + assert!(matches!(result, CallValue::Void)); + } + + #[test] + fn every_simple_code_is_accepted() { + for code in simple_type_chars().chars() { + let code = code.to_string(); + assert!( + ffi_type_from_code(&code).is_some(), + "code {code:?} not accepted by call's arg/return lowering" + ); + } + } + + #[test] + fn scalar_lowering_helpers_agree_where_carrier_matches() { + // For codes whose ffi carrier type matches their ctypes signedness the + // two lowering helpers agree. + let buffer = 0x1122_3344_5566_7788u64.to_ne_bytes(); + for code in ["b", "B", "h", "H", "i", "I", "q", "Q", "d", "f"] { + let by_code = ffi_value_from_type_code(code, &buffer); + let by_type = + ffi_value_from_type(&buffer, ffi_type_from_code(code).unwrap()).unwrap(); + assert_eq!( + format!("{by_code:?}"), + format!("{by_type:?}"), + "code {code}" + ); + } + } + + #[test] + fn scalar_lowering_helpers_diverge_for_signed_char() { + // `ffi_value_from_type` classifies purely by libffi Type identity, while + // `ffi_value_from_type_code` carries ctypes signedness: for 'c' (u8 + // carrier, signed value) the two intentionally differ. `call` uses only + // the code-based helper. + let buffer = [200u8]; + assert!(matches!( + ffi_value_from_type_code("c", &buffer), + FfiValue::I8(-56) + )); + assert!(matches!( + ffi_value_from_type(&buffer, ffi_type_from_code("c").unwrap()), + Some(FfiValue::U8(200)) + )); + } + + // --- by-value aggregate arguments ------------------------------------- + + #[test] + fn passes_struct_by_value() { + let addr = sum_pair as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[3, 4]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(7) + )); + } + + #[test] + fn passes_nested_struct_by_value() { + let addr = sum_outer as *const () as usize; + let inner = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let layout = CTypeLayout::Struct { + fields: vec![inner, CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[5, 6, 7]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(18) + )); + } + + #[test] + fn passes_array_in_struct_by_value() { + let addr = sum_arr_struct as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Array { + element: Box::new(CTypeLayout::Simple('i')), + length: 3, + size: 12, + }, + CTypeLayout::Simple('i'), + ], + size: core::mem::size_of::(), + }; + let buffer = i32_bytes(&[1, 2, 3, 4]); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(10) + )); + } + + #[test] + fn passes_float_pair_struct_by_value() { + let addr = sum_pair_f32 as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('f'), CTypeLayout::Simple('f')], + size: core::mem::size_of::(), + }; + let mut buffer = Vec::new(); + buffer.extend_from_slice(&1.5f32.to_ne_bytes()); + buffer.extend_from_slice(&2.25f32.to_ne_bytes()); + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("f"), + CallOptions::default(), + ) + .unwrap(); + match decode_type_code("f", scalar_bytes(&result)) { + DecodedValue::Float(v) => assert!((v - 3.75).abs() < 1e-6), + _ => panic!("expected Float return"), + } + } + + #[test] + fn passes_large_struct_by_value() { + let addr = sum_big as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Simple('q'), + CTypeLayout::Simple('q'), + CTypeLayout::Simple('q'), + ], + size: core::mem::size_of::(), + }; + let mut buffer = Vec::new(); + for v in [11i64, 22, 33] { + buffer.extend_from_slice(&v.to_ne_bytes()); + } + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("q"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!( + decode_type_code("q", scalar_bytes(&result)), + DecodedValue::Signed(66) + )); + } + + // --- by-value aggregate returns --------------------------------------- + + #[test] + fn returns_small_struct_by_value() { + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: core::mem::size_of::(), + }; + let result = call( + ret_pair as *const () as usize, + &[], + CallRet::Aggregate(&layout), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), i32_bytes(&[10, 20]).as_slice()); + } + + #[test] + fn returns_odd_size_structs_by_value() { + let s3 = CTypeLayout::Struct { + fields: vec![ + CTypeLayout::Simple('B'), + CTypeLayout::Simple('B'), + CTypeLayout::Simple('B'), + ], + size: 3, + }; + let result = call( + ret_s3 as *const () as usize, + &[], + CallRet::Aggregate(&s3), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), &[1u8, 2, 3]); + + let s5 = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('B'); 5], + size: 5, + }; + let result = call( + ret_s5 as *const () as usize, + &[], + CallRet::Aggregate(&s5), + CallOptions::default(), + ) + .unwrap(); + assert_eq!(aggregate_bytes(&result), &[1u8, 2, 3, 4, 5]); + + let s12 = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'); 3], + size: 12, + }; + let result = call( + ret_s12 as *const () as usize, + &[], + CallRet::Aggregate(&s12), + CallOptions::default(), + ) + .unwrap(); + assert_eq!( + aggregate_bytes(&result), + i32_bytes(&[100, 200, 300]).as_slice() + ); + } + + #[test] + fn returns_large_struct_by_value() { + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('q'); 3], + size: core::mem::size_of::(), + }; + let result = call( + ret_big as *const () as usize, + &[], + CallRet::Aggregate(&layout), + CallOptions::default(), + ) + .unwrap(); + let mut expected = Vec::new(); + for v in [1i64, 2, 3] { + expected.extend_from_slice(&v.to_ne_bytes()); + } + assert_eq!(aggregate_bytes(&result), expected.as_slice()); + } + + // --- pointers, layout, unions, errors --------------------------------- + + #[test] + fn pointer_return_round_trips_address() { + extern "C" fn echo_ptr(p: usize) -> usize { + p + } + let addr = echo_ptr as *const () as usize; + let sentinel = 0xDEAD_BEEFusize; + let result = call( + addr, + &[CallArg::Pointer(sentinel)], + CallRet::Code("P"), + CallOptions::default(), + ) + .unwrap(); + assert!(matches!(result, CallValue::Pointer(p) if p == sentinel)); + let result = call( + addr, + &[CallArg::Pointer(sentinel)], + CallRet::Pointer, + CallOptions::default(), + ) + .unwrap(); + assert!(matches!(result, CallValue::Pointer(p) if p == sentinel)); + } + + #[test] + fn layout_size_matches_repr_c() { + assert_eq!(CTypeLayout::Simple('i').size(), core::mem::size_of::()); + assert_eq!(CTypeLayout::Pointer.size(), core::mem::size_of::()); + assert_eq!( + CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: 8, + } + .size(), + 8 + ); + assert_eq!(CTypeLayout::Opaque { size: 5 }.size(), 5); + assert_eq!( + CTypeLayout::Array { + element: Box::new(CTypeLayout::Simple('i')), + length: 3, + size: 12, + } + .size(), + 12 + ); + } + + #[test] + fn union_layout_reports_size_and_lowers() { + let layout = CTypeLayout::Union { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('d')], + size: 8, + }; + assert_eq!(layout.size(), 8); + assert!(layout.to_ffi_type().is_ok()); + } + + #[test] + fn null_addr_is_error() { + let result = call(0, &[], CallRet::Void, CallOptions::default()); + assert_eq!(result.err(), Some(CallError::NullFunctionPointer)); + } + + #[test] + fn unknown_arg_code_is_error() { + let addr = noop as *const () as usize; + let result = call( + addr, + &[CallArg::Typed { + code: "@", + buffer: &[], + }], + CallRet::Void, + CallOptions::default(), + ); + assert_eq!( + result.err(), + Some(CallError::UnknownTypeCode("@".to_string())) + ); + } + + #[test] + fn unknown_return_code_is_error() { + let addr = noop as *const () as usize; + let result = call(addr, &[], CallRet::Code("@"), CallOptions::default()); + assert_eq!( + result.err(), + Some(CallError::UnknownTypeCode("@".to_string())) + ); + } + + #[test] + fn short_aggregate_buffer_is_error() { + let addr = sum_pair as *const () as usize; + let layout = CTypeLayout::Struct { + fields: vec![CTypeLayout::Simple('i'), CTypeLayout::Simple('i')], + size: 8, + }; + let buffer = [0u8; 4]; + let result = call( + addr, + &[CallArg::Aggregate { + layout: &layout, + buffer: &buffer, + }], + CallRet::Code("i"), + CallOptions::default(), + ); + assert_eq!( + result.err(), + Some(CallError::BufferTooSmall { + expected: 8, + got: 4, + }) + ); + } + + // EINVAL: a valid errno value on all unix targets, so it round-trips + // through crate::os::set_errno/get_errno. + #[cfg(not(windows))] + const ERRNO_MARKER: i32 = 22; + + #[cfg(not(windows))] + extern "C" fn write_errno_marker() -> i32 { + crate::os::set_errno(ERRNO_MARKER); + 7 + } + + #[cfg(not(windows))] + #[test] + fn errno_swap_window_captures_and_restores() { + // Distinguish the real platform errno from the ctypes-local one. + crate::os::set_errno(11); + super::super::CTYPES_LOCAL_ERRNO.with(|e| e.set(99)); + let result = call( + write_errno_marker as *const () as usize, + &[], + CallRet::Code("i"), + CallOptions { + use_errno: true, + use_last_error: false, + }, + ) + .unwrap(); + assert!(matches!( + decode_type_code("i", scalar_bytes(&result)), + DecodedValue::Signed(7) + )); + // The function's errno write landed in the ctypes-local slot... + assert_eq!( + super::super::CTYPES_LOCAL_ERRNO.with(|e| e.get()), + ERRNO_MARKER + ); + // ...and the real errno was restored to its pre-call value. + assert_eq!(crate::os::get_errno(), 11); + } + } +} diff --git a/crates/host_env/src/faulthandler.rs b/crates/host_env/src/faulthandler.rs index 3afbdebb42b..5b19359167b 100644 --- a/crates/host_env/src/faulthandler.rs +++ b/crates/host_env/src/faulthandler.rs @@ -12,6 +12,10 @@ use alloc::vec::Vec; #[cfg(unix)] use parking_lot::Mutex; + +#[cfg(unix)] +pub use libc::{SA_NODEFER, c_int}; +pub use libc::{SIGFPE, SIGSEGV}; #[cfg(windows)] use windows_sys::Win32::System::{ Diagnostics::Debug::{ diff --git a/crates/host_env/src/fcntl.rs b/crates/host_env/src/fcntl.rs index 2467a8727bc..6fb974ba514 100644 --- a/crates/host_env/src/fcntl.rs +++ b/crates/host_env/src/fcntl.rs @@ -5,6 +5,32 @@ use std::os::fd::BorrowedFd; use crate::os::CheckLibcResult; +pub use libc::{F_GETFD, F_GETFL, F_SETFD, F_SETFL, FD_CLOEXEC}; + +#[cfg(not(target_os = "wasi"))] +pub use libc::{F_DUPFD, F_DUPFD_CLOEXEC, F_GETLK, F_SETLK, F_SETLKW}; + +#[cfg(not(any(target_os = "wasi", target_os = "redox")))] +pub use libc::{F_GETOWN, F_RDLCK, F_SETOWN, F_UNLCK, F_WRLCK, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN}; + +#[cfg(target_vendor = "apple")] +pub use libc::{F_FULLFSYNC, F_NOCACHE}; + +#[cfg(target_os = "freebsd")] +pub use libc::{F_DUP2FD, F_DUP2FD_CLOEXEC}; + +#[cfg(any(target_os = "android", target_os = "linux"))] +pub use libc::{F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW}; + +#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] +pub use libc::{ + F_ADD_SEALS, F_GET_SEALS, F_GETLEASE, F_GETPIPE_SZ, F_NOTIFY, F_SEAL_GROW, F_SEAL_SEAL, + F_SEAL_SHRINK, F_SEAL_WRITE, F_SETLEASE, F_SETPIPE_SZ, +}; + +#[cfg(any(target_os = "dragonfly", target_os = "netbsd", target_vendor = "apple"))] +pub use libc::F_GETPATH; + pub fn normalize_ioctl_request(request: i64) -> libc::c_ulong { (request as u32) as libc::c_ulong } diff --git a/crates/host_env/src/fileutils.rs b/crates/host_env/src/fileutils.rs index 79cee1cb551..a8e56bb1c0b 100644 --- a/crates/host_env/src/fileutils.rs +++ b/crates/host_env/src/fileutils.rs @@ -1,32 +1,25 @@ // Python/fileutils.c in CPython #![allow(non_snake_case)] +use alloc::ffi::CString; + #[cfg(not(windows))] -pub use libc::stat as StatStruct; +pub use rustix::fs::Stat as StatStruct; #[cfg(windows)] pub use windows::{StatStruct, fstat}; #[cfg(not(windows))] pub fn fstat(fd: crate::crt_fd::Borrowed<'_>) -> std::io::Result { - let mut stat = core::mem::MaybeUninit::uninit(); - unsafe { - let ret = libc::fstat(fd.as_raw(), stat.as_mut_ptr()); - if ret == -1 { - Err(crate::os::errno_io_error()) - } else { - Ok(stat.assume_init()) - } - } + rustix::fs::fstat(fd).map_err(Into::into) } #[cfg(windows)] pub mod windows { use crate::crt_fd; use crate::windows::ToWideString; - use alloc::ffi::CString; use libc::{S_IFCHR, S_IFDIR, S_IFMT}; - use std::ffi::{OsStr, OsString}; + use std::ffi::OsStr; use std::os::windows::io::AsRawHandle; use std::sync::OnceLock; use windows_sys::Win32::Foundation::{ @@ -41,6 +34,7 @@ pub mod windows { use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; use windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_SYMLINK; use windows_sys::core::PCWSTR; + use windows_sys::w; pub const S_IFIFO: libc::c_int = 0o010000; pub const S_IFLNK: libc::c_int = 0o120000; @@ -94,11 +88,15 @@ pub mod windows { // _Py_fstat_noraise in cpython pub fn fstat(fd: crt_fd::Borrowed<'_>) -> std::io::Result { - let h = crt_fd::as_handle(fd); - if h.is_err() { - unsafe { SetLastError(ERROR_INVALID_HANDLE) }; - } - let h = h?; + let h = match crt_fd::as_handle(fd) { + Ok(h) => h, + Err(_) => { + // An invalid fd is reported as a Win32 handle error so the + // OSError carries winerror = ERROR_INVALID_HANDLE. + unsafe { SetLastError(ERROR_INVALID_HANDLE) }; + return Err(std::io::Error::last_os_error()); + } + }; let h = h.as_raw_handle(); // reset stat? @@ -165,8 +163,8 @@ pub mod windows { (time_out, nsec_out as _) } - fn file_time_to_time_t_nsec(in_ptr: &FILETIME) -> (libc::time_t, libc::c_int) { - let in_val: i64 = unsafe { core::mem::transmute_copy(in_ptr) }; + fn file_time_to_time_t_nsec(in_ptr: FILETIME) -> (libc::time_t, libc::c_int) { + let in_val: i64 = unsafe { core::mem::transmute_copy(&in_ptr) }; let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec. let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS; (time_out, nsec_out as _) @@ -196,10 +194,10 @@ pub mod windows { ) } else { ( - file_time_to_time_t_nsec(&info.ftCreationTime), + file_time_to_time_t_nsec(info.ftCreationTime), (0, 0), - file_time_to_time_t_nsec(&info.ftLastWriteTime), - file_time_to_time_t_nsec(&info.ftLastAccessTime), + file_time_to_time_t_nsec(info.ftLastWriteTime), + file_time_to_time_t_nsec(info.ftLastAccessTime), ) }; let st_nlink = info.nNumberOfLinks as i32; @@ -306,16 +304,13 @@ pub mod windows { let GetFileInformationByName = GET_FILE_INFORMATION_BY_NAME .get_or_init(|| { - let library_name = - OsString::from("api-ms-win-core-file-l2-1-4.dll").to_wide_with_nul(); - let module = unsafe { LoadLibraryW(library_name.as_ptr()) }; + let library_name = w!("api-ms-win-core-file-l2-1-4.dll"); + let module = unsafe { LoadLibraryW(library_name) }; if module.is_null() { return None; } - let name = CString::new("GetFileInformationByName").unwrap(); - if let Some(proc) = - unsafe { GetProcAddress(module, name.as_bytes_with_nul().as_ptr()) } - { + let name = c"GetFileInformationByName"; + if let Some(proc) = unsafe { GetProcAddress(module, name.as_ptr().cast()) } { Some(unsafe { core::mem::transmute::< unsafe extern "system" fn() -> isize, @@ -442,11 +437,26 @@ pub mod windows { } } +/// C `FILE *` handle as returned by [`fopen`] and consumed by [`fclose`]. +pub type CFile = libc::FILE; + +/// Close a file opened with [`fopen`]. +/// +/// # Safety +/// `fp` must be a non-null pointer returned by [`fopen`] and must not have been +/// closed already. +pub unsafe fn fclose(fp: *mut CFile) -> core::ffi::c_int { + unsafe { libc::fclose(fp) } +} + // _Py_fopen_obj in cpython (Python/fileutils.c:1757-1835) // Open a file using std::fs::File and convert to FILE* // Automatically handles path encoding and EINTR retries -pub fn fopen(path: &std::path::Path, mode: &str) -> std::io::Result<*mut libc::FILE> { - use alloc::ffi::CString; +#[expect( + clippy::std_instead_of_core, + reason = "false positive: core::io::ErrorKind is unstable (core_io)" +)] +pub fn fopen(path: &std::path::Path, mode: &str) -> std::io::Result<*mut CFile> { use std::fs::File; // Currently only supports read mode diff --git a/crates/host_env/src/fs.rs b/crates/host_env/src/fs.rs index 911bd4575b9..01040e73ed5 100644 --- a/crates/host_env/src/fs.rs +++ b/crates/host_env/src/fs.rs @@ -1,7 +1,7 @@ use std::{ fs::{self, File, Metadata, ReadDir}, io, - path::Path, + path::{Path, PathBuf}, }; pub fn open(path: impl AsRef) -> io::Result { @@ -44,10 +44,16 @@ pub fn open_write(path: impl AsRef) -> io::Result { fs::OpenOptions::new().write(true).open(path) } -pub fn canonicalize(path: impl AsRef) -> io::Result { +pub fn canonicalize(path: impl AsRef) -> io::Result { fs::canonicalize(path) } +/// Resolve `binary_name` to an absolute path by searching `PATH` (and `PATHEXT` on Windows). +#[cfg(not(target_arch = "wasm32"))] +pub fn which>(binary_name: T) -> Option { + ::which::which(binary_name).ok() +} + #[cfg(windows)] pub fn open_write_with_custom_flags(path: impl AsRef, flags: u32) -> io::Result { use std::os::windows::fs::OpenOptionsExt; diff --git a/crates/host_env/src/grp.rs b/crates/host_env/src/grp.rs index 131369ce949..54afe8aa54d 100644 --- a/crates/host_env/src/grp.rs +++ b/crates/host_env/src/grp.rs @@ -1,5 +1,7 @@ use std::io; +pub use libc::gid_t; + pub struct Group { pub name: String, pub passwd: String, diff --git a/crates/host_env/src/io.rs b/crates/host_env/src/io.rs index 4ae1e4b3641..f32ef2f6944 100644 --- a/crates/host_env/src/io.rs +++ b/crates/host_env/src/io.rs @@ -2,6 +2,9 @@ use core::ffi::CStr; use std::io; +#[cfg(any(unix, target_os = "wasi"))] +use rustix::{fs::FileType, io::Errno}; + #[cfg(any(unix, target_os = "wasi"))] use crate::fileutils; use crate::{crt_fd, os}; @@ -148,8 +151,8 @@ pub struct FileTargetInfo { #[cfg(any(unix, target_os = "wasi"))] pub fn inspect_file_target(fd: crt_fd::Borrowed<'_>) -> io::Result { let status = fileutils::fstat(fd)?; - if (status.st_mode & libc::S_IFMT) == libc::S_IFDIR { - return Err(io::Error::from_raw_os_error(libc::EISDIR)); + if FileType::from_raw_mode(status.st_mode).is_dir() { + return Err(io::Error::from(Errno::ISDIR)); } #[allow(clippy::useless_conversion, reason = "needed for 32-bit platforms")] let blksize = (status.st_blksize > 1).then(|| i64::from(status.st_blksize)); @@ -196,6 +199,29 @@ pub fn is_seekable(fd: crt_fd::Borrowed<'_>) -> bool { os::seek_fd(fd, 0, libc::SEEK_CUR).is_ok() } +/// Whether a read from `fd` answers from data the file already holds, rather +/// than waiting for whoever writes the other end. +/// +/// Seeking answers this everywhere but Windows, where a pipe seeks too -- +/// `lseek` on one succeeds and reports a position, so a reader that took +/// seekability for an answer would wait on a peer while holding whatever it +/// holds for the length of the call. +#[cfg(not(windows))] +pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool { + is_seekable(fd) +} + +#[cfg(windows)] +pub fn reads_without_waiting(fd: crt_fd::Borrowed<'_>) -> bool { + use std::os::windows::io::AsRawHandle; + use windows_sys::Win32::Storage::FileSystem::{FILE_TYPE_DISK, GetFileType}; + + let Ok(handle) = crt_fd::as_handle(fd) else { + return false; + }; + unsafe { GetFileType(handle.as_raw_handle() as _) == FILE_TYPE_DISK } +} + pub fn validate_whence(whence: i32) -> bool { let standard = (0..=2).contains(&whence); #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "linux"))] diff --git a/crates/host_env/src/io_unsupported.rs b/crates/host_env/src/io_unsupported.rs index e46f05af900..d9fdc5d0d23 100644 --- a/crates/host_env/src/io_unsupported.rs +++ b/crates/host_env/src/io_unsupported.rs @@ -176,6 +176,10 @@ pub fn is_seekable(_fd: crt_fd::Borrowed<'_>) -> bool { false } +pub fn reads_without_waiting(_fd: crt_fd::Borrowed<'_>) -> bool { + false +} + pub fn validate_whence(whence: i32) -> bool { (0..=2).contains(&whence) } diff --git a/crates/host_env/src/lib.rs b/crates/host_env/src/lib.rs index 99f67b2b496..975ca21b626 100644 --- a/crates/host_env/src/lib.rs +++ b/crates/host_env/src/lib.rs @@ -30,6 +30,7 @@ pub mod fileutils; pub mod fs; #[cfg(any(unix, windows))] pub mod locale; +pub mod readline; #[cfg(windows)] pub mod windows; @@ -52,6 +53,11 @@ pub mod posix; #[cfg(target_os = "wasi")] #[path = "posix_wasi.rs"] pub mod posix; +#[cfg(windows)] +#[path = "posix_windows.rs"] +pub mod posix; +#[cfg(any(unix, target_os = "wasi"))] +pub mod posix_unix_like; #[cfg(unix)] pub mod pwd; #[cfg(unix)] @@ -64,6 +70,10 @@ pub mod time; #[cfg(windows)] pub mod cert_store; +#[cfg(target_os = "macos")] +pub mod system_configuration { + pub use ::system_configuration::*; +} #[cfg(any(unix, windows))] pub mod faulthandler; #[cfg(any(unix, windows))] diff --git a/crates/host_env/src/locale.rs b/crates/host_env/src/locale.rs index 52fa7904421..6363515081c 100644 --- a/crates/host_env/src/locale.rs +++ b/crates/host_env/src/locale.rs @@ -1,6 +1,23 @@ use alloc::vec::Vec; use core::{ffi::CStr, ptr}; +pub use libc::{LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME}; + +#[cfg(all(unix, not(any(target_os = "ios", target_os = "redox"))))] +pub use libc::LC_MESSAGES; + +#[cfg(all( + unix, + not(any(target_os = "ios", target_os = "android", target_os = "redox")) +))] +pub use libc::{ + ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7, ABMON_1, ABMON_2, ABMON_3, + ABMON_4, ABMON_5, ABMON_6, ABMON_7, ABMON_8, ABMON_9, ABMON_10, ABMON_11, ABMON_12, ALT_DIGITS, + AM_STR, CODESET, CRNCYSTR, D_FMT, D_T_FMT, DAY_1, DAY_2, DAY_3, DAY_4, DAY_5, DAY_6, DAY_7, + ERA, ERA_D_FMT, ERA_D_T_FMT, ERA_T_FMT, MON_1, MON_2, MON_3, MON_4, MON_5, MON_6, MON_7, MON_8, + MON_9, MON_10, MON_11, MON_12, NOEXPR, PM_STR, RADIXCHAR, T_FMT, T_FMT_AMPM, THOUSEP, YESEXPR, +}; + #[cfg(windows)] #[repr(C)] struct RawLconv { diff --git a/crates/host_env/src/mmap.rs b/crates/host_env/src/mmap.rs index 62dc50f1c31..c1071a716e4 100644 --- a/crates/host_env/src/mmap.rs +++ b/crates/host_env/src/mmap.rs @@ -5,6 +5,62 @@ use std::io; +#[cfg(unix)] +pub use libc::{ + MADV_DONTNEED, MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL, MADV_WILLNEED, MAP_ANON, + MAP_ANONYMOUS, MAP_PRIVATE, MAP_SHARED, PROT_EXEC, PROT_READ, PROT_WRITE, +}; + +#[cfg(target_os = "macos")] +pub use libc::{MADV_FREE_REUSABLE, MADV_FREE_REUSE}; + +#[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "fuchsia", + target_os = "freebsd", + target_os = "linux", + target_os = "netbsd", + target_os = "openbsd", + target_vendor = "apple" +))] +pub use libc::MADV_FREE; + +#[cfg(target_os = "linux")] +pub use libc::{ + MADV_DODUMP, MADV_DOFORK, MADV_DONTDUMP, MADV_DONTFORK, MADV_HUGEPAGE, MADV_HWPOISON, + MADV_MERGEABLE, MADV_NOHUGEPAGE, MADV_REMOVE, MADV_UNMERGEABLE, +}; + +#[cfg(any( + target_os = "android", + all( + target_os = "linux", + any( + target_arch = "aarch64", + target_arch = "arm", + target_arch = "powerpc", + target_arch = "powerpc64", + target_arch = "s390x", + target_arch = "x86", + target_arch = "x86_64", + target_arch = "sparc64" + ) + ) +))] +pub use libc::MADV_SOFT_OFFLINE; + +#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +pub use libc::{MAP_DENYWRITE, MAP_EXECUTABLE, MAP_POPULATE}; + +#[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "netbsd"))] +pub use libc::MAP_STACK; + +#[cfg(target_os = "freebsd")] +pub use libc::{MADV_AUTOSYNC, MADV_CORE, MADV_NOCORE, MADV_NOSYNC, MADV_PROTECT}; + +pub use libc::EOVERFLOW; + #[cfg(windows)] use crate::windows::{CheckWin32Bool, HandleToOwned}; #[cfg(unix)] diff --git a/crates/host_env/src/multiprocessing.rs b/crates/host_env/src/multiprocessing.rs index 4e79b2573cb..067a8630777 100644 --- a/crates/host_env/src/multiprocessing.rs +++ b/crates/host_env/src/multiprocessing.rs @@ -13,7 +13,7 @@ use alloc::ffi::CString; use std::io; #[cfg(unix)] -use libc::sem_t; +pub use libc::{sem_t, timespec}; #[cfg(unix)] use nix::errno::Errno; @@ -32,12 +32,13 @@ pub enum SemError { AlreadyExists, NotFound, InvalidInput, + InteriorNul, Other(i32), } #[cfg(unix)] impl SemError { - fn from_errno(err: Errno) -> Self { + const fn from_errno(err: Errno) -> Self { match err { Errno::EAGAIN => Self::WouldBlock, Errno::ETIMEDOUT => Self::TimedOut, @@ -49,14 +50,14 @@ impl SemError { } } - pub fn raw_os_error(self) -> i32 { + pub const fn raw_os_error(self) -> i32 { match self { Self::WouldBlock => Errno::EAGAIN as i32, Self::TimedOut => Errno::ETIMEDOUT as i32, Self::Interrupted => Errno::EINTR as i32, Self::AlreadyExists => Errno::EEXIST as i32, Self::NotFound => Errno::ENOENT as i32, - Self::InvalidInput => Errno::EINVAL as i32, + Self::InvalidInput | Self::InteriorNul => Errno::EINVAL as i32, Self::Other(code) => code, } } @@ -119,7 +120,7 @@ impl SemHandle { value: u32, unlink: bool, ) -> Result<(Self, Option), SemError> { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let raw = unsafe { libc::sem_open(cname.as_ptr(), libc::O_CREAT | libc::O_EXCL, 0o600, value) }; if raw == libc::SEM_FAILED { @@ -141,7 +142,7 @@ impl SemHandle { } pub fn open_existing(name: &str) -> Result { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let raw = unsafe { libc::sem_open(cname.as_ptr(), 0) }; if raw == libc::SEM_FAILED { Err(SemError::from_errno(Errno::last())) @@ -305,18 +306,18 @@ pub fn is_too_many_posts(err: u32) -> bool { } #[cfg(unix)] -pub fn semaphore_name(name: &str) -> Result { - let mut full = String::with_capacity(name.len() + 1); +pub fn semaphore_name(name: &str) -> Result { + let mut full = String::with_capacity(name.len() + 2); if !name.starts_with('/') { full.push('/'); } full.push_str(name); - CString::new(full) + CString::new(full).map_err(|_| SemError::InteriorNul) } #[cfg(unix)] pub fn sem_unlink(name: &str) -> Result<(), SemError> { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let res = unsafe { libc::sem_unlink(cname.as_ptr()) }; if res < 0 { Err(SemError::from_errno(Errno::last())) diff --git a/crates/host_env/src/nt.rs b/crates/host_env/src/nt.rs index 4c77b30e616..7e0591600b1 100644 --- a/crates/host_env/src/nt.rs +++ b/crates/host_env/src/nt.rs @@ -22,19 +22,22 @@ use crate::{ windows::{CheckWin32Bool, CheckWin32Handle, CheckWin32Sentinel, HandleToOwned, ToWideString}, }; use libc::intptr_t; -use windows_sys::Win32::{ - Foundation::{ - CloseHandle, ERROR_INVALID_HANDLE, GetLastError, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH, - }, - Globalization::{CP_UTF8, MultiByteToWideChar, WideCharToMultiByte}, - Storage::FileSystem::{ - CreateFileW, FILE_BASIC_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, - FILE_READ_ATTRIBUTES, FILE_TYPE_UNKNOWN, FileBasicInfo, FindClose, FindFirstFileW, - GetFileAttributesW, GetFileInformationByHandleEx, GetFileType, GetFullPathNameW, - INVALID_FILE_ATTRIBUTES, OPEN_EXISTING, SetFileAttributesW, SetFileInformationByHandle, - WIN32_FIND_DATAW, +use windows_sys::{ + Win32::{ + Foundation::{ + CloseHandle, ERROR_INVALID_HANDLE, GetLastError, HANDLE, INVALID_HANDLE_VALUE, MAX_PATH, + }, + Globalization::{CP_UTF8, MultiByteToWideChar, WideCharToMultiByte}, + Storage::FileSystem::{ + CreateFileW, FILE_BASIC_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, + FILE_READ_ATTRIBUTES, FILE_TYPE_UNKNOWN, FileBasicInfo, FindClose, FindFirstFileW, + GetFileAttributesW, GetFileInformationByHandleEx, GetFileType, GetFullPathNameW, + INVALID_FILE_ATTRIBUTES, OPEN_EXISTING, SetFileAttributesW, SetFileInformationByHandle, + WIN32_FIND_DATAW, + }, + System::{Console, Threading}, }, - System::{Console, Threading}, + w, }; pub type Handle = HANDLE; @@ -1172,12 +1175,10 @@ pub fn mkdir(path: &widestring::WideCStr, mode: i32) -> io::Result<()> { lpSecurityDescriptor: core::ptr::null_mut(), bInheritHandle: 0, }; - let sddl: Vec = "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)\0" - .encode_utf16() - .collect(); + let sddl = w!("D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"); unsafe { ConvertStringSecurityDescriptorToSecurityDescriptorW( - sddl.as_ptr(), + sddl, SDDL_REVISION_1, &mut sec_attr.lpSecurityDescriptor, core::ptr::null_mut(), @@ -1333,7 +1334,9 @@ pub fn readlink(path: &Path) -> Result { let path_slice = &buffer[path_start..path_end]; let mut wide_chars: Vec = path_slice - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) .collect(); @@ -1697,10 +1700,10 @@ pub fn get_terminal_size_handle(h: HANDLE) -> io::Result<(usize, usize)> { if err != windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED { return Err(io::Error::last_os_error()); } - let conout: Vec = "CONOUT$\0".encode_utf16().collect(); + let conout = w!("CONOUT$"); let console_handle = unsafe { CreateFileW( - conout.as_ptr(), + conout, windows_sys::Win32::Foundation::GENERIC_READ | windows_sys::Win32::Foundation::GENERIC_WRITE, windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ diff --git a/crates/host_env/src/os.rs b/crates/host_env/src/os.rs index bd2a5acb906..7af8f586110 100644 --- a/crates/host_env/src/os.rs +++ b/crates/host_env/src/os.rs @@ -4,13 +4,15 @@ use crate::crt_fd; #[cfg(windows)] use crate::fs; +#[cfg(windows)] +pub use crate::posix::rename; +#[cfg(any(unix, target_os = "wasi"))] +pub use crate::posix_unix_like::rename; #[cfg(any(unix, windows))] use core::ffi::CStr; use core::str::Utf8Error; #[cfg(windows)] use core::time::Duration; -#[cfg(unix)] -use rustix::fd::AsFd; use std::{ env, ffi::{OsStr, OsString}, @@ -30,6 +32,23 @@ use { }, }; +#[cfg(not(any(unix, windows, target_os = "wasi")))] +pub fn rename( + from: impl AsRef, + from_fd: Option>, + to: impl AsRef, + to_fd: Option>, +) -> io::Result<()> { + if from_fd.is_none() && to_fd.is_none() { + std::fs::rename(from, to) + } else { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "renameat is not available on this platform", + )) + } +} + /// Convert exit code to std::process::ExitCode /// /// On Windows, this supports the full u32 range including STATUS_CONTROL_C_EXIT (0xC000013A). @@ -267,41 +286,6 @@ pub fn copy_file_range( rustix::fs::copy_file_range(src, offset_src, dst, offset_dst, count) } -#[cfg(not(unix))] -pub fn rename( - from: impl AsRef, - from_fd: Option>, - to: impl AsRef, - to_fd: Option>, -) -> io::Result<()> { - if from_fd.is_none() && to_fd.is_none() { - // TODO: Rust's implementation always overwrites the file so ensure consistency between - // operating systems. We need to use windows-sys directly to distinguish between - // os.rename and os.replace. - std::fs::rename(from, to) - } else { - core::hint::cold_path(); - Err(io::Error::new( - io::ErrorKind::Unsupported, - "renameat is not available on this platform", - )) - } -} - -#[cfg(unix)] -pub fn rename( - from: impl AsRef, - from_fd: Option>, - to: impl AsRef, - to_fd: Option>, -) -> io::Result<()> { - let from = from.as_ref(); - let from_fd = from_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd); - let to = to.as_ref(); - let to_fd = to_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd); - rustix::fs::renameat(from_fd, from, to_fd, to).map_err(Into::into) -} - #[cfg(windows)] pub fn seek_fd( fd: crt_fd::Borrowed<'_>, @@ -377,11 +361,53 @@ impl ErrorExt for io::Error { } #[cfg(windows)] fn posix_errno(&self) -> i32 { + // A C runtime error carries its exact errno as the payload; report it + // directly instead of round-tripping through a Win32 error code. + if let Some(crt) = self.get_ref().and_then(|e| e.downcast_ref::()) { + return crt.0; + } let winerror = self.raw_os_error().unwrap_or(0); winerror_to_errno(winerror) } } +/// Wraps a raw C runtime `errno` inside an [`io::Error`]. +/// +/// CRT functions (`open`, `read`, `dup`, ...) report failures through `errno`, +/// not `GetLastError`. Translating that `errno` into a Win32 error code is +/// lossy — any value missing from [`errno_to_winerror`] collapses to `EINVAL` — +/// and also attaches a spurious `winerror` to the resulting `OSError`. Carrying +/// the `errno` as the error payload lets [`ErrorExt::posix_errno`] recover it +/// exactly while leaving `raw_os_error()` empty, so no `winerror` is reported. +#[cfg(windows)] +#[derive(Debug)] +struct CrtErrno(i32); + +#[cfg(windows)] +impl core::fmt::Display for CrtErrno { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match crate::errno::strerror_string(self.0) { + Some(msg) => f.write_str(&msg), + None => write!(f, "os error {}", self.0), + } + } +} + +#[cfg(windows)] +impl core::error::Error for CrtErrno {} + +/// Build an [`io::Error`] that preserves a raw C runtime `errno`. +/// +/// The [`io::ErrorKind`] is derived from the closest Win32 mapping so callers +/// matching on `kind()` keep working, while the exact `errno` is preserved for +/// [`ErrorExt::posix_errno`]. +#[cfg(windows)] +#[must_use] +pub fn io_error_from_errno(errno: i32) -> io::Error { + let kind = io::Error::from_raw_os_error(errno_to_winerror(errno)).kind(); + io::Error::new(kind, CrtErrno(errno)) +} + #[cfg(all(not(windows), not(target_arch = "wasm32")))] impl ErrorExt for rustix::io::Errno { fn posix_errno(&self) -> i32 { @@ -394,9 +420,7 @@ impl ErrorExt for rustix::io::Errno { #[cfg(windows)] #[must_use] pub fn errno_io_error() -> io::Error { - let errno: i32 = get_errno(); - let winerror = errno_to_winerror(errno); - io::Error::from_raw_os_error(winerror) + io_error_from_errno(get_errno()) } #[cfg(not(windows))] @@ -495,13 +519,14 @@ pub fn set_errno(value: i32) { #[cfg(not(any(unix, windows, target_os = "wasi")))] pub fn set_errno(_value: i32) {} -#[cfg(unix)] +// WASIp1, like Unix, provides byte-preserving OsStr conversions. +#[cfg(any(unix, all(target_os = "wasi", not(target_env = "p2"))))] pub fn bytes_as_os_str(b: &[u8]) -> Result<&std::ffi::OsStr, Utf8Error> { - use std::os::unix::ffi::OsStrExt; + use self::ffi::OsStrExt; Ok(std::ffi::OsStr::from_bytes(b)) } -#[cfg(not(unix))] +#[cfg(not(any(unix, all(target_os = "wasi", not(target_env = "p2")))))] pub fn bytes_as_os_str(b: &[u8]) -> Result<&std::ffi::OsStr, Utf8Error> { Ok(core::str::from_utf8(b)?.as_ref()) } diff --git a/crates/host_env/src/posix.rs b/crates/host_env/src/posix.rs index f10e99ac938..1e8d4cabe1e 100644 --- a/crates/host_env/src/posix.rs +++ b/crates/host_env/src/posix.rs @@ -1,5 +1,4 @@ use alloc::ffi::CString; -#[cfg(all(unix, not(target_os = "redox")))] use alloc::vec::Vec; use core::ffi::CStr; #[cfg(all(unix, not(target_os = "redox")))] @@ -10,6 +9,10 @@ use std::os::fd::FromRawFd; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd}; use std::path::Path; +pub use super::posix_unix_like::*; + +pub use libc::{c_char, pid_t}; + pub struct UnameInfo { pub sysname: String, pub nodename: String, @@ -18,6 +21,12 @@ pub struct UnameInfo { pub machine: String, } +#[derive(Debug)] +pub struct UnameDecodeError { + pub bytes: Vec, + pub error: core::str::Utf8Error, +} + #[cfg(all(unix, not(target_os = "redox")))] #[derive(Clone, Copy, Debug)] pub struct StatVfsInfo { @@ -174,26 +183,6 @@ pub fn fcopyfile(in_fd: i32, out_fd: i32, flags: u32) -> std::io::Result<()> { } } -#[cfg(not(windows))] -pub fn make_dir(path: &CStr, mode: u32) -> std::io::Result<()> { - let ret = unsafe { libc::mkdir(path.as_ptr(), mode as _) }; - if ret < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(()) - } -} - -#[cfg(all(not(windows), not(target_os = "redox")))] -pub fn make_dir_at(dir_fd: i32, path: &CStr, mode: u32) -> std::io::Result<()> { - let ret = unsafe { libc::mkdirat(dir_fd, path.as_ptr(), mode as _) }; - if ret < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(()) - } -} - #[cfg(unix)] pub fn link_paths(src: &CStr, dst: &CStr, follow_symlinks: bool) -> std::io::Result<()> { let flags = if follow_symlinks { @@ -320,46 +309,6 @@ pub fn fchown(fd: BorrowedFd<'_>, uid: Option, gid: Option) -> std::io .map_err(std::io::Error::from) } -#[cfg(not(windows))] -pub fn stat_path( - path: &OsStr, - dir_fd: Option, - follow_symlinks: bool, -) -> std::io::Result> { - use crate::os::ffi::OsStrExt; - - let path = match CString::new(path.as_bytes()) { - Ok(path) => path, - Err(_) => return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)), - }; - - let mut stat = core::mem::MaybeUninit::uninit(); - #[cfg(not(target_os = "redox"))] - if let Some(dir_fd) = dir_fd { - let flags = if follow_symlinks { - 0 - } else { - libc::AT_SYMLINK_NOFOLLOW - }; - let ret = unsafe { libc::fstatat(dir_fd, path.as_ptr(), stat.as_mut_ptr(), flags) }; - if ret < 0 { - return Err(std::io::Error::last_os_error()); - } - return Ok(Some(unsafe { stat.assume_init() })); - } - - let ret = if follow_symlinks { - unsafe { libc::stat(path.as_ptr(), stat.as_mut_ptr()) } - } else { - unsafe { libc::lstat(path.as_ptr(), stat.as_mut_ptr()) } - }; - if ret < 0 { - Err(std::io::Error::last_os_error()) - } else { - Ok(Some(unsafe { stat.assume_init() })) - } -} - #[cfg(not(windows))] pub fn stat_fd(fd: crate::crt_fd::Borrowed<'_>) -> std::io::Result { crate::fileutils::fstat(fd) @@ -410,14 +359,23 @@ pub fn fchownat( .map_err(std::io::Error::from) } -pub fn uname_info() -> Result { +pub fn uname_info() -> Result { + fn decode(value: &CStr) -> Result { + core::str::from_utf8(value.to_bytes()) + .map(str::to_owned) + .map_err(|error| UnameDecodeError { + bytes: value.to_bytes().to_vec(), + error, + }) + } + let info = rustix::system::uname(); Ok(UnameInfo { - sysname: info.sysname().to_str()?.into(), - nodename: info.nodename().to_str()?.into(), - release: info.release().to_str()?.into(), - version: info.version().to_str()?.into(), - machine: info.machine().to_str()?.into(), + sysname: decode(info.sysname())?, + nodename: decode(info.nodename())?, + release: decode(info.release())?, + version: decode(info.version())?, + machine: decode(info.machine())?, }) } @@ -1436,8 +1394,9 @@ fn build_posix_spawn_attrs( target_os = "hurd", ))] { + #[allow(clippy::useless_conversion)] flags.insert(nix::spawn::PosixSpawnFlags::from_bits_retain( - libc::POSIX_SPAWN_SETSID, + libc::POSIX_SPAWN_SETSID.into(), )); } #[cfg(not(any( @@ -1447,6 +1406,10 @@ fn build_posix_spawn_attrs( target_os = "illumos", target_os = "hurd", )))] + #[expect( + clippy::std_instead_of_core, + reason = "false positive: core::io::ErrorKind is unstable (core_io); expect is co-gated with the usage so it is not left unfulfilled on platforms where this block is compiled out" + )] { return Err(std::io::Error::new( std::io::ErrorKind::Unsupported, diff --git a/crates/host_env/src/posix_unix_like.rs b/crates/host_env/src/posix_unix_like.rs new file mode 100644 index 00000000000..183feb5316b --- /dev/null +++ b/crates/host_env/src/posix_unix_like.rs @@ -0,0 +1,66 @@ +//! Common POSIX implementations across Unix-likes. + +use std::{io, path::Path}; + +use rustix::{ + fd::AsFd, + fs::{self, AtFlags}, +}; + +pub use rustix::fs::RawMode; + +use crate::{crt_fd, fileutils::StatStruct}; + +/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/mkdir.html +pub fn make_dir( + dir_fd: Option>, + path: impl AsRef, + mode: fs::RawMode, +) -> io::Result<()> { + let dir_fd = dir_fd.as_ref().map_or(fs::CWD, AsFd::as_fd); + fs::mkdirat(dir_fd, path.as_ref(), mode.into()).map_err(Into::into) +} + +/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html +pub fn rename( + from: impl AsRef, + from_fd: Option>, + to: impl AsRef, + to_fd: Option>, +) -> io::Result<()> { + let from = from.as_ref(); + let from_fd = from_fd.as_ref().map_or(fs::CWD, AsFd::as_fd); + let to = to.as_ref(); + let to_fd = to_fd.as_ref().map_or(fs::CWD, AsFd::as_fd); + fs::renameat(from_fd, from, to_fd, to).map_err(Into::into) +} + +/// https://docs.python.org/3/library/os.html#os.replace +/// +/// Atomically replace `to` with `from`. +/// POSIX's rename already atomically replaces targets, so this function just forwards to [`rename`]. +#[inline] +pub fn replace( + from: impl AsRef, + from_fd: Option>, + to: impl AsRef, + to_fd: Option>, +) -> io::Result<()> { + rename(from, from_fd, to, to_fd) +} + +pub fn stat_path( + path: impl AsRef, + dir_fd: Option>, + follow_symlinks: bool, +) -> io::Result> { + let flags = if follow_symlinks { + AtFlags::empty() + } else { + AtFlags::SYMLINK_NOFOLLOW + }; + let dir_fd = dir_fd.as_ref().map_or(fs::CWD, AsFd::as_fd); + fs::statat(dir_fd, path.as_ref(), flags) + .map(Option::Some) + .map_err(Into::into) +} diff --git a/crates/host_env/src/posix_wasi.rs b/crates/host_env/src/posix_wasi.rs index d4eff8c6866..791991e981d 100644 --- a/crates/host_env/src/posix_wasi.rs +++ b/crates/host_env/src/posix_wasi.rs @@ -1,57 +1,17 @@ use alloc::ffi::CString; use core::{ffi::CStr, time::Duration}; -use std::{ffi::OsStr, io}; +use rustix::fd::AsFd; +use std::{ffi::OsStr, io, path::Path}; -use crate::os::CheckLibcResult; +pub use super::posix_unix_like::*; -pub fn make_dir(path: &CStr, mode: u32) -> io::Result<()> { - unsafe { libc::mkdir(path.as_ptr(), mode as _) }.check_libc_neg()?; - Ok(()) -} - -pub fn make_dir_at(dir_fd: i32, path: &CStr, mode: u32) -> io::Result<()> { - unsafe { libc::mkdirat(dir_fd, path.as_ptr(), mode as _) }.check_libc_neg()?; - Ok(()) -} +use crate::{crt_fd, os::CheckLibcResult}; pub fn remove_dir_at(dir_fd: i32, path: &CStr) -> io::Result<()> { unsafe { libc::unlinkat(dir_fd, path.as_ptr(), libc::AT_REMOVEDIR) }.check_libc_neg()?; Ok(()) } -pub fn stat_path( - path: &OsStr, - dir_fd: Option, - follow_symlinks: bool, -) -> io::Result> { - use crate::os::ffi::OsStrExt; - - let path = match CString::new(path.as_bytes()) { - Ok(path) => path, - Err(_) => return Err(io::Error::from(io::ErrorKind::InvalidInput)), - }; - - let mut stat = core::mem::MaybeUninit::uninit(); - if let Some(dir_fd) = dir_fd { - let flags = if follow_symlinks { - 0 - } else { - libc::AT_SYMLINK_NOFOLLOW - }; - unsafe { libc::fstatat(dir_fd, path.as_ptr(), stat.as_mut_ptr(), flags) } - .check_libc_neg()?; - return Ok(Some(unsafe { stat.assume_init() })); - } - - let ret = if follow_symlinks { - unsafe { libc::stat(path.as_ptr(), stat.as_mut_ptr()) } - } else { - unsafe { libc::lstat(path.as_ptr(), stat.as_mut_ptr()) } - }; - ret.check_libc_neg()?; - Ok(Some(unsafe { stat.assume_init() })) -} - pub fn stat_fd(fd: crate::crt_fd::Borrowed<'_>) -> io::Result { crate::fileutils::fstat(fd) } diff --git a/crates/host_env/src/posix_windows.rs b/crates/host_env/src/posix_windows.rs new file mode 100644 index 00000000000..e78bd8f743f --- /dev/null +++ b/crates/host_env/src/posix_windows.rs @@ -0,0 +1,99 @@ +//! POSIX-compatible API for Windows. +//! +//! Python wraps POSIX syscalls such as `mkdir` and `open`. Windows doesn't directly implement +//! these syscalls, but they can be emulated with a mix of the Windows API and the Rust standard +//! library, the latter of which calls the former. + +use core::hint::cold_path; +use std::{fs, io, path::Path}; + +use widestring::WideCString; +use windows_sys::Win32::{ + Foundation::FALSE, + Storage::FileSystem::{MOVE_FILE_FLAGS, MOVEFILE_REPLACE_EXISTING, MoveFileExW}, +}; + +use crate::crt_fd; + +pub type RawMode = u32; + +pub fn make_dir( + dir_fd: Option>, + path: impl AsRef, + _mode: RawMode, +) -> io::Result<()> { + debug_assert!(dir_fd.is_none()); + // TODO: On Windows, Python has an override if the mode is 0o700 + fs::create_dir(path) +} + +/// https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html +#[inline] +pub fn rename( + from: impl AsRef, + #[cfg_attr(not(debug_assertions), expect(unused_variables))] from_fd: Option< + crt_fd::Borrowed<'_>, + >, + to: impl AsRef, + #[cfg_attr(not(debug_assertions), expect(unused_variables))] to_fd: Option< + crt_fd::Borrowed<'_>, + >, +) -> io::Result<()> { + debug_assert!(from_fd.is_none()); + debug_assert!(to_fd.is_none()); + + rename_impl(from, to, 0) +} + +/// https://docs.python.org/3/library/os.html#os.replace +/// +/// Atomically replace `to` with `from`. +#[inline] +pub fn replace( + from: impl AsRef, + #[cfg_attr(not(debug_assertions), expect(unused_variables))] from_fd: Option< + crt_fd::Borrowed<'_>, + >, + to: impl AsRef, + #[cfg_attr(not(debug_assertions), expect(unused_variables))] to_fd: Option< + crt_fd::Borrowed<'_>, + >, +) -> io::Result<()> { + debug_assert!(from_fd.is_none()); + debug_assert!(to_fd.is_none()); + + rename_impl(from, to, MOVEFILE_REPLACE_EXISTING) +} + +fn rename_impl( + from: impl AsRef, + to: impl AsRef, + flags: MOVE_FILE_FLAGS, +) -> io::Result<()> { + let from = WideCString::from_os_str(from.as_ref()) + .map_err(io::Error::other)? + .into_vec_with_nul(); + let to = WideCString::from_os_str(to.as_ref()) + .map_err(io::Error::other)? + .into_vec_with_nul(); + + // SAFETY: + // * from and to are NUL terminated wide strings + let success = unsafe { + // Rust's [`std::fs::rename`] is more complicated than CPython's. Rust attempts to use modern APIs + // where available, such as `FileRenameInfoEx`, which better map to POSIX. CPython simply + // calls MoveFileExW so we'll do that for parity. However, it may be better to use the new + // APIs and fall back if possible, especially if they're faster. + // + // Unlike POSIX's rename, MoveFileExW does not automatically move between volumes. + // This is expected behavior in CPython. + MoveFileExW(from.as_ptr(), to.as_ptr(), flags) + }; + + if success != FALSE { + Ok(()) + } else { + cold_path(); + Err(io::Error::last_os_error()) + } +} diff --git a/crates/vm/src/readline.rs b/crates/host_env/src/readline.rs similarity index 86% rename from crates/vm/src/readline.rs rename to crates/host_env/src/readline.rs index bd0ecd73912..1015ec093aa 100644 --- a/crates/vm/src/readline.rs +++ b/crates/host_env/src/readline.rs @@ -12,7 +12,7 @@ pub enum ReadlineResult { Line(String), Eof, Interrupt, - Io(std::io::Error), + Io(io::Error), #[cfg(unix)] OsError(String), Other(OtherError), @@ -106,34 +106,18 @@ pub mod rustyline_readline { } pub fn load_history(&mut self, path: &Path) -> OtherResult<()> { - #[cfg(not(feature = "host_env"))] - { - let _ = path; - Err(io::Error::other("history requires the `host_env` feature").into()) - } - #[cfg(feature = "host_env")] - { - self.repl.load_history(path)?; - Ok(()) - } + self.repl.load_history(path)?; + Ok(()) } pub fn save_history(&mut self, path: &Path) -> OtherResult<()> { - #[cfg(not(feature = "host_env"))] - { - let _ = path; - Err(io::Error::other("history requires the `host_env` feature").into()) - } - #[cfg(feature = "host_env")] + if !path.exists() + && let Some(parent) = path.parent() { - if !path.exists() - && let Some(parent) = path.parent() - { - crate::host_env::fs::create_dir_all(parent)?; - } - self.repl.save_history(path)?; - Ok(()) + crate::fs::create_dir_all(parent)?; } + self.repl.save_history(path)?; + Ok(()) } pub fn add_history_entry(&mut self, entry: &str) -> OtherResult<()> { diff --git a/crates/host_env/src/resource.rs b/crates/host_env/src/resource.rs index 587428fe9b0..fdf4e1e6288 100644 --- a/crates/host_env/src/resource.rs +++ b/crates/host_env/src/resource.rs @@ -2,6 +2,40 @@ use std::io; use crate::os::CheckLibcResult; +pub use libc::{ + RLIM_INFINITY, RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_MEMLOCK, + RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK, c_long, rlim_t, rlimit, timeval, +}; + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "emscripten"))] +pub use libc::{RLIMIT_MSGQUEUE, RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_SIGPENDING}; + +#[cfg(target_os = "linux")] +pub use libc::RLIMIT_RTTIME; + +#[cfg(any( + target_os = "freebsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos" +))] +pub use libc::RLIMIT_SBSIZE; + +#[cfg(any(target_os = "freebsd", target_os = "solaris", target_os = "illumos"))] +pub use libc::{RLIMIT_NPTS, RLIMIT_SWAP}; + +#[cfg(any(target_os = "solaris", target_os = "illumos"))] +pub use libc::RLIMIT_VMEM; + +#[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "freebsd"))] +pub use libc::RUSAGE_THREAD; + +#[cfg(not(any(target_os = "windows", target_os = "redox")))] +pub use libc::{RUSAGE_CHILDREN, RUSAGE_SELF}; + +#[cfg(target_os = "android")] +pub const RLIM_NLIMITS: libc::c_int = 16; + #[derive(Debug, Clone, Copy)] pub struct RUsage { pub ru_utime: libc::timeval, diff --git a/crates/host_env/src/select.rs b/crates/host_env/src/select.rs index 385a6a110b2..3191f8d97c0 100644 --- a/crates/host_env/src/select.rs +++ b/crates/host_env/src/select.rs @@ -1,6 +1,16 @@ use core::mem::MaybeUninit; use std::io; +#[cfg(unix)] +pub use libc::{EINTR, FD_SETSIZE, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI}; + +#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))] +pub use libc::{ + EPOLL_CLOEXEC, EPOLLERR, EPOLLET, EPOLLEXCLUSIVE, EPOLLHUP, EPOLLIN, EPOLLMSG, EPOLLONESHOT, + EPOLLOUT, EPOLLPRI, EPOLLRDBAND, EPOLLRDHUP, EPOLLRDNORM, EPOLLWAKEUP, EPOLLWRBAND, + EPOLLWRNORM, +}; + #[cfg(unix)] pub mod platform { pub use libc::pollfd; diff --git a/crates/host_env/src/shm.rs b/crates/host_env/src/shm.rs index 78e7d3921bc..b50e460b79a 100644 --- a/crates/host_env/src/shm.rs +++ b/crates/host_env/src/shm.rs @@ -3,6 +3,8 @@ use std::io; use crate::os::CheckLibcResult; +pub use libc::mode_t; + pub fn shm_open(name: &CStr, flags: libc::c_int, mode: libc::c_uint) -> io::Result { #[cfg(target_os = "freebsd")] let mode = mode.try_into().unwrap(); diff --git a/crates/host_env/src/signal.rs b/crates/host_env/src/signal.rs index ab1974f37df..cd11998e5ff 100644 --- a/crates/host_env/src/signal.rs +++ b/crates/host_env/src/signal.rs @@ -327,8 +327,8 @@ pub fn valid_signals(max_signum: usize) -> io::Result> { } #[cfg(unix)] -pub fn sigset_contains(mask: &libc::sigset_t, signum: i32) -> bool { - unsafe { libc::sigismember(mask, signum) == 1 } +pub fn sigset_contains(mask: libc::sigset_t, signum: i32) -> bool { + unsafe { libc::sigismember(&mask, signum) == 1 } } #[cfg(windows)] diff --git a/crates/host_env/src/socket.rs b/crates/host_env/src/socket.rs index c409132a725..542031e6fa8 100644 --- a/crates/host_env/src/socket.rs +++ b/crates/host_env/src/socket.rs @@ -7,9 +7,52 @@ use std::os::fd::AsRawFd; #[cfg(unix)] use std::{io, os::fd::BorrowedFd}; +/// Returns the system's hostname. +#[cfg(not(target_arch = "wasm32"))] +pub fn hostname() -> std::ffi::OsString { + gethostname::gethostname() +} + +#[cfg(not(target_arch = "wasm32"))] +pub use ::dns_lookup as dns; +#[cfg(not(target_arch = "wasm32"))] +pub use ::socket2 as raw; + +/// Returns the first non-loopback MAC address as 6 bytes, or `None` when no +/// MAC address is available or the lookup fails. +#[cfg(not(any( + target_os = "ios", + target_os = "android", + target_os = "windows", + target_arch = "wasm32", + target_os = "redox" +)))] +pub fn mac_address() -> Option<[u8; 6]> { + mac_address::get_mac_address() + .ok() + .flatten() + .map(|m| m.bytes()) +} + +#[cfg(unix)] +pub use libc::{AF_UNIX, SOCK_STREAM, sa_family_t, sockaddr_storage, socklen_t}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub use libc::{AF_ALG, AF_CAN}; + +// bionic (Android) does not define the CAN/ALG sockaddr structs. +#[cfg(target_os = "linux")] +pub use libc::{sockaddr_alg, sockaddr_can}; + +/// Set the system's hostname from its filesystem-encoded bytes. +/// +/// `socketmodule.c socket_sethostname` reads the argument as a buffer and +/// passes `buf.buf`/`buf.len` straight to the syscall, so a name is not +/// required to be UTF-8; taking `&[u8]` keeps that true here as well. #[cfg(all(unix, not(target_os = "redox")))] -pub fn sethostname(hostname: &str) -> io::Result<()> { - nix::unistd::sethostname(hostname).map_err(io::Error::from) +pub fn sethostname(hostname: &[u8]) -> io::Result<()> { + use std::os::unix::ffi::OsStrExt; + nix::unistd::sethostname(std::ffi::OsStr::from_bytes(hostname)).map_err(io::Error::from) } #[cfg(unix)] diff --git a/crates/host_env/src/syslog.rs b/crates/host_env/src/syslog.rs index 8820b8f1c5d..2ba38377326 100644 --- a/crates/host_env/src/syslog.rs +++ b/crates/host_env/src/syslog.rs @@ -3,6 +3,16 @@ use core::ffi::CStr; use parking_lot::RwLock; use std::{os::raw::c_char, sync::OnceLock}; +pub use libc::{ + LOG_ALERT, LOG_AUTH, LOG_CONS, LOG_CRIT, LOG_DAEMON, LOG_DEBUG, LOG_EMERG, LOG_ERR, LOG_INFO, + LOG_KERN, LOG_LOCAL0, LOG_LOCAL1, LOG_LOCAL2, LOG_LOCAL3, LOG_LOCAL4, LOG_LOCAL5, LOG_LOCAL6, + LOG_LOCAL7, LOG_LPR, LOG_MAIL, LOG_NDELAY, LOG_NEWS, LOG_NOTICE, LOG_NOWAIT, LOG_ODELAY, + LOG_PID, LOG_SYSLOG, LOG_USER, LOG_UUCP, LOG_WARNING, +}; + +#[cfg(not(target_os = "redox"))] +pub use libc::{LOG_AUTHPRIV, LOG_CRON, LOG_PERROR}; + #[derive(Debug)] enum GlobalIdent { Explicit(Box), diff --git a/crates/host_env/src/termios.rs b/crates/host_env/src/termios.rs index 074d03a455b..612f68be924 100644 --- a/crates/host_env/src/termios.rs +++ b/crates/host_env/src/termios.rs @@ -1,5 +1,55 @@ pub type Termios = ::termios::Termios; +#[cfg(any(target_os = "illumos", target_os = "solaris"))] +pub use libc::{CSTART, CSTOP, CSWTCH}; + +#[cfg(any( + target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +))] +pub use libc::{FIOASYNC, TIOCGETD, TIOCSETD}; + +pub use libc::{FIOCLEX, FIONBIO, TIOCGWINSZ, TIOCSWINSZ}; + +#[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +))] +pub use libc::{ + FIONCLEX, FIONREAD, TIOCEXCL, TIOCM_CAR, TIOCM_CD, TIOCM_CTS, TIOCM_DSR, TIOCM_DTR, TIOCM_LE, + TIOCM_RI, TIOCM_RNG, TIOCM_RTS, TIOCM_SR, TIOCM_ST, TIOCMBIC, TIOCMBIS, TIOCMGET, TIOCMSET, + TIOCNXCL, TIOCSCTTY, +}; + +#[cfg(any(target_os = "android", target_os = "linux"))] +pub use libc::{ + IBSHIFT, TCFLSH, TCGETA, TCGETS, TCSBRK, TCSETA, TCSETAF, TCSETAW, TCSETS, TCSETSF, TCSETSW, + TCXONC, TIOCGSERIAL, TIOCGSOFTCAR, TIOCINQ, TIOCLINUX, TIOCSSOFTCAR, XTABS, +}; + +#[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos" +))] +pub use libc::{TIOCCONS, TIOCGPGRP, TIOCOUTQ, TIOCSPGRP, TIOCSTI}; + +#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "macos"))] +pub use libc::{ + TIOCNOTTY, TIOCPKT, TIOCPKT_DATA, TIOCPKT_DOSTOP, TIOCPKT_FLUSHREAD, TIOCPKT_FLUSHWRITE, + TIOCPKT_NOSTOP, TIOCPKT_START, TIOCPKT_STOP, +}; + #[cfg(any( target_os = "android", target_os = "freebsd", @@ -115,3 +165,27 @@ pub fn tcflush(fd: i32, queue: i32) -> std::io::Result<()> { pub fn tcflow(fd: i32, action: i32) -> std::io::Result<()> { ::termios::tcflow(fd, action) } + +pub fn tcgetwinsize(fd: i32) -> std::io::Result<(u16, u16)> { + let mut size: libc::winsize = unsafe { core::mem::zeroed() }; + let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; + if ret < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok((size.ws_row, size.ws_col)) +} + +pub fn tcsetwinsize(fd: i32, row: u16, col: u16) -> std::io::Result<()> { + let mut size: libc::winsize = unsafe { core::mem::zeroed() }; + let ret = unsafe { libc::ioctl(fd, TIOCGWINSZ as _, &mut size) }; + if ret < 0 { + return Err(std::io::Error::last_os_error()); + } + size.ws_row = row; + size.ws_col = col; + let ret = unsafe { libc::ioctl(fd, TIOCSWINSZ as _, &size) }; + if ret < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index 3e3227d78de..19f320dad33 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -15,7 +15,7 @@ pub const SEC_TO_NS: i64 = SEC_TO_MS * MS_TO_NS; pub const NS_TO_MS: i64 = 1000 * 1000; pub const NS_TO_US: i64 = 1000; -/// Access to the C runtime's `tzset` / `timezone` / `daylight` / `tzname` +/// Access to the C runtime's `tzset` / `timezone` / `altzone` / `daylight` / `tzname` /// globals used by Python's `time` module. /// /// Not available under MSVC (which exposes these only via the @@ -28,6 +28,10 @@ pub mod tz { static c_daylight: core::ffi::c_int; #[link_name = "timezone"] static c_timezone: core::ffi::c_long; + // Set by `build.rs` when `time.h` exposes `altzone` (CPython `HAVE_ALTZONE`). + #[cfg(has_altzone)] + #[link_name = "altzone"] + static c_altzone: core::ffi::c_long; #[link_name = "tzname"] static c_tzname: [*const core::ffi::c_char; 2]; #[link_name = "tzset"] @@ -43,6 +47,22 @@ pub mod tz { unsafe { c_timezone } } + /// DST offset west of UTC in seconds, matching CPython's `time.altzone`. + /// + /// Uses the C `altzone` global when available; otherwise falls back to + /// `timezone - 3600` (same as CPython without `HAVE_ALTZONE`). + #[must_use] + pub fn altzone() -> core::ffi::c_long { + #[cfg(has_altzone)] + { + unsafe { c_altzone } + } + #[cfg(not(has_altzone))] + { + timezone() - 3600 + } + } + #[cfg(not(target_os = "freebsd"))] #[must_use] pub fn daylight() -> core::ffi::c_int { @@ -207,11 +227,13 @@ pub fn process_times() -> std::io::Result { }) } -#[cfg(unix)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg(any(unix, target_os = "wasi"))] +#[derive(Copy, Clone, Debug)] +// WASI libc represents clockid_t as an opaque pointer type without Eq or PartialEq. +#[cfg_attr(unix, derive(Eq, PartialEq))] pub struct ClockId(libc::clockid_t); -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl ClockId { pub const fn from_raw(raw: libc::clockid_t) -> Self { Self(raw) @@ -239,6 +261,7 @@ impl ClockId { target_os = "solaris", target_os = "openbsd", target_os = "redox", + target_os = "wasi", )))] pub const CLOCK_THREAD_CPUTIME_ID: Self = Self(libc::CLOCK_THREAD_CPUTIME_ID); } @@ -255,6 +278,20 @@ pub fn clock_gettime(id: ClockId) -> std::io::Result { .map_err(std::io::Error::from) } +#[cfg(target_os = "wasi")] +pub fn clock_gettime(id: ClockId) -> std::io::Result { + let mut ts = core::mem::MaybeUninit::::uninit(); + + let ret = unsafe { libc::clock_gettime(id.as_raw(), ts.as_mut_ptr()) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + + let ts = unsafe { ts.assume_init() }; + + Ok(Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)) +} + #[cfg(all(unix, not(target_os = "redox")))] pub fn clock_getres(id: ClockId) -> std::io::Result { nix::time::clock_getres(nix_clock_id(id)) @@ -297,7 +334,7 @@ pub fn gethrvtime_duration() -> Duration { Duration::from_nanos(unsafe { libc::gethrvtime() }) } -#[cfg(target_env = "msvc")] +#[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug)] pub struct WindowsTimeZoneInfo { @@ -308,7 +345,7 @@ pub struct WindowsTimeZoneInfo { pub daylight_name: String, } -#[cfg(target_env = "msvc")] +#[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] fn decode_tz_name(name: &[u16]) -> String { widestring::decode_utf16_lossy(name.iter().copied()) @@ -316,7 +353,7 @@ fn decode_tz_name(name: &[u16]) -> String { .collect() } -#[cfg(target_env = "msvc")] +#[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] #[must_use] pub fn get_tz_info() -> WindowsTimeZoneInfo { @@ -618,10 +655,9 @@ unsafe extern "C" { #[cfg(windows)] pub fn strftime_ascii(fmt: &str, tm: &libc::tm) -> Result { - if fmt.contains('\0') { - return Err(CheckedTmError::EmbeddedNul); - } - let fmt_wide: Vec = fmt.encode_utf16().chain(core::iter::once(0)).collect(); + let fmt_wide = widestring::WideCString::from_str(fmt) + .map_err(|_| CheckedTmError::EmbeddedNul)? + .into_vec_with_nul(); let mut size = 1024usize; let max_scale = 256usize.saturating_mul(fmt.len().max(1)); loop { diff --git a/crates/host_env/src/winapi.rs b/crates/host_env/src/winapi.rs index 4e4536c3518..19e18f32d3f 100644 --- a/crates/host_env/src/winapi.rs +++ b/crates/host_env/src/winapi.rs @@ -3,22 +3,20 @@ reason = "This module mirrors Win32 APIs with raw handle and pointer parameters." )] +use core::hint::cold_path; use std::{io, path::Path}; -use windows_sys::Win32::{ - Foundation::{HANDLE, HMODULE, WAIT_FAILED}, - System::Threading::PROCESS_INFORMATION, -}; use crate::windows::{CheckWin32Bool, CheckWin32Handle}; +use memchr::memchr; pub use windows_sys::Win32::{ Foundation::{ DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NETNAME_DELETED, ERROR_NO_DATA, ERROR_NO_SYSTEM_RESOURCES, ERROR_NOT_FOUND, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, ERROR_PORT_UNREACHABLE, ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, - ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, STILL_ACTIVE, WAIT_ABANDONED_0, WAIT_OBJECT_0, - WAIT_TIMEOUT, + ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, HANDLE, HMODULE, STILL_ACTIVE, + WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT, }, Globalization::{ LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, LCMAP_LINGUISTIC_CASING, @@ -57,16 +55,17 @@ pub use windows_sys::Win32::{ ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_BREAKAWAY_FROM_JOB, CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS, HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, - NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, REALTIME_PRIORITY_CLASS, - STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, STARTF_PREVENTPINNING, - STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID, STARTF_TITLEISLINKNAME, - STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS, STARTF_USEFILLATTRIBUTE, - STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW, STARTF_USESIZE, - STARTF_USESTDHANDLES, + NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, PROCESS_INFORMATION, + REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, + STARTF_PREVENTPINNING, STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID, + STARTF_TITLEISLINKNAME, STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS, + STARTF_USEFILLATTRIBUTE, STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW, + STARTF_USESIZE, STARTF_USESTDHANDLES, }, }, UI::WindowsAndMessaging::SW_HIDE, }; +use windows_sys::w; pub type Handle = HANDLE; pub type StdHandle = windows_sys::Win32::System::Console::STD_HANDLE; @@ -312,7 +311,8 @@ pub fn build_environment_block( let mut last_entry: HashMap> = HashMap::new(); for (key, value) in entries { - if key.contains('\0') || value.contains('\0') { + if memchr(b'\0', key.as_bytes()).is_some() || memchr(b'\0', value.as_bytes()).is_some() { + cold_path(); return Err(BuildEnvironmentBlockError::ContainsNul); } if key.is_empty() || key[1..].contains('=') { @@ -1094,14 +1094,14 @@ where return Err(MimeRegistryReadError::Os(err)); } - let content_type_key: Vec = "Content Type\0".encode_utf16().collect(); + let content_type_key = w!("Content Type"); let mut type_buf = [0u16; 256]; let mut cb_type = (type_buf.len() * 2) as u32; let mut reg_type = 0; let err = unsafe { RegQueryValueExW( subkey, - content_type_key.as_ptr(), + content_type_key, core::ptr::null_mut(), &mut reg_type, type_buf.as_mut_ptr().cast(), diff --git a/crates/host_env/src/windows.rs b/crates/host_env/src/windows.rs index bde8d679737..635f12f3f38 100644 --- a/crates/host_env/src/windows.rs +++ b/crates/host_env/src/windows.rs @@ -4,24 +4,27 @@ use std::{ io, os::windows::ffi::{OsStrExt, OsStringExt}, }; -use windows_sys::Win32::{ - Foundation::{ - E_POINTER, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_NO_UNICODE_TRANSLATION, - MAX_PATH, S_OK, - }, - Networking::WinSock::WSAStartup, - Storage::FileSystem::{ - GetFileVersionInfoSizeW, GetFileVersionInfoW, VS_FIXEDFILEINFO, VerQueryValueW, - }, - System::{ - Diagnostics::Debug::{ - FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM, - FORMAT_MESSAGE_IGNORE_INSERTS, FormatMessageW, +use windows_sys::{ + Win32::{ + Foundation::{ + E_POINTER, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, + ERROR_NO_UNICODE_TRANSLATION, MAX_PATH, S_OK, + }, + Networking::WinSock::WSAStartup, + Storage::FileSystem::{ + GetFileVersionInfoSizeW, GetFileVersionInfoW, VS_FIXEDFILEINFO, VerQueryValueW, + }, + System::{ + Diagnostics::Debug::{ + FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM, + FORMAT_MESSAGE_IGNORE_INSERTS, FormatMessageW, + }, + LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}, + SystemInformation::{GetVersionExW, OSVERSIONINFOEXW, OSVERSIONINFOW}, + Threading::{GetCurrentThreadStackLimits, SetThreadStackGuarantee}, }, - LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}, - SystemInformation::{GetVersionExW, OSVERSIONINFOEXW, OSVERSIONINFOW}, - Threading::{GetCurrentThreadStackLimits, SetThreadStackGuarantee}, }, + w, }; /// _MAX_ENV from Windows CRT stdlib.h - maximum environment variable size @@ -154,8 +157,8 @@ pub struct WindowsVersionInfo { fn get_kernel32_version() -> io::Result<(u32, u32, u32)> { unsafe { - let module_name: Vec = OsStr::new("kernel32.dll").to_wide_with_nul(); - let h_kernel32 = GetModuleHandleW(module_name.as_ptr()).check_nonnull()?; + let module_name = w!("kernel32.dll"); + let h_kernel32 = GetModuleHandleW(module_name).check_nonnull()?; let mut kernel32_path = [0u16; MAX_PATH as usize]; let len = GetModuleFileNameW( @@ -181,13 +184,13 @@ fn get_kernel32_version() -> io::Result<(u32, u32, u32)> { ) .check_win32_bool()?; - let sub_block: Vec = OsStr::new("").to_wide_with_nul(); + let sub_block = w!(""); let mut ffi_ptr: *mut VS_FIXEDFILEINFO = core::ptr::null_mut(); let mut ffi_len: u32 = 0; VerQueryValueW( ver_block.as_ptr() as *const _, - sub_block.as_ptr(), + sub_block, &mut ffi_ptr as *mut *mut VS_FIXEDFILEINFO as *mut *mut _, &mut ffi_len as *mut u32, ) diff --git a/crates/host_env/src/wmi.rs b/crates/host_env/src/wmi.rs index a6d77f77e9f..2b46eebcbe5 100644 --- a/crates/host_env/src/wmi.rs +++ b/crates/host_env/src/wmi.rs @@ -6,16 +6,20 @@ #![allow(unsafe_op_in_unsafe_fn)] use core::ffi::c_void; -use core::ptr::{null, null_mut}; +use core::ptr::{NonNull, null, null_mut}; +use widestring::WideCString; use windows_sys::Win32::Foundation::{ - CloseHandle, ERROR_BROKEN_PIPE, ERROR_MORE_DATA, ERROR_NOT_ENOUGH_MEMORY, GetLastError, HANDLE, - WAIT_OBJECT_0, WAIT_TIMEOUT, + CloseHandle, ERROR_BROKEN_PIPE, ERROR_INVALID_NAME, ERROR_MORE_DATA, ERROR_NOT_ENOUGH_MEMORY, + GetLastError, HANDLE, WAIT_OBJECT_0, WAIT_TIMEOUT, }; use windows_sys::Win32::Storage::FileSystem::{ReadFile, WriteFile}; use windows_sys::Win32::System::Pipes::CreatePipe; use windows_sys::Win32::System::Threading::{ CreateEventW, CreateThread, GetExitCodeThread, SetEvent, WaitForSingleObject, }; +use windows_sys::w; + +use crate::ctypes::wcslen; pub const BUFFER_SIZE: usize = 8192; @@ -238,7 +242,7 @@ unsafe fn object_end_enumeration(this: *mut c_void) -> HRESULT { method(this) } -fn hresult_from_win32(err: u32) -> HRESULT { +const fn hresult_from_win32(err: u32) -> HRESULT { if err == 0 { 0 } else { @@ -246,26 +250,14 @@ fn hresult_from_win32(err: u32) -> HRESULT { } } -fn succeeded(hr: HRESULT) -> bool { +const fn succeeded(hr: HRESULT) -> bool { hr >= 0 } -fn failed(hr: HRESULT) -> bool { +const fn failed(hr: HRESULT) -> bool { hr < 0 } -fn wide_str(s: &str) -> Vec { - s.encode_utf16().chain(core::iter::once(0)).collect() -} - -unsafe fn wcslen(s: *const u16) -> usize { - let mut len = 0; - while unsafe { *s.add(len) } != 0 { - len += 1; - } - len -} - unsafe fn wait_event(event: HANDLE, timeout: u32) -> u32 { match unsafe { WaitForSingleObject(event, timeout) } { WAIT_OBJECT_0 => 0, @@ -352,8 +344,8 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { } if succeeded(hr) { - let root_cimv2 = wide_str("ROOT\\CIMV2"); - let bstr_root = unsafe { SysAllocString(root_cimv2.as_ptr()) }; + let root_cimv2 = w!("ROOT\\CIMV2"); + let bstr_root = unsafe { SysAllocString(root_cimv2) }; hr = unsafe { locator_connect_server( locator, @@ -390,8 +382,8 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { }; } if succeeded(hr) { - let wql = wide_str("WQL"); - let bstr_wql = unsafe { SysAllocString(wql.as_ptr()) }; + let wql = w!("WQL"); + let bstr_wql = unsafe { SysAllocString(wql) }; hr = unsafe { services_exec_query( services, @@ -471,16 +463,25 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { } if succeeded(hr) && (flavor & WBEM_FLAVOR_MASK_ORIGIN) != WBEM_FLAVOR_ORIGIN_SYSTEM { + let Some(cb_str1) = NonNull::new(prop_name) + .map(|prop_name| (unsafe { wcslen(prop_name) } * 2) as u32) + else { + unsafe { + SysFreeString(prop_name); + } + break; + }; + let mut prop_str = [0u16; BUFFER_SIZE]; hr = unsafe { VariantToString(&prop_value, prop_str.as_mut_ptr(), BUFFER_SIZE as u32) }; + let cb_str2 = NonNull::new(prop_str.as_ptr().cast_mut()) + .map(|prop_str| (unsafe { wcslen(prop_str) } * 2) as u32) + .expect("prop_str is never null"); - if succeeded(hr) { - let cb_str1 = (unsafe { wcslen(prop_name) } * 2) as u32; - let cb_str2 = (unsafe { wcslen(prop_str.as_ptr()) } * 2) as u32; - - if unsafe { + if succeeded(hr) + && unsafe { WriteFile( write_pipe, prop_name as *const _, @@ -489,36 +490,35 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { null_mut(), ) } == 0 - || unsafe { - WriteFile( - write_pipe, - &eq_sign as *const u16 as *const _, - 2, - &mut written, - null_mut(), - ) - } == 0 - || unsafe { - WriteFile( - write_pipe, - prop_str.as_ptr() as *const _, - cb_str2, - &mut written, - null_mut(), - ) - } == 0 - || unsafe { - WriteFile( - write_pipe, - &null_sep as *const u16 as *const _, - 2, - &mut written, - null_mut(), - ) - } == 0 - { - hr = hresult_from_win32(unsafe { GetLastError() }); - } + || unsafe { + WriteFile( + write_pipe, + &eq_sign as *const u16 as *const _, + 2, + &mut written, + null_mut(), + ) + } == 0 + || unsafe { + WriteFile( + write_pipe, + prop_str.as_ptr() as *const _, + cb_str2, + &mut written, + null_mut(), + ) + } == 0 + || unsafe { + WriteFile( + write_pipe, + &null_sep as *const u16 as *const _, + 2, + &mut written, + null_mut(), + ) + } == 0 + { + hr = hresult_from_win32(unsafe { GetLastError() }); } unsafe { @@ -555,7 +555,9 @@ unsafe fn query_thread_impl(param: *mut c_void) -> u32 { } pub fn exec_query(query_str: &str) -> Result { - let query_wide = wide_str(query_str); + let query = WideCString::from_str(query_str) + .map_err(|_| ExecQueryError::Code(ERROR_INVALID_NAME))? + .into(); let mut h_thread: HANDLE = null_mut(); let mut err: u32 = 0; @@ -577,7 +579,7 @@ pub fn exec_query(query_str: &str) -> Result { err = GetLastError(); } else { let thread_data = Box::new(QueryThreadData { - query: query_wide, + query, write_pipe, init_event, connect_event, diff --git a/crates/jit/src/instructions.rs b/crates/jit/src/instructions.rs index 6aff9f093e5..a3c4ca800c4 100644 --- a/crates/jit/src/instructions.rs +++ b/crates/jit/src/instructions.rs @@ -39,6 +39,7 @@ impl JitValue { JitType::Int => Self::Int(val), JitType::Float => Self::Float(val), JitType::Bool => Self::Bool(val), + JitType::None => unreachable!("None cannot be used as an argument type"), } } @@ -47,7 +48,8 @@ impl JitValue { Self::Int(_) => Some(JitType::Int), Self::Float(_) => Some(JitType::Float), Self::Bool(_) => Some(JitType::Bool), - Self::None | Self::Null | Self::Tuple(_) | Self::FuncRef(_) => None, + Self::None => Some(JitType::None), + Self::Null | Self::Tuple(_) | Self::FuncRef(_) => None, } } @@ -112,8 +114,9 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> { #[expect(clippy::mut_mut, reason = "This seems like a false positive")] let builder = &mut self.builder; let ty = val.to_jit_type().ok_or(JitCompileError::NotSupported)?; + let cranelift_ty = ty.to_cranelift().ok_or(JitCompileError::NotSupported)?; let local = self.variables[idx].get_or_insert_with(|| { - let var = builder.declare_var(ty.to_cranelift()); + let var = builder.declare_var(cranelift_ty); Local { var, ty: ty.clone(), @@ -328,27 +331,27 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> { } fn return_value(&mut self, val: JitValue) -> Result<(), JitCompileError> { - if let Some(ref ty) = self.sig.ret { - // If the signature has a return type, enforce it - if val.to_jit_type().as_ref() != Some(ty) { + let val_type = val.to_jit_type().ok_or(JitCompileError::NotSupported)?; + if let Some(ref ret_type) = self.sig.ret { + if ret_type != &val_type { return Err(JitCompileError::NotSupported); } } else { - // First time we see a return, define it in the signature - let ty = val.to_jit_type().ok_or(JitCompileError::NotSupported)?; - self.sig.ret = Some(ty.clone()); - self.builder - .func - .signature - .returns - .push(AbiParam::new(ty.to_cranelift())); + self.sig.ret = Some(val_type.clone()); + if let Some(val_type) = val_type.to_cranelift() { + self.builder + .func + .signature + .returns + .push(AbiParam::new(val_type)); + } } - // If this is e.g. an Int, Float, or Bool we have a Cranelift `Value`. - // If we have JitValue::None or .Tuple(...) but can't handle that, error out (or handle differently). - let cr_val = val.into_value().ok_or(JitCompileError::NotSupported)?; - - self.builder.ins().return_(&[cr_val]); + if let Some(cr_val) = val.into_value() { + self.builder.ins().return_(&[cr_val]); + } else { + self.builder.ins().return_(&[]); + } Ok(()) } @@ -545,8 +548,22 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> { match self.stack.pop().ok_or(JitCompileError::BadBytecode)? { JitValue::FuncRef(reference) => { let call = self.builder.ins().call(reference, &args); - let returns = self.builder.inst_results(call); - self.stack.push(JitValue::Int(returns[0])); + // The only callable reachable here is this function itself, + // so the result carries the declared return type - it is not + // always an Int. A function whose return type is still + // unknown has no return slot in the signature it was + // declared with, and there is nothing to type the result as. + let ret = match *self.builder.inst_results(call) { + [] => None, + [val] => Some(val), + _ => return Err(JitCompileError::NotSupported), + }; + let val = match (self.sig.ret.clone(), ret) { + (Some(JitType::None), None) => JitValue::None, + (Some(ty), Some(val)) => JitValue::from_type_and_value(ty, val), + _ => return Err(JitCompileError::NotSupported), + }; + self.stack.push(val); Ok(()) } @@ -666,6 +683,11 @@ impl<'a, 'b> FunctionCompiler<'a, 'b> { | Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => { let oparg = var_nums.get(arg); let (idx1, idx2) = oparg.indexes(); + + #[expect( + clippy::tuple_array_conversions, + reason = "Seems like a false positive" + )] for idx in [idx1, idx2] { let local = self.variables[idx] .as_ref() diff --git a/crates/jit/src/lib.rs b/crates/jit/src/lib.rs index dbaa4a3eb26..0c700e93cf8 100644 --- a/crates/jit/src/lib.rs +++ b/crates/jit/src/lib.rs @@ -61,19 +61,12 @@ impl Jit { ret: Option, ) -> Result<(FuncId, JitSig), JitCompileError> { for arg in args { - self.ctx - .func - .signature - .params - .push(AbiParam::new(arg.to_cranelift())); + let arg = arg.to_cranelift().ok_or(JitCompileError::NotSupported)?; + self.ctx.func.signature.params.push(AbiParam::new(arg)); } - if ret.is_some() { - self.ctx - .func - .signature - .returns - .push(AbiParam::new(ret.clone().unwrap().to_cranelift())); + if let Some(ret) = ret.as_ref().and_then(JitType::to_cranelift) { + self.ctx.func.signature.returns.push(AbiParam::new(ret)); } let id = self.module.declare_function( @@ -167,7 +160,10 @@ impl CompiledCode { libffi::middle::CodePtr::from_ptr(self.code as *const _), cif_args, ); - self.sig.ret.as_ref().map(|ty| value.to_typed(ty)) + match self.sig.ret.as_ref() { + Some(JitType::None) | None => None, + Some(ty) => Some(value.to_typed(ty)), + } } } } @@ -193,14 +189,16 @@ pub enum JitType { Int, Float, Bool, + None, } impl JitType { - fn to_cranelift(&self) -> types::Type { + fn to_cranelift(&self) -> Option { match self { - Self::Int => types::I64, - Self::Float => types::F64, - Self::Bool => types::I8, + Self::Int => Some(types::I64), + Self::Float => Some(types::F64), + Self::Bool => Some(types::I8), + Self::None => None, } } @@ -209,6 +207,7 @@ impl JitType { Self::Int => libffi::middle::Type::i64(), Self::Float => libffi::middle::Type::f64(), Self::Bool => libffi::middle::Type::u8(), + Self::None => libffi::middle::Type::void(), } } } @@ -306,6 +305,7 @@ impl UnTypedAbiValue { JitType::Int => AbiValue::Int(self.int), JitType::Float => AbiValue::Float(self.float), JitType::Bool => AbiValue::Bool(self.boolean != 0), + JitType::None => unreachable!("None has no ABI value"), } } } diff --git a/crates/jit/tests/bool_tests.rs b/crates/jit/tests/bool_tests.rs index 8a5f4ea9db3..1874ee4d55d 100644 --- a/crates/jit/tests/bool_tests.rs +++ b/crates/jit/tests/bool_tests.rs @@ -202,4 +202,18 @@ mod tests { assert_eq!(lte(false, 1), Ok(1)); assert_eq!(lte(true, 0), Ok(0)); } + + #[test] + fn recursive_bool() { + let recursive_bool = jit_function! { recursive_bool(n: i64) -> bool => r##" + def recursive_bool(n: int) -> bool: + if n == 0: + return True + return not recursive_bool(n - 1) + "## }; + + assert_eq!(recursive_bool(0), Ok(true)); + assert_eq!(recursive_bool(1), Ok(false)); + assert_eq!(recursive_bool(4), Ok(true)); + } } diff --git a/crates/jit/tests/float_tests.rs b/crates/jit/tests/float_tests.rs index b9bbb3ea63c..f667b1e764a 100644 --- a/crates/jit/tests/float_tests.rs +++ b/crates/jit/tests/float_tests.rs @@ -379,4 +379,18 @@ mod tests { assert_eq!(float_lte(f64::NAN, f64::NAN), Ok(false)); assert_eq!(float_lte(f64::INFINITY, f64::NEG_INFINITY), Ok(false)); } + + #[test] + fn recursive_float() { + let recursive_float = jit_function! { recursive_float(n: i64) -> f64 => r##" + def recursive_float(n: int) -> float: + if n == 0: + return 1.0 + return recursive_float(n - 1) / 2.0 + "## }; + + assert_eq!(recursive_float(0), Ok(1.0)); + assert_eq!(recursive_float(1), Ok(0.5)); + assert_eq!(recursive_float(4), Ok(0.0625)); + } } diff --git a/crates/jit/tests/misc_tests.rs b/crates/jit/tests/misc_tests.rs index b73100ad6ec..5404df0a769 100644 --- a/crates/jit/tests/misc_tests.rs +++ b/crates/jit/tests/misc_tests.rs @@ -2,16 +2,15 @@ mod tests { use rustpython_jit::{AbiValue, JitArgumentError}; - // TODO currently broken - // #[test] - // fn test_no_return_value() { - // let func = jit_function! { func() => r##" - // def func(): - // pass - // "## }; - // - // assert_eq!(func(), Ok(())); - // } + #[test] + fn no_return_value() { + let func = jit_function! { func() => r##" + def func(): + pass + "## }; + + assert_eq!(func(), Ok(())); + } #[test] fn invoke() { diff --git a/crates/literal/Cargo.toml b/crates/literal/Cargo.toml index b9795a771eb..60350f7937d 100644 --- a/crates/literal/Cargo.toml +++ b/crates/literal/Cargo.toml @@ -9,13 +9,13 @@ license = { workspace = true } rust-version = { workspace = true } [dependencies] +rustpython-unicode = { workspace = true } rustpython-wtf8 = { workspace = true } hexf-parse = { workspace = true } is-macro.workspace = true lexical-parse-float = { workspace = true, features = ["format"] } num-traits = { workspace = true } -icu_properties = { workspace = true } [dev-dependencies] rand = { workspace = true } diff --git a/crates/literal/src/char.rs b/crates/literal/src/char.rs deleted file mode 100644 index 5b446cc1a19..00000000000 --- a/crates/literal/src/char.rs +++ /dev/null @@ -1,26 +0,0 @@ -use icu_properties::props::{EnumeratedProperty, GeneralCategory}; - -/// According to python following categories aren't printable: -/// * Cc (Other, Control) -/// * Cf (Other, Format) -/// * Cs (Other, Surrogate) -/// * Co (Other, Private Use) -/// * Cn (Other, Not Assigned) -/// * Zl Separator, Line ('\u2028', LINE SEPARATOR) -/// * Zp Separator, Paragraph ('\u2029', PARAGRAPH SEPARATOR) -/// * Zs (Separator, Space) other than ASCII space('\x20'). -pub fn is_printable(c: char) -> bool { - let cat = GeneralCategory::for_char(c); - - !matches!( - cat, - GeneralCategory::SpaceSeparator - | GeneralCategory::LineSeparator - | GeneralCategory::ParagraphSeparator - | GeneralCategory::Control - | GeneralCategory::Format - | GeneralCategory::Surrogate - | GeneralCategory::PrivateUse - | GeneralCategory::Unassigned - ) -} diff --git a/crates/literal/src/complex.rs b/crates/literal/src/complex.rs index bbfc88cb367..bc193f0f204 100644 --- a/crates/literal/src/complex.rs +++ b/crates/literal/src/complex.rs @@ -19,9 +19,9 @@ fn component_to_string(value: f64) -> String { if exponent < 16 && exponent > -5 { // Normal magnitude — Rust's default Display emits "1" for 1.0, // "1.5" for 1.5, "1000000000000000" for 1e15, etc. - value.to_string() + float::prefer_cpython_tie_repr(value.to_string(), value) } else { - alloc::format!("{significand}e{exponent:+#03}") + float::prefer_cpython_tie_repr(alloc::format!("{significand}e{exponent:+#03}"), value) } } else { // nan / inf / -inf — `format!("{x:e}")` produces e.g. "NaN" with no @@ -70,6 +70,14 @@ pub fn parse_str(s: &str) -> Option<(f64, f64)> { Some(s) => s.strip_suffix(')')?.trim(), }; + // Whitespace is only allowed around the whole string and the optional + // parentheses, never inside the numeric token. Reject it here so that + // `float::parse_str` (which tolerates surrounding whitespace on a part) + // does not let e.g. "1 +2j" through. + if s.contains(char::is_whitespace) { + return None; + } + let value = match s.strip_suffix(|c| c == 'j' || c == 'J') { None => (float::parse_str(s)?, 0.0), Some(mut s) => { @@ -96,3 +104,39 @@ pub fn parse_str(s: &str) -> Option<(f64, f64)> { }; Some(value) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_rejects_internal_whitespace() { + // Whitespace inside the numeric token is invalid, even where a bare + // `float::parse_str` on a fragment would tolerate it. + for s in [ + "1 +2j", "1 2j", "1 +2 j", "+ 1j", "1.5 j", "(1 +2j)", "2 -3j", + ] { + assert_eq!(parse_str(s), None, "{s:?} must not parse"); + } + } + + #[test] + fn parse_allows_surrounding_and_paren_whitespace() { + for s in [" 1+2j ", " (1+2j) ", "( 1+2j )"] { + assert_eq!(parse_str(s), Some((1.0, 2.0)), "{s:?}"); + } + } + + #[test] + fn parse_basic() { + assert_eq!(parse_str("1"), Some((1.0, 0.0))); + assert_eq!(parse_str("1j"), Some((0.0, 1.0))); + assert_eq!(parse_str("j"), Some((0.0, 1.0))); + assert_eq!(parse_str("-j"), Some((0.0, -1.0))); + assert_eq!(parse_str("1+2j"), Some((1.0, 2.0))); + assert_eq!(parse_str("1e5j"), Some((0.0, 1e5))); + assert_eq!(parse_str("1_000"), Some((1000.0, 0.0))); + assert_eq!(parse_str(""), None); + assert_eq!(parse_str("abc"), None); + } +} diff --git a/crates/literal/src/escape.rs b/crates/literal/src/escape.rs index 1099c0a02bc..50dce8b264c 100644 --- a/crates/literal/src/escape.rs +++ b/crates/literal/src/escape.rs @@ -204,7 +204,7 @@ impl UnicodeEscape<'_> { '\\' | '\t' | '\r' | '\n' => 2, ch if ch < ' ' || ch as u32 == 0x7f => 4, // \xHH ch if ch.is_ascii() => 1, - ch if crate::char::is_printable(ch) => { + ch if rustpython_unicode::classify::is_repr_printable(ch) => { // max = std::cmp::max(ch, max); ch.len_utf8() } @@ -238,7 +238,7 @@ impl UnicodeEscape<'_> { ch if ch.is_ascii() => { write!(formatter, "\\x{:02x}", ch as u8) } - ch if crate::char::is_printable(ch) => formatter.write_char(ch), + ch if rustpython_unicode::classify::is_repr_printable(ch) => formatter.write_char(ch), '\0'..='\u{ff}' => { write!(formatter, "\\x{:02x}", ch as u32) } diff --git a/crates/literal/src/float.rs b/crates/literal/src/float.rs index 56a2b542993..2db4fd084a4 100644 --- a/crates/literal/src/float.rs +++ b/crates/literal/src/float.rs @@ -209,11 +209,13 @@ pub fn format_general( } } -fn prefer_cpython_tie_repr(s: String, value: f64) -> String { - let Some(exponent_pos) = s.find('e') else { - return s; - }; - let Some(digit_pos) = s[..exponent_pos].bytes().rposition(|b| b.is_ascii_digit()) else { +pub(crate) fn prefer_cpython_tie_repr(s: String, value: f64) -> String { + // Rust's shortest float formatter can land on the odd-digit neighbour of a + // rounding tie where round-half-to-even (what `repr` uses) picks the even + // one. When the last significant digit is odd and its even neighbour still + // round-trips and is no further from the value, prefer the even neighbour. + let boundary = s.find('e').unwrap_or(s.len()); + let Some(digit_pos) = s[..boundary].bytes().rposition(|b| b.is_ascii_digit()) else { return s; }; @@ -258,11 +260,11 @@ fn checked_pow_u128(base: u128, exp: u32) -> Option { } fn parse_decimal_rational(s: &str) -> Option<(u128, u32)> { - let exponent_pos = s.find('e')?; - let exponent = s[exponent_pos + 1..].parse::().ok()?; - let significand = s[..exponent_pos] - .strip_prefix('-') - .unwrap_or(&s[..exponent_pos]); + let (mantissa, exponent) = match s.find('e') { + Some(pos) => (&s[..pos], s[pos + 1..].parse::().ok()?), + None => (s, 0), + }; + let significand = mantissa.strip_prefix('-').unwrap_or(mantissa); let dot_pos = significand.find('.'); let frac_digits = dot_pos .map(|pos| significand.len().saturating_sub(pos + 1)) @@ -325,7 +327,7 @@ pub fn to_string(value: f64) -> String { if is_integer(value) { format!("{value:.1?}") } else { - value.to_string() + prefer_cpython_tie_repr(value.to_string(), value) } } else { prefer_cpython_tie_repr(format!("{significand}e{exponent:+#03}"), value) @@ -351,6 +353,21 @@ mod tests { "6.1005353927612305e-05" ); } + + #[test] + fn repr_normal_range_uses_cpython_tie_digit() { + // Rust's shortest formatter yields "161852602146008.13" for this + // value; round-half-to-even (what `repr` uses) picks "…08.12". + assert_eq!( + to_string(f64::from_bits(0x42e26687db6b9b04)), + "161852602146008.12" + ); + // Non-tie values are left untouched. + assert_eq!(to_string(1.5), "1.5"); + assert_eq!(to_string(0.1), "0.1"); + assert_eq!(to_string(12.34), "12.34"); + assert_eq!(to_string(100.0), "100.0"); + } } pub fn from_hex(s: &str) -> Option { diff --git a/crates/literal/src/lib.rs b/crates/literal/src/lib.rs index a863dd87738..6d520900142 100644 --- a/crates/literal/src/lib.rs +++ b/crates/literal/src/lib.rs @@ -2,7 +2,6 @@ extern crate alloc; -pub mod char; pub mod complex; pub mod escape; pub mod float; diff --git a/crates/sre_engine/Cargo.toml b/crates/sre_engine/Cargo.toml index 8400a34b567..03b3f609801 100644 --- a/crates/sre_engine/Cargo.toml +++ b/crates/sre_engine/Cargo.toml @@ -15,11 +15,11 @@ name = "benches" harness = false [dependencies] +rustpython-unicode = { workspace = true } rustpython-wtf8 = { workspace = true } num_enum = { workspace = true } bitflags = { workspace = true } optional = { workspace = true } -icu_properties = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/crates/sre_engine/src/engine.rs b/crates/sre_engine/src/engine.rs index a91ea3455fb..c2a6ac81975 100644 --- a/crates/sre_engine/src/engine.rs +++ b/crates/sre_engine/src/engine.rs @@ -110,6 +110,21 @@ impl Marks { self.marks_stack.pop(); } + fn stack_depth(&self) -> usize { + self.marks_stack.len() + } + + fn discard_to(&mut self, depth: usize) { + self.marks_stack.truncate(depth); + } + + fn restore_to(&mut self, depth: usize) { + let (marks, last_index) = self.marks_stack[depth].clone(); + self.marks = marks; + self.last_index = last_index; + self.marks_stack.truncate(depth); + } + fn clear(&mut self) { self.last_index = -1; self.marks.clear(); @@ -144,6 +159,7 @@ impl State { jump: Jump::OpCode, repeat_ctx_id: usize::MAX, count: -1, + marks_stack_base: usize::MAX, }; _match(req, self, ctx) } @@ -165,6 +181,7 @@ impl State { jump: Jump::OpCode, repeat_ctx_id: usize::MAX, count: -1, + marks_stack_base: usize::MAX, }; if ctx.peek_code(&req, 0) == SreOpcode::INFO as u32 { @@ -483,6 +500,7 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo } Jump::PossessiveRepeat2 => { if popped_result { + ctx.cursor = state.cursor; ctx.count += 1; ctx.jump = Jump::PossessiveRepeat1; continue 'context; @@ -495,6 +513,7 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo if ((ctx.count as usize) < max_count || max_count == MAXREPEAT) && ctx.cursor.position != state.cursor.position { + ctx.marks_stack_base = state.marks.stack_depth(); state.marks.push(); ctx.cursor = state.cursor; let mut next = ctx.next_offset(4, Jump::PossessiveRepeat4); @@ -507,12 +526,12 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo } Jump::PossessiveRepeat4 => { if popped_result { - state.marks.pop_discard(); + state.marks.discard_to(ctx.marks_stack_base); ctx.count += 1; ctx.jump = Jump::PossessiveRepeat3; continue 'context; } - state.marks.pop(); + state.marks.restore_to(ctx.marks_stack_base); state.cursor = ctx.cursor; ctx.skip_code_from(req, 1); ctx.skip_code(1); @@ -562,7 +581,11 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo ..ctx }; - for _ in group_start..group_end { + // Walk the group itself rather than counting to its + // width: `g_ctx` is already stepping over exactly + // the characters being compared, so its own cursor + // is the loop bound. + while g_ctx.cursor.position < group_end { #[allow(clippy::redundant_closure_call)] if ctx.at_end(req) || $f(ctx.peek_char::()) != $f(g_ctx.peek_char::()) @@ -979,12 +1002,12 @@ fn search_info_literal( return true; } + // `state.cursor` is `req.start + skip`, the position the + // tail match resumes from; advancing past the prefix + // instead would resume at `req.start + len` and only + // agree when the prefix ends at the skip boundary. let mut next_ctx = ctx; - if skip != 0 { - next_ctx.advance_char::(); - } else { - next_ctx.cursor = state.cursor; - } + next_ctx.cursor = state.cursor; if _match(req, state, next_ctx) { return true; @@ -1057,6 +1080,7 @@ struct MatchContext { jump: Jump, repeat_ctx_id: usize, count: isize, + marks_stack_base: usize, } impl MatchContext { @@ -1147,7 +1171,9 @@ impl MatchContext { mut word_checker: F, ) -> bool { if self.at_beginning() && self.at_end(req) { - return false; + // Python 3.14 changed `\B` to match an empty input. Keep the + // boundary predicate false there, but its negation true. + return true; } let that = !self.at_beginning() && word_checker(self.back_peek_char::()); let this = !self.at_end(req) && word_checker(self.peek_char::()); diff --git a/crates/sre_engine/src/string.rs b/crates/sre_engine/src/string.rs index 6c8b9a567b4..5cc1b04b9fc 100644 --- a/crates/sre_engine/src/string.rs +++ b/crates/sre_engine/src/string.rs @@ -1,6 +1,12 @@ -use icu_properties::props::{EnumeratedProperty, GeneralCategory, GeneralCategoryGroup}; use rustpython_wtf8::Wtf8; +/// A position in the subject, paired with the byte pointer it resolves to. +/// +/// `position` is a **character index**, never a byte offset. The engine does +/// arithmetic on it directly — it subtracts two positions to get a character +/// count, adds a repeat count to get a bound, and compares one against a +/// lookbehind width — so the unit is part of the [`StrDrive`] contract rather +/// than a detail each implementation may pick. #[derive(Debug, Clone, Copy)] pub struct StringCursor { pub(crate) ptr: *const u8, @@ -16,15 +22,43 @@ impl Default for StringCursor { } } +/// Random access over the subject being matched. +/// +/// An implementation chooses how a character is spelled in memory — one byte +/// for `&[u8]`, one code point for `&str` and `&Wtf8` — but **not** how +/// positions are counted. Every position this trait produces or consumes is a +/// character index: `count` is the subject's length in characters, and +/// `skip(n)` advances a cursor's `position` by exactly `n`. +/// +/// That is load-bearing, not incidental. The engine reads position arithmetic +/// as character arithmetic in several places — `_count` bounds a repeat with +/// `position + max_count` and reports the repeat's length as a difference of +/// positions, `ASSERT` tests `position < back` against a lookbehind width, and +/// `search_info` recovers a match start as `position - (len - 1)`. A drive +/// that stored byte offsets here would leave all of those type-correct and +/// silently wrong, and would index a lookbehind out of bounds. +/// +/// So a drive over a variable-width encoding pays for the mapping: `count` +/// and `create_cursor` have to resolve character indices, and cannot simply +/// hand back byte lengths and byte offsets. pub trait StrDrive: Copy { + /// The subject's length, in characters. fn count(&self) -> usize; + /// A cursor at character index `n`. fn create_cursor(&self, n: usize) -> StringCursor; + /// Move `cursor` to character index `n`, from wherever it is now. fn adjust_cursor(&self, cursor: &mut StringCursor, n: usize); + /// Consume one character, returning it; `position` grows by one. fn advance(cursor: &mut StringCursor) -> u32; + /// The character at `cursor`, without moving it. fn peek(cursor: &StringCursor) -> u32; + /// Skip `n` characters, so `position` grows by exactly `n`. fn skip(cursor: &mut StringCursor, n: usize); + /// Step back over one character, returning it; `position` shrinks by one. fn back_advance(cursor: &mut StringCursor) -> u32; + /// The character before `cursor`, without moving it. fn back_peek(cursor: &StringCursor) -> u32; + /// Step back `n` characters, so `position` shrinks by exactly `n`. fn back_skip(cursor: &mut StringCursor, n: usize); } @@ -333,92 +367,77 @@ const fn utf8_is_cont_byte(byte: u8) -> bool { /// Mask of the value bits of a continuation byte. const CONT_MASK: u8 = 0b0011_1111; -const fn is_py_ascii_whitespace(b: u8) -> bool { - matches!(b, b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' | b'\x0B') -} +// Character-class and case predicates for the SRE engine. +// +// Every predicate takes a raw `u32` code point (SRE decodes strings into `u32`s, +// including lone surrogates) and returns whether it belongs to the class. +// ASCII-mode predicates only ever consider byte values; Unicode-mode predicates +// consult the shared property tables in `rustpython_unicode::classify`. + +const UNDERSCORE: u32 = '_' as u32; #[inline] pub(crate) fn is_word(ch: u32) -> bool { - ch == '_' as u32 || u8::try_from(ch).is_ok_and(|x| x.is_ascii_alphanumeric()) + ch == UNDERSCORE || u8::try_from(ch).is_ok_and(|x| x.is_ascii_alphanumeric()) } + #[inline] pub(crate) fn is_space(ch: u32) -> bool { - u8::try_from(ch).is_ok_and(is_py_ascii_whitespace) + u8::try_from(ch).is_ok_and(rustpython_wtf8::is_py_ascii_whitespace) } + #[inline] pub(crate) fn is_digit(ch: u32) -> bool { u8::try_from(ch).is_ok_and(|x| x.is_ascii_digit()) } + #[inline] pub(crate) fn is_loc_alnum(ch: u32) -> bool { // FIXME: Ignore the locales u8::try_from(ch).is_ok_and(|x| x.is_ascii_alphanumeric()) } + #[inline] pub(crate) fn is_loc_word(ch: u32) -> bool { - ch == '_' as u32 || is_loc_alnum(ch) + ch == UNDERSCORE || is_loc_alnum(ch) } + #[inline] pub(crate) const fn is_linebreak(ch: u32) -> bool { ch == '\n' as u32 } + #[inline] #[must_use] pub fn lower_ascii(ch: u32) -> u32 { u8::try_from(ch).map_or(ch, |x| x.to_ascii_lowercase() as u32) } + #[inline] pub(crate) fn lower_locate(ch: u32) -> u32 { // FIXME: Ignore the locales lower_ascii(ch) } + #[inline] pub(crate) fn upper_locate(ch: u32) -> u32 { // FIXME: Ignore the locales u8::try_from(ch).map_or(ch, |x| x.to_ascii_uppercase() as u32) } + #[inline] pub(crate) fn is_uni_digit(ch: u32) -> bool { - // TODO: check with cpython - char::try_from(ch).is_ok_and(|x| x.is_ascii_digit()) + // SRE_UNI_IS_DIGIT matches Unicode decimal digits (Py_UNICODE_ISDECIMAL), + // not just ASCII 0-9. + char::try_from(ch).is_ok_and(rustpython_unicode::classify::is_decimal) } + #[inline] pub(crate) fn is_uni_space(ch: u32) -> bool { - // TODO: check with cpython - is_space(ch) - || matches!( - ch, - 0x0009 - | 0x000A - | 0x000B - | 0x000C - | 0x000D - | 0x001C - | 0x001D - | 0x001E - | 0x001F - | 0x0020 - | 0x0085 - | 0x00A0 - | 0x1680 - | 0x2000 - | 0x2001 - | 0x2002 - | 0x2003 - | 0x2004 - | 0x2005 - | 0x2006 - | 0x2007 - | 0x2008 - | 0x2009 - | 0x200A - | 0x2028 - | 0x2029 - | 0x202F - | 0x205F - | 0x3000 - ) + // SRE_UNI_IS_SPACE is Py_UNICODE_ISSPACE. + char::try_from(ch).is_ok_and(rustpython_unicode::classify::is_space) } + #[inline] pub(crate) const fn is_uni_linebreak(ch: u32) -> bool { matches!( @@ -426,28 +445,28 @@ pub(crate) const fn is_uni_linebreak(ch: u32) -> bool { 0x000A | 0x000B | 0x000C | 0x000D | 0x001C | 0x001D | 0x001E | 0x0085 | 0x2028 | 0x2029 ) } + #[inline] pub(crate) fn is_uni_alnum(ch: u32) -> bool { // TODO: check with cpython - char::try_from(ch).is_ok_and(|c| { - GeneralCategoryGroup::Letter - .union(GeneralCategoryGroup::Number) - .contains(GeneralCategory::for_char(c)) - }) + char::try_from(ch).is_ok_and(rustpython_unicode::classify::is_alnum) } + #[inline] pub(crate) fn is_uni_word(ch: u32) -> bool { - ch == '_' as u32 || is_uni_alnum(ch) + ch == UNDERSCORE || is_uni_alnum(ch) } + #[inline] #[must_use] pub fn lower_unicode(ch: u32) -> u32 { - // TODO: check with cpython - char::try_from(ch).map_or(ch, |x| x.to_lowercase().next().unwrap() as u32) + // SRE_UNI_LOWER is Py_UNICODE_TOLOWER, the simple one-to-one mapping. + char::try_from(ch).map_or(ch, |x| rustpython_unicode::case::simple_lowercase(x) as u32) } + #[inline] #[must_use] pub fn upper_unicode(ch: u32) -> u32 { - // TODO: check with cpython - char::try_from(ch).map_or(ch, |x| x.to_uppercase().next().unwrap() as u32) + // SRE_UNI_UPPER is Py_UNICODE_TOUPPER, the simple one-to-one mapping. + char::try_from(ch).map_or(ch, |x| rustpython_unicode::case::simple_uppercase(x) as u32) } diff --git a/crates/sre_engine/tests/tests.rs b/crates/sre_engine/tests/tests.rs index f1edb64cdbf..0a9f45c374e 100644 --- a/crates/sre_engine/tests/tests.rs +++ b/crates/sre_engine/tests/tests.rs @@ -2,6 +2,7 @@ #[cfg(test)] mod tests { use rustpython_sre_engine::{Request, State, StrDrive}; + use rustpython_wtf8::Wtf8Buf; struct Pattern { #[expect(unused, reason = "Needed for automated script")] @@ -44,7 +45,7 @@ mod tests { #[rustfmt::skip] let big_b = Pattern { pattern: "\\B", code: &[14, 4, 0, 0, 0, 6, 11, 1] }; // END GENERATED let (req, mut state) = big_b.state(""); - assert!(!state.search(req)); + assert!(state.search(req)); } #[test] @@ -169,6 +170,40 @@ mod tests { assert!(!state.py_match(&req)); } + #[test] + fn possessive_repeat_keeps_last_capture() { + use optional::Optioned; + + let single_code = &[17, 0, 24, 6, 0, 1, 16, 101, 1, 17, 1, 1]; + let req = Request::new("eeea", 3, usize::MAX, single_code, false); + let mut single_state = State::default(); + assert!(single_state.py_match(&req)); + assert_eq!( + single_state.marks.get(0), + (Optioned::some(3), Optioned::some(3)) + ); + + // (e?){2,4}+a: the fourth successful iteration is empty, so group 1 + // must retain its final empty span rather than the previous "e". + #[rustfmt::skip] let optional = Pattern { + pattern: "(e?){2,4}+a", + code: &[14, 4, 0, 1, 5, 28, 14, 2, 4, 17, 0, 24, 6, 0, 1, 16, 101, 1, 17, 1, 1, 16, 97, 1], + }; + let (req, mut state) = optional.state("eeea"); + assert!(state.py_match(&req)); + assert_eq!(state.marks.get(0), (Optioned::some(3), Optioned::some(3))); + + // ((x)|y|z){3}+: group 1 is the final "z"; group 2 retains "x". + #[rustfmt::skip] let alternation = Pattern { + pattern: "((x)|y|z){3}+", + code: &[14, 4, 0, 3, 3, 28, 28, 3, 3, 17, 0, 7, 9, 17, 2, 16, 120, 17, 3, 15, 12, 5, 16, 121, 15, 7, 5, 16, 122, 15, 2, 0, 17, 1, 1, 1], + }; + let (req, mut state) = alternation.state("xyz"); + assert!(state.py_match(&req)); + assert_eq!(state.marks.get(0), (Optioned::some(2), Optioned::some(3))); + assert_eq!(state.marks.get(1), (Optioned::some(0), Optioned::some(1))); + } + #[test] fn bug_20998() { // pattern p = re.compile('[a-c]+', re.I) @@ -181,6 +216,23 @@ mod tests { assert_eq!(state.cursor.position, 3); } + #[test] + fn ascii_ignore_keeps_nonascii_range_literal() { + // pattern p = re.compile(r'[\u0430-\u045f]', re.I | re.A) + // + // ASCII-only case folding must not discard an exact non-ASCII range: + // U+0450 lies in the compiled U+0430..U+045F interval. + #[rustfmt::skip] let p = Pattern { + pattern: "[\\u0430-\\u045f]", + code: &[14, 8, 4, 1, 1, 22, 1072, 1119, 0, 13, 5, 22, 1072, 1119, 0, 1], + }; + let (req, mut state) = p.state("\u{0450}"); + assert!(state.py_match(&req)); + let subject = Wtf8Buf::from("\u{0450}"); + let (req, mut state) = p.state(subject.as_ref()); + assert!(state.py_match(&req)); + } + #[test] fn bigcharset() { // pattern p = re.compile('[a-z]*', re.I) @@ -200,4 +252,19 @@ mod tests { #[rustfmt::skip] let p = Pattern { pattern: "\u{e0}+", code: &[14, 4, 0, 1, 4294967295, 24, 6, 1, 4294967295, 16, 224, 1, 1] }; // END GENERATED } + + #[test] + fn search_literal_prefix_longer_than_skip() { + // The INFO block carries prefix_len=4 and prefix_skip=2, so the tail + // match has to resume at the skip boundary rather than past the whole + // prefix. + // pattern p = re.compile('ab(cd)') + // START GENERATED by generate_tests.py + #[rustfmt::skip] let p = Pattern { pattern: "ab(cd)", code: &[14, 14, 1, 4, 4, 4, 2, 97, 98, 99, 100, 0, 0, 0, 0, 16, 97, 16, 98, 17, 0, 16, 99, 16, 100, 17, 1, 1] }; + // END GENERATED + let (req, mut state) = p.state("xabcdcd"); + assert!(state.search(req)); + assert_eq!(state.start, 1); + assert_eq!(state.cursor.position, 5); + } } diff --git a/crates/stdlib/Cargo.toml b/crates/stdlib/Cargo.toml index a24811c4aea..98a6677de16 100644 --- a/crates/stdlib/Cargo.toml +++ b/crates/stdlib/Cargo.toml @@ -32,6 +32,7 @@ rustpython-derive = { workspace = true } rustpython-vm = { workspace = true, default-features = false, features = ["compiler"]} rustpython-common = { workspace = true } rustpython-host_env = { workspace = true } +rustpython-unicode = { workspace = true } ruff_python_parser = { workspace = true } ruff_python_ast = { workspace = true } @@ -52,6 +53,7 @@ num_enum = { workspace = true } parking_lot = { workspace = true } phf = { workspace = true, default-features = true, features = ["macros"] } rapidhash = { workspace = true } +scopeguard = { workspace = true } memchr = { workspace = true } base64 = { workspace = true } @@ -76,12 +78,6 @@ hmac = { workspace = true } pbkdf2 = { workspace = true, features = ["hmac"] } constant_time_eq = { workspace = true } -## unicode stuff -unicode_names2 = { workspace = true } -# update version all at the same time -icu_properties = { workspace = true } -icu_normalizer = { workspace = true } - # compression adler32 = { workspace = true } crc32fast = { workspace = true } @@ -90,21 +86,17 @@ libz-rs-sys = { workspace = true } bzip2 = { workspace = true } # tkinter +jiff = { workspace = true } tk-sys = { workspace = true, optional = true } tcl-sys = { workspace = true, optional = true } widestring = { workspace = true, optional = true } -chrono.workspace = true # uuid [target.'cfg(not(any(target_os = "ios", target_os = "android", target_os = "windows", target_arch = "wasm32", target_os = "redox")))'.dependencies] -mac_address = { workspace = true } uuid = { workspace = true, features = ["v1"] } # mmap + socket dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gethostname = { workspace = true } -socket2 = { workspace = true, features = ["all"] } -dns-lookup = { workspace = true } # OpenSSL dependencies (optional, for ssl-openssl feature) openssl = { workspace = true, optional = true } @@ -134,16 +126,9 @@ xz-sys = { workspace = true } paste = { workspace = true } widestring = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -system-configuration = { workspace = true } - [dev-dependencies] insta = { workspace = true } rustpython-pylib = { workspace = true, features = [ "freeze-stdlib" ] } -[build-dependencies] -icu_normalizer = { workspace = true } -icu_properties = { workspace = true } - [lints] workspace = true diff --git a/crates/stdlib/build.rs b/crates/stdlib/build.rs index 4cf7b21d4b7..95c34c4fb3c 100644 --- a/crates/stdlib/build.rs +++ b/crates/stdlib/build.rs @@ -1,606 +1,4 @@ -#![allow( - clippy::disallowed_methods, - reason = "build scripts cannot use rustpython-host_env" -)] - -// spell-checker:ignore decomp DECOMP ossl osslconf - -extern crate alloc; - -use core::num::NonZeroUsize; - -use alloc::collections::{BTreeMap, BTreeSet}; - -use std::{ - env, - fs::{self, File}, - io::{self, BufRead, BufReader, BufWriter, Write}, - path::{Path, PathBuf}, - thread, -}; - -use icu_properties::props::{EnumeratedProperty, GeneralCategory, NumericType}; - -fn generate_unicode_3_2() { - let path = PathBuf::from(env::var("OUT_DIR").unwrap()) - .join("generated") - .join("unicode_3_2.rs"); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - let mut writer = BufWriter::new(File::create(&path).unwrap()); - - let base = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("unicode") - .join("ucd32"); - - write_derived( - &base, - "DerivedGeneralCategory-3.2.0.txt", - "GENERAL_CATEGORY", - "(u32, u32, GeneralCategory)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - let id = parse_general(id); - if id != GeneralCategory::Unassigned { - Some((start, end, id)) - } else { - None - } - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _, _)| *start); - write!(writer, "[").unwrap(); - for (start, end, id) in values { - write!(writer, "({start}, {end}, GeneralCategory::{id:?}),").unwrap(); - } - write!(writer, "];").unwrap(); - }, - ); - - write_derived( - &base, - "DerivedEastAsianWidth-3.2.0.txt", - "EAST_ASIAN_WIDTH", - "(u32, u32, EastAsianWidth)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - let id = parse_eaw(id); - if id != "EastAsianWidth::Neutral" { - Some((start, end, id)) - } else { - None - } - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _, _)| *start); - write!(writer, "[").unwrap(); - for (start, end, id) in values { - write!(writer, "({start}, {end}, {id}),").unwrap(); - } - write!(writer, "];").unwrap(); - }, - ); - - write_derived( - &base, - "DerivedBidiClass-3.2.0.txt", - "BIDI_CLASS", - "(u32, u32, BidiClass)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - let id = parse_bidi(id); - if id != "BidiClass::LeftToRight" { - Some((start, end, id)) - } else { - None - } - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _, _)| *start); - write!(writer, "[").unwrap(); - for (start, end, id) in values { - write!(writer, "({start}, {end}, {id}),").unwrap(); - } - write!(writer, "];").unwrap(); - }, - ); - - write_derived( - &base, - "DerivedBinaryProperties-3.2.0.txt", - "BIDI_MIRRORED", - "(u32, u32)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - assert_eq!( - "Bidi_Mirrored", - id.trim(), - "DerivedBinaryProperties-3.2.0 only has Bidi_Mirrored" - ); - Some((start, end)) - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _)| *start); - writeln!(writer, "{values:?};").unwrap(); - }, - ); - - write_derived( - &base, - "DerivedCombiningClass-3.2.0.txt", - "COMBINING_CLASS", - "(u32, u32, CanonicalCombiningClass)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - let id: u8 = id.parse().unwrap(); - if id == 0 { - return None; - } - Some((start, end, id)) - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _, _)| *start); - write!(writer, "[").unwrap(); - for (start, end, id) in values { - write!( - writer, - "({start}, {end}, CanonicalCombiningClass::from_icu4c_value({id}))," - ) - .unwrap(); - } - writeln!(writer, "];").unwrap(); - }, - ); -} - -fn generate_numeric_type() { - let path = PathBuf::from(env::var("OUT_DIR").unwrap()) - .join("generated") - .join("unicode_num_type.rs"); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - let mut writer = BufWriter::new(File::create(&path).unwrap()); - - let base = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("unicode") - .join("ucd32"); - - write_derived( - &base, - "DerivedNumericType-3.2.0.txt", - "NUMERIC_TYPE_DIFF", - "(u32, u32, NumericType)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, id, _| { - let id = parse_numeric_type_str(id); - let differs = (start..=end).any(|c| match char::from_u32(c) { - Some(c) => { - let modern = parse_numeric_type_val(NumericType::for_char(c)); - modern != id - } - None => true, - }); - - if differs { - Some((start, end, id)) - } else { - None - } - }, - |writer, mut values| { - values.sort_unstable_by_key(|(start, _, _)| *start); - write!(writer, "[").unwrap(); - for (start, end, id) in values { - write!(writer, "({start}, {end}, {id}),").unwrap(); - } - writeln!(writer, "];").unwrap(); - }, - ); -} - -fn generate_numeric_value() { - let path = PathBuf::from(env::var("OUT_DIR").unwrap()) - .join("generated") - .join("unicode_numeric_value.rs"); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - let mut writer = BufWriter::new(File::create(&path).unwrap()); - - // Ideally, this would store the diffs between the two tables. However, we need 3.2.0 - // membership as well as different chars. The final tables are both smaller than storing the - // full 3.2.0 value table. - let ucd32 = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("unicode") - .join("ucd32"); - let mut ucd32_diffs = BTreeMap::new(); - let mut ucd32_member = BTreeSet::new(); - let numeric_32 = - BufReader::new(File::open(ucd32.join("DerivedNumericValues-3.2.0.txt")).unwrap()); - parse_unicode_3_2( - numeric_32, - NonZeroUsize::new(1).unwrap(), - &mut io::empty(), - |start, end, value, _| { - let value: f64 = value - .parse() - .expect("Unicode data contains valid properties"); - ucd32_diffs.insert((start, end), value); - ucd32_member.insert((start, end)); - Option::<()>::None - }, - |_writer, _values| {}, - ); - - let ucd_latest = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("unicode") - .join("latest"); - - write_derived( - &ucd_latest, - "DerivedNumericValues.txt", - "NUMERIC_VALUES", - "(u32, u32, f64)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, end, value, _| { - let value: f64 = value - .parse() - .expect("Unicode data contains valid properties"); - - if ucd32_diffs - .get(&(start, end)) - .is_some_and(|old_v| *old_v == value) - { - ucd32_diffs.remove(&(start, end)); - } - - Some((start, end, value)) - }, - |writer, mut values| { - values.sort_unstable_by_key(|(ch, _, _)| *ch); - writeln!(writer, "{values:?};").unwrap(); - }, - ); - - // TODO: More flexible parser - writeln!( - writer, - "static NUMERIC_VALUES_DIFF: &[(u32, u32, f64)] = &[" - ) - .unwrap(); - for ((start, end), value) in ucd32_diffs { - write!(writer, "({start}, {end}, {value:?}),").unwrap(); - } - writeln!(writer, "];").unwrap(); - - // Compress membership table - let mut iter = ucd32_member.iter(); - let &(mut start_prev, mut end_prev) = iter.next().unwrap(); - let mut membership = Vec::new(); - - for &(start, end) in iter { - if start <= end_prev + 1 { - end_prev = end_prev.max(end); - } else { - membership.push((start_prev, end_prev)); - start_prev = start; - end_prev = end; - } - } - membership.push((start_prev, end_prev)); - membership.sort_unstable_by_key(|&(start, _)| start); - - writeln!(writer, "static NUMERIC_VAL_EXISTS_32: &[(u32, u32)] = &").unwrap(); - write!(writer, "{membership:?};").unwrap(); -} - -fn generate_unicode_latest() { - let path = PathBuf::from(env::var("OUT_DIR").unwrap()) - .join("generated") - .join("unicode_latest.rs"); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - let mut writer = BufWriter::new(File::create(&path).unwrap()); - - let base = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("unicode") - .join("latest"); - - // NOTE: - // This ONLY parses compatibility decomposition because Python exposes the tags. The tags are - // the "", "", et cetera bits before the decomposition. Thus, we can save space - // by using icu4x's CanonicalDecomposer for non-compatibility decomposition. - let mut decomp_ranges = Vec::new(); - write_derived( - &base, - "UnicodeData.txt", - "DECOMP_COMPAT", - "(u32, DecompositionType, usize)", - NonZeroUsize::new(5).unwrap(), - &mut writer, - |start, _end, value, _| { - // We're building a sparse array. Most characters don't decompose, so we don't - // need to literally store a row for each char. - if value.is_empty() { - return None; - } - - let (dtype, decomp) = value.split_once('>').map(|(dtype, decomp)| { - let dtype = dtype.strip_prefix('<').unwrap_or_else(|| { - panic!("Compatibility decomp; expected \n\tgot: {value}") - }); - ( - parse_decomp_type(dtype), - decomp - .split_whitespace() - .map(|s| u32::from_str_radix(s, 16).unwrap()), - ) - })?; - - decomp_ranges.extend(decomp); - let end = decomp_ranges.len(); - - Some((start, dtype, end)) - }, - |writer, values| { - // UnicodeData.txt should already be sorted - write!(writer, "[").unwrap(); - for (start, dtype, end) in values { - write!(writer, "({start}, DecompositionType::{dtype:?}, {end}),").unwrap(); - } - writeln!(writer, "];").unwrap(); - }, - ); - - writeln!(writer, "static DECOMP_RANGE: &[u32] = &{decomp_ranges:?};").unwrap(); - - // Normalization corrections is super small - only a handful chars at the time of writing. - write_derived( - &base, - "NormalizationCorrections.txt", - "DECOMP_UPDATES", - "(u32, u32)", - NonZeroUsize::new(1).unwrap(), - &mut writer, - |start, _end, value, line| { - let original = u32::from_str_radix(value.trim(), 16).unwrap_or_else(|e| { - panic!("field 2 of decomp corrections should be a char in hex: {value} {e}") - }); - let version = line - .rsplit(';') - .next() - .unwrap_or_else(|| { - panic!("field 4 of decomp corrections should be a UCD version: {line}") - }) - .split_once('#') - .unwrap() - .0 - .trim(); - - // `version` = when the char was updated. Therefore, we use the incorrect chars past - // 3.2.0 but skip the chars fixed in 3.2.0 because they'll already be right. - if version != "3.2.0" { - Some((start, original)) - } else { - None - } - }, - |writer, mut values| { - values.sort_unstable_by_key(|(c, _)| *c); - write!(writer, "{values:?};").unwrap(); - }, - ); -} - -#[expect(clippy::too_many_arguments)] -fn write_derived( - base: &Path, - file_name: &str, - static_name: &str, - array_type: &str, - field: NonZeroUsize, - writer: &mut W, - parse: P, - write_vec: FW, -) where - W: Write, - P: FnMut(u32, u32, &str, &str) -> Option, - FW: FnMut(&mut W, Vec), -{ - let path = base.join(file_name); - let reader = BufReader::new(File::open(path).unwrap()); - writeln!(writer, "static {static_name}: &[{array_type}] = &").unwrap(); - parse_unicode_3_2(reader, field, writer, parse, write_vec); -} - -/// Parse Unicode 3.2.0 property files. -fn parse_unicode_3_2( - reader: impl BufRead, - field: NonZeroUsize, - writer: &mut W, - mut parse: P, - mut write_vec: FW, -) where - W: Write, - P: FnMut(u32, u32, &str, &str) -> Option, - FW: FnMut(&mut W, Vec), -{ - let mut parsed = Vec::new(); - - for line in reader.lines().map(Result::unwrap) { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - let mut fields = line.split(';'); - let range = fields.next().expect("Unicode data is missing a char range"); - let id = fields - .nth(field.get().saturating_sub(1)) - .expect("Unicode data is missing a property"); - let (start, end) = match range.split_once("..") { - Some((left, right)) => { - let start = u32::from_str_radix(left.trim(), 16).unwrap(); - let end = u32::from_str_radix(right.trim(), 16).unwrap(); - (start, end) - } - None => { - let start = u32::from_str_radix(range.trim(), 16).unwrap(); - (start, start) - } - }; - - let id = id.split_once('#').map_or(id, |(left, _)| left).trim(); - if let Some(val) = parse(start, end, id, line) { - parsed.push(val); - } - } - write_vec(writer, parsed); -} - -fn parse_general(id: &str) -> GeneralCategory { - match id.trim() { - "Cn" => GeneralCategory::Unassigned, - "Lu" => GeneralCategory::UppercaseLetter, - "Ll" => GeneralCategory::LowercaseLetter, - "Lt" => GeneralCategory::TitlecaseLetter, - "Lm" => GeneralCategory::ModifierLetter, - "Lo" => GeneralCategory::OtherLetter, - "Mn" => GeneralCategory::NonspacingMark, - "Mc" => GeneralCategory::SpacingMark, - "Me" => GeneralCategory::EnclosingMark, - "Nd" => GeneralCategory::DecimalNumber, - "Nl" => GeneralCategory::LetterNumber, - "No" => GeneralCategory::OtherNumber, - "Zs" => GeneralCategory::SpaceSeparator, - "Zl" => GeneralCategory::LineSeparator, - "Zp" => GeneralCategory::ParagraphSeparator, - "Cc" => GeneralCategory::Control, - "Cf" => GeneralCategory::Format, - "Co" => GeneralCategory::PrivateUse, - "Cs" => GeneralCategory::Surrogate, - "Pd" => GeneralCategory::DashPunctuation, - "Ps" => GeneralCategory::OpenPunctuation, - "Pe" => GeneralCategory::ClosePunctuation, - "Pc" => GeneralCategory::ConnectorPunctuation, - "Pi" => GeneralCategory::InitialPunctuation, - "Pf" => GeneralCategory::FinalPunctuation, - "Po" => GeneralCategory::OtherPunctuation, - "Sm" => GeneralCategory::MathSymbol, - "Sc" => GeneralCategory::CurrencySymbol, - "Sk" => GeneralCategory::ModifierSymbol, - "So" => GeneralCategory::OtherSymbol, - invalid => unreachable!("Unicode data contains valid properties: {invalid}"), - } -} - -fn parse_eaw(id: &str) -> &'static str { - match id.trim() { - "N" => "EastAsianWidth::Neutral", - "A" => "EastAsianWidth::Ambiguous", - "H" => "EastAsianWidth::Halfwidth", - "F" => "EastAsianWidth::Fullwidth", - "Na" => "EastAsianWidth::Narrow", - "W" => "EastAsianWidth::Wide", - invalid => unreachable!("Unicode data contains valid properties: {invalid}"), - } -} - -fn parse_bidi(id: &str) -> &'static str { - match id.trim() { - "L" => "BidiClass::LeftToRight", - "R" => "BidiClass::RightToLeft", - "EN" => "BidiClass::EuropeanNumber", - "ES" => "BidiClass::EuropeanSeparator", - "ET" => "BidiClass::EuropeanTerminator", - "AN" => "BidiClass::ArabicNumber", - "CS" => "BidiClass::CommonSeparator", - "B" => "BidiClass::ParagraphSeparator", - "S" => "BidiClass::SegmentSeparator", - "WS" => "BidiClass::WhiteSpace", - "ON" => "BidiClass::OtherNeutral", - "LRE" => "BidiClass::LeftToRightEmbedding", - "LRO" => "BidiClass::LeftToRightOverride", - "AL" => "BidiClass::ArabicLetter", - "RLE" => "BidiClass::RightToLeftEmbedding", - "RLO" => "BidiClass::RightToLeftOverride", - "PDF" => "BidiClass::PopDirectionalFormat", - "NSM" => "BidiClass::NonspacingMark", - "BN" => "BidiClass::BoundaryNeutral", - "FSI" => "BidiClass::FirstStrongIsolate", - "LRI" => "BidiClass::LeftToRightIsolate", - "RLI" => "BidiClass::RightToLeftIsolate", - "PDI" => "BidiClass::PopDirectionalIsolate", - invalid => unreachable!("Unicode data contains valid properties: {invalid}"), - } -} - -fn parse_numeric_type_val(val: NumericType) -> &'static str { - match val { - NumericType::None => "none", - NumericType::Decimal => "decimal", - NumericType::Digit => "digit", - NumericType::Numeric => "numeric", - _ => unreachable!("Unicode data contains valid properties"), - } -} - -fn parse_numeric_type_str(id: &str) -> &'static str { - match id { - "none" => "NumericType::None", - "decimal" => "NumericType::Decimal", - "digit" => "NumericType::Digit", - "numeric" => "NumericType::Numeric", - invalid => unreachable!("Unicode data contains valid properties: {invalid}"), - } -} - -#[derive(Debug, Default)] -enum DecompositionType { - #[default] - Canonical, - Compat, - Circle, - Final, - Font, - Fraction, - Initial, - Isolated, - Medial, - Narrow, - Nobreak, - Small, - Square, - Sub, - Super, - Vertical, - Wide, -} - -fn parse_decomp_type(id: &str) -> DecompositionType { - match id { - "canonical" => DecompositionType::Canonical, - "compat" => DecompositionType::Compat, - "circle" => DecompositionType::Circle, - "final" => DecompositionType::Final, - "font" => DecompositionType::Font, - "fraction" => DecompositionType::Fraction, - "initial" => DecompositionType::Initial, - "isolated" => DecompositionType::Isolated, - "medial" => DecompositionType::Medial, - "narrow" => DecompositionType::Narrow, - "noBreak" => DecompositionType::Nobreak, - "small" => DecompositionType::Small, - "square" => DecompositionType::Square, - "sub" => DecompositionType::Sub, - "super" => DecompositionType::Super, - "vertical" => DecompositionType::Vertical, - "wide" => DecompositionType::Wide, - invalid => unreachable!("Unicode data contains valid properties: {invalid}"), - } -} +// spell-checker:ignore ossl osslconf fn main() { println!(r#"cargo::rustc-check-cfg=cfg(osslconf, values("OPENSSL_NO_COMP"))"#); @@ -655,16 +53,4 @@ fn main() { println!("cargo::rustc-cfg=openssl_vendored") } } - - println!("cargo:rerun-if-changed=unicode/ucd32"); - println!("cargo:rerun-if-changed=unicode/latest"); - - let t_32 = thread::spawn(generate_unicode_3_2); - let t_numeric_type = thread::spawn(generate_numeric_type); - let t_numeric_value = thread::spawn(generate_numeric_value); - let t_latest = thread::spawn(generate_unicode_latest); - t_32.join().unwrap(); - t_numeric_type.join().unwrap(); - t_numeric_value.join().unwrap(); - t_latest.join().unwrap(); } diff --git a/crates/stdlib/src/_asyncio.rs b/crates/stdlib/src/_asyncio.rs index 48be4115ed6..c3f28590e6a 100644 --- a/crates/stdlib/src/_asyncio.rs +++ b/crates/stdlib/src/_asyncio.rs @@ -12,8 +12,8 @@ pub(crate) mod _asyncio { vm::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{ - PyBaseException, PyBaseExceptionRef, PyDict, PyDictRef, PyGenericAlias, PyList, - PyListRef, PyModule, PySet, PyTuple, PyType, PyTypeRef, + PyBaseException, PyBaseExceptionRef, PyDict, PyGenericAlias, PyList, PyListRef, + PyModule, PySet, PyTuple, PyType, PyTypeRef, }, extend_module, function::{FuncArgs, KwArgs, OptionalArg, OptionalOption, PySetterValue}, @@ -101,7 +101,7 @@ pub(crate) mod _asyncio { } impl FutureState { - fn as_str(&self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::Pending => "PENDING", Self::Cancelled => "CANCELLED", @@ -566,7 +566,7 @@ pub(crate) mod _asyncio { let args = if let Some(ctx) = context { FuncArgs::new( vec![callback, future_arg], - KwArgs::new(core::iter::once(("context".to_owned(), ctx)).collect()), + KwArgs::new(core::iter::once((Wtf8Buf::from("context"), ctx)).collect()), ) } else { FuncArgs::new(vec![callback, future_arg], KwArgs::default()) @@ -724,47 +724,56 @@ pub(crate) mod _asyncio { /// Add waiter to fut_awaited_by with single-object optimization fn awaited_by_add(&self, waiter: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let mut awaited_by = self.fut_awaited_by.write(); - if awaited_by.is_none() { - // First waiter - store directly - *awaited_by = Some(waiter); - return Ok(()); - } + // Storing a waiter in the set runs its __hash__ and __eq__, which can + // come back to this future, so the field is locked only while it is + // read or written. + let existing = { + let mut awaited_by = self.fut_awaited_by.write(); + match awaited_by.as_ref() { + // First waiter - store directly + None => { + *awaited_by = Some(waiter); + return Ok(()); + } + Some(existing) => existing.clone(), + } + }; if self.fut_awaited_by_is_set.load(Ordering::Relaxed) { // Already a Set - add to it - let set = awaited_by.as_ref().unwrap(); - vm.call_method(set, "add", (waiter,))?; - } else { - // Single object - convert to Set - let existing = awaited_by.take().unwrap(); - let new_set = PySet::default().into_ref(&vm.ctx); - new_set.add(existing, vm)?; - new_set.add(waiter, vm)?; - *awaited_by = Some(new_set.into()); - self.fut_awaited_by_is_set.store(true, Ordering::Relaxed); + return vm.call_method(&existing, "add", (waiter,)).map(drop); } + + // Single object - convert to Set + let new_set = PySet::default().into_ref(&vm.ctx); + new_set.add(existing, vm)?; + new_set.add(waiter, vm)?; + *self.fut_awaited_by.write() = Some(new_set.into()); + self.fut_awaited_by_is_set.store(true, Ordering::Relaxed); Ok(()) } /// Discard waiter from fut_awaited_by with single-object optimization fn awaited_by_discard(&self, waiter: &PyObject, vm: &VirtualMachine) -> PyResult<()> { - let mut awaited_by = self.fut_awaited_by.write(); - if awaited_by.is_none() { - return Ok(()); - } - - let obj = awaited_by.as_ref().unwrap(); - if !self.fut_awaited_by_is_set.load(Ordering::Relaxed) { - // Single object - check if it matches - if obj.is(waiter) { - *awaited_by = None; + // As in awaited_by_add, discarding from the set runs Python. + let set = { + let mut awaited_by = self.fut_awaited_by.write(); + let Some(obj) = awaited_by.as_ref() else { + return Ok(()); + }; + if !self.fut_awaited_by_is_set.load(Ordering::Relaxed) { + // Single object - check if it matches + if obj.is(waiter) { + *awaited_by = None; + } + return Ok(()); } - } else { - // It's a Set - use discard - vm.call_method(obj, "discard", (waiter.to_owned(),))?; - } - Ok(()) + obj.clone() + }; + + // It's a Set - use discard + vm.call_method(&set, "discard", (waiter.to_owned(),)) + .map(drop) } #[pymethod] @@ -779,7 +788,7 @@ pub(crate) mod _asyncio { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -1036,7 +1045,7 @@ pub(crate) mod _asyncio { ))); } - let exc = if exc_type.fast_isinstance(vm.ctx.types.type_type) { + let exc: PyBaseExceptionRef = if exc_type.fast_isinstance(vm.ctx.types.type_type) { // exc_type is a class let exc_class: PyTypeRef = exc_type.clone().downcast().unwrap(); // Must be a subclass of BaseException @@ -1047,12 +1056,23 @@ pub(crate) mod _asyncio { } let val = exc_val.unwrap_or_none(vm); - if vm.is_none(&val) { + let exc = if vm.is_none(&val) { exc_type.call((), vm)? } else if val.fast_isinstance(&exc_class) { val } else { exc_type.call((val,), vm)? + }; + match exc.downcast() { + Ok(exc) => exc, + Err(obj) => { + let exc_class_repr = exc_class.as_object().repr(vm)?; + vm.new_type_error(format!( + "calling {} should have returned an instance of BaseException, not {}", + exc_class_repr.as_wtf8(), + obj.class() + )) + } } } else if exc_type.fast_isinstance(vm.ctx.exceptions.base_exception_type) { // exc_type is an exception instance @@ -1063,7 +1083,7 @@ pub(crate) mod _asyncio { vm.new_type_error("instance exception may not have a separate value") ); } - exc_type + exc_type.downcast().unwrap() } else { // exc_type is neither a class nor an exception instance return Err(vm.new_type_error(format!( @@ -1075,10 +1095,11 @@ pub(crate) mod _asyncio { if let OptionalArg::Present(tb) = exc_tb && !vm.is_none(&tb) { - exc.set_attr(vm.ctx.intern_str("__traceback__"), tb, vm)?; + exc.as_object() + .set_attr(vm.ctx.intern_str("__traceback__"), tb, vm)?; } - Err(exc.downcast().unwrap()) + Err(exc) } #[pymethod] @@ -1498,7 +1519,7 @@ pub(crate) mod _asyncio { let args = if let Some(ctx) = context { FuncArgs::new( vec![callback, task_arg], - KwArgs::new(core::iter::once(("context".to_owned(), ctx)).collect()), + KwArgs::new(core::iter::once((Wtf8Buf::from("context"), ctx)).collect()), ) } else { FuncArgs::new(vec![callback, task_arg], KwArgs::default()) @@ -1527,7 +1548,7 @@ pub(crate) mod _asyncio { let cancel_args = if let Some(ref m) = msg_value { FuncArgs::new( vec![], - KwArgs::new(core::iter::once(("msg".to_owned(), m.clone())).collect()), + KwArgs::new(core::iter::once((Wtf8Buf::from("msg"), m.clone())).collect()), ) } else { FuncArgs::new(vec![], KwArgs::default()) @@ -1840,7 +1861,7 @@ pub(crate) mod _asyncio { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -2213,7 +2234,7 @@ pub(crate) mod _asyncio { let cancel_args = if let Some(ref m) = cancel_msg { FuncArgs::new( vec![], - KwArgs::new(core::iter::once(("msg".to_owned(), m.clone())).collect()), + KwArgs::new(core::iter::once((Wtf8Buf::from("msg"), m.clone())).collect()), ) } else { FuncArgs::new(vec![], KwArgs::default()) @@ -2405,7 +2426,9 @@ pub(crate) mod _asyncio { // Slow path: look up in the module-level dict for cross-thread queries let current_tasks = get_current_tasks_dict(vm)?; - let dict: PyDictRef = current_tasks.downcast().unwrap(); + let Ok(dict) = current_tasks.downcast::() else { + return Ok(vm.ctx.none()); + }; match dict.get_item(&*loop_obj, vm) { Ok(task) => Ok(task), @@ -2485,15 +2508,17 @@ pub(crate) mod _asyncio { #[pyfunction] fn _enter_task(loop_: PyObjectRef, task: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { // Per-thread check, matching CPython's ts->asyncio_running_task - { - let running_task = vm.asyncio_running_task.borrow(); - if running_task.is_some() { - return Err(vm.new_runtime_error(format!( - "Cannot enter into task {:?} while another task {:?} is being executed.", - task, - running_task.as_ref().unwrap() - ))); - } + let running_task = vm.asyncio_running_task.borrow().clone(); + if let Some(running_task) = running_task { + let task_repr = task.repr(vm)?; + let running_task_repr = running_task.repr(vm)?; + return Err(vm.new_runtime_error(wtf8_concat!( + "Cannot enter into task ", + task_repr.as_wtf8(), + " while another task ", + running_task_repr.as_wtf8(), + " is being executed." + ))); } *vm.asyncio_running_task.borrow_mut() = Some(task.clone()); @@ -2729,16 +2754,20 @@ pub(crate) mod _asyncio { } } + fn get_invalid_state_error_type(vm: &VirtualMachine) -> PyResult { + let module = vm.import("asyncio.exceptions", 0)?; + let exc_type = vm + .get_attribute_opt(module, vm.ctx.intern_str("InvalidStateError"))? + .ok_or_else(|| vm.new_attribute_error("InvalidStateError not found"))?; + exc_type + .downcast() + .map_err(|_| vm.new_type_error("InvalidStateError is not a type")) + } + fn new_invalid_state_error(vm: &VirtualMachine, msg: &str) -> PyBaseExceptionRef { - match vm.import("asyncio.exceptions", 0) { - Ok(module) => { - match vm.get_attribute_opt(module, vm.ctx.intern_str("InvalidStateError")) { - Ok(Some(exc_type)) => match exc_type.call((msg,), vm) { - Ok(exc) => exc.downcast().unwrap(), - Err(_) => vm.new_runtime_error(msg.to_string()), - }, - _ => vm.new_runtime_error(msg.to_string()), - } + match get_invalid_state_error_type(vm) { + Ok(invalid_state_error) => { + vm.new_exception_msg(invalid_state_error, msg.to_string().into()) } Err(_) => vm.new_runtime_error(msg.to_string()), } diff --git a/crates/stdlib/src/_opcode.rs b/crates/stdlib/src/_opcode.rs index 2b2a70b5572..e6fc3276c31 100644 --- a/crates/stdlib/src/_opcode.rs +++ b/crates/stdlib/src/_opcode.rs @@ -205,7 +205,7 @@ mod tests { let scope = vm.new_scope_with_builtins(); let code_obj = vm .compile(source.trim(), Mode::Exec, FNAME) - .map_err(|err| vm.new_syntax_error(&err, Some(source))) + .map_err(|err| err.into_pyexception(vm, Some(source))) .unwrap(); scope.globals.set_item("code", code_obj.into(), vm).unwrap(); @@ -228,7 +228,7 @@ output = re.sub(r'(0xdeadbeef', tmp let py_code_obj = vm .compile(py_source, Mode::Exec, FNAME) - .map_err(|err| vm.new_syntax_error(&err, Some(py_source))) + .map_err(|err| err.into_pyexception(vm, Some(py_source))) .unwrap(); vm.run_code_obj(py_code_obj, scope.clone()).unwrap(); diff --git a/crates/stdlib/src/_queue.rs b/crates/stdlib/src/_queue.rs index 65456bd355b..96e77a34a0b 100644 --- a/crates/stdlib/src/_queue.rs +++ b/crates/stdlib/src/_queue.rs @@ -31,6 +31,17 @@ mod _queue { const INITIAL_RING_BUF_CAPACITY: usize = 8; + /// `parking_lot`'s `Condvar` doesn't expose a mid-wait signal to us (unlike + /// CPython's raw `sem_timedwait`, which reports `EINTR`), so we poll instead. + // FIXME: interim stopgap. The signal already interrupts the wait with EINTR + // (SA_RESTART cleared via `siginterrupt`), but `parking_lot::Condvar` swallows + // it and re-parks, forcing this poll. Replace with a shared interruptible + // timed-wait that surfaces EINTR (`poll`/wakeup-fd, portable incl. macOS; or + // `sem_timedwait` where available) -- `_thread` lock and `Thread.join` share + // this defect. Then this constant and the chunking loop go away. + #[cfg(feature = "threading")] + const SIGNAL_CHECK_INTERVAL: Duration = Duration::from_millis(50); + #[pyattr] #[pyclass(module = "_queue", name = "Empty", base = PyException)] #[repr(transparent)] @@ -63,40 +74,70 @@ mod _queue { } } - fn release(&self) { + /// Take `mutex`, detaching first so that blocking on it cannot stall a + /// stop-the-world request. + /// + /// A waiter holds this mutex across its `allow_threads` wait, so it can + /// still hold it when it is stopped. An attached thread blocking on it + /// would then never reach a safepoint, the stop would never complete, + /// and the holder would never be resumed to release it. + fn lock_count(&self, vm: &VirtualMachine) -> parking_lot::MutexGuard<'_, usize> { + vm.allow_threads(|| self.mutex.lock()) + } + + fn release(&self, vm: &VirtualMachine) { { - let mut count = self.mutex.lock(); + let mut count = self.lock_count(vm); *count += 1; } // lock dropped. now we can notify a waiting thread self.cond.notify_one(); } - /// Returns `true` if the semaphore was acquired, `false` on timeout. - #[must_use] - fn acquire(&self, block: bool, deadline: Option, vm: &VirtualMachine) -> bool { - let mut count = self.mutex.lock(); + /// `Ok(true)` if acquired, `Ok(false)` on timeout, `Err` if a signal + /// handler raised (e.g. `KeyboardInterrupt`) while we were waiting. + fn acquire( + &self, + block: bool, + deadline: Option, + vm: &VirtualMachine, + ) -> PyResult { loop { - if *count > 0 { - *count -= 1; - return true; - } + // Guard must be dropped before check_signals() below, since a + // signal handler may call back into this same queue. + { + let mut count = self.lock_count(vm); + + if *count > 0 { + *count -= 1; + return Ok(true); + } - if !block { - return false; - } + if !block { + return Ok(false); + } + + let now = Instant::now(); + let chunk_deadline = deadline.map_or_else( + || now + SIGNAL_CHECK_INTERVAL, + |dl| dl.min(now + SIGNAL_CHECK_INTERVAL), + ); - match deadline { - Some(dl) => { - let result = vm.allow_threads(|| self.cond.wait_until(&mut count, dl)); - if result.timed_out() && *count == 0 { - return false; - } + vm.allow_threads(|| self.cond.wait_until(&mut count, chunk_deadline)); + + if *count > 0 { + *count -= 1; + return Ok(true); } - None => { - vm.allow_threads(|| self.cond.wait(&mut count)); + + if let Some(dl) = deadline + && Instant::now() >= dl + { + return Ok(false); } } + + vm.check_signals()?; } } } @@ -121,11 +162,15 @@ mod _queue { } impl PySimpleQueue { - fn push(&self, item: PyObjectRef) { + #[cfg_attr( + not(feature = "threading"), + expect(unused_variables, reason = "only the semaphore needs the vm") + )] + fn push(&self, item: PyObjectRef, vm: &VirtualMachine) { self.buf.lock().push_back(item); #[cfg(feature = "threading")] - self.sem.release(); + self.sem.release(vm); } /// Returns a strong reference from the head of the buffer. @@ -191,14 +236,14 @@ mod _queue { } #[pymethod] - fn put(&self, args: PutArgs) { + fn put(&self, args: PutArgs, vm: &VirtualMachine) { let PutArgs { item, .. } = args; - self.push(item); + self.push(item, vm); } #[pymethod] - fn put_nowait(&self, item: PyObjectRef) { - self.push(item); + fn put_nowait(&self, item: PyObjectRef, vm: &VirtualMachine) { + self.push(item, vm); } #[pymethod] @@ -227,7 +272,7 @@ mod _queue { #[cfg(feature = "threading")] { - if !self.sem.acquire(block, deadline, vm) { + if !self.sem.acquire(block, deadline, vm)? { return Err(empty_error(vm)); } } @@ -239,7 +284,7 @@ mod _queue { fn get_nowait(&self, vm: &VirtualMachine) -> PyResult { #[cfg(feature = "threading")] { - if !self.sem.acquire(false, None, vm) { + if !self.sem.acquire(false, None, vm)? { return Err(empty_error(vm)); } } @@ -252,7 +297,7 @@ mod _queue { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/stdlib/src/_sqlite3.rs b/crates/stdlib/src/_sqlite3.rs index cbe9b85c63c..02d40845058 100644 --- a/crates/stdlib/src/_sqlite3.rs +++ b/crates/stdlib/src/_sqlite3.rs @@ -31,11 +31,11 @@ mod _sqlite3 { sqlite3_column_double, sqlite3_column_int64, sqlite3_column_name, sqlite3_column_text, sqlite3_column_type, sqlite3_complete, sqlite3_context, sqlite3_context_db_handle, sqlite3_create_collation_v2, sqlite3_create_function_v2, sqlite3_create_window_function, - sqlite3_data_count, sqlite3_db_handle, sqlite3_errcode, sqlite3_errmsg, sqlite3_exec, - sqlite3_expanded_sql, sqlite3_extended_errcode, sqlite3_finalize, sqlite3_get_autocommit, - sqlite3_interrupt, sqlite3_last_insert_rowid, sqlite3_libversion, sqlite3_limit, - sqlite3_open_v2, sqlite3_prepare_v2, sqlite3_progress_handler, sqlite3_reset, - sqlite3_result_blob, sqlite3_result_double, sqlite3_result_error, + sqlite3_data_count, sqlite3_db_config, sqlite3_db_handle, sqlite3_errcode, sqlite3_errmsg, + sqlite3_exec, sqlite3_expanded_sql, sqlite3_extended_errcode, sqlite3_finalize, + sqlite3_get_autocommit, sqlite3_interrupt, sqlite3_last_insert_rowid, sqlite3_libversion, + sqlite3_limit, sqlite3_open_v2, sqlite3_prepare_v2, sqlite3_progress_handler, + sqlite3_reset, sqlite3_result_blob, sqlite3_result_double, sqlite3_result_error, sqlite3_result_error_nomem, sqlite3_result_error_toobig, sqlite3_result_int64, sqlite3_result_null, sqlite3_result_text, sqlite3_set_authorizer, sqlite3_sleep, sqlite3_step, sqlite3_stmt, sqlite3_stmt_busy, sqlite3_stmt_readonly, sqlite3_threadsafe, @@ -54,7 +54,7 @@ mod _sqlite3 { use rustpython_vm::{ __exports::paste, AsObject, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, - TryFromBorrowedObject, VirtualMachine, atomic_func, + TryFromBorrowedObject, TryFromObject, VirtualMachine, atomic_func, builtins::{ PyBaseException, PyBaseExceptionRef, PyByteArray, PyBytes, PyDict, PyDictRef, PyFloat, PyInt, PyIntRef, PyModule, PySlice, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, @@ -161,7 +161,17 @@ mod _sqlite3 { SQLITE_ALTER_TABLE, SQLITE_ANALYZE, SQLITE_ATTACH, SQLITE_CREATE_INDEX, SQLITE_CREATE_TABLE, SQLITE_CREATE_TEMP_INDEX, SQLITE_CREATE_TEMP_TABLE, SQLITE_CREATE_TEMP_TRIGGER, SQLITE_CREATE_TEMP_VIEW, SQLITE_CREATE_TRIGGER, - SQLITE_CREATE_VIEW, SQLITE_CREATE_VTABLE, SQLITE_DELETE, SQLITE_DENY, SQLITE_DETACH, + SQLITE_CREATE_VIEW, SQLITE_CREATE_VTABLE, SQLITE_DBCONFIG_DEFENSIVE, + SQLITE_DBCONFIG_DQS_DDL, SQLITE_DBCONFIG_DQS_DML, SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE, + SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, SQLITE_DBCONFIG_ENABLE_COMMENTS, + SQLITE_DBCONFIG_ENABLE_FKEY, SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, + SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_DBCONFIG_ENABLE_QPSG, + SQLITE_DBCONFIG_ENABLE_TRIGGER, SQLITE_DBCONFIG_ENABLE_VIEW, + SQLITE_DBCONFIG_LEGACY_ALTER_TABLE, SQLITE_DBCONFIG_LEGACY_FILE_FORMAT, + SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE, SQLITE_DBCONFIG_RESET_DATABASE, + SQLITE_DBCONFIG_REVERSE_SCANORDER, SQLITE_DBCONFIG_STMT_SCANSTATUS, + SQLITE_DBCONFIG_TRIGGER_EQP, SQLITE_DBCONFIG_TRUSTED_SCHEMA, + SQLITE_DBCONFIG_WRITABLE_SCHEMA, SQLITE_DELETE, SQLITE_DENY, SQLITE_DETACH, SQLITE_DROP_INDEX, SQLITE_DROP_TABLE, SQLITE_DROP_TEMP_INDEX, SQLITE_DROP_TEMP_TABLE, SQLITE_DROP_TEMP_TRIGGER, SQLITE_DROP_TEMP_VIEW, SQLITE_DROP_TRIGGER, SQLITE_DROP_VIEW, SQLITE_DROP_VTABLE, SQLITE_FUNCTION, SQLITE_IGNORE, SQLITE_INSERT, SQLITE_LIMIT_ATTACHED, @@ -322,7 +332,7 @@ mod _sqlite3 { ))) } } else { - Err(vm.new_type_error(format!( + Err(vm.new_value_error(format!( "autocommit must be True, False, or sqlite3.LEGACY_TRANSACTION_CONTROL, not {}", obj.class().name() ))) @@ -330,6 +340,19 @@ mod _sqlite3 { } } + struct IsolationLevelArg(Option); + + impl TryFromObject for IsolationLevelArg { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + if vm.is_none(&obj) { + return Ok(Self(None)); + } + obj.downcast::() + .map(|s| Self(Some(s))) + .map_err(|_| vm.new_type_error("isolation_level must be str or None".to_owned())) + } + } + #[derive(FromArgs)] struct ConnectArgs { #[pyarg(any)] @@ -338,8 +361,8 @@ mod _sqlite3 { timeout: TimeoutSeconds, #[pyarg(any, default = 0)] detect_types: c_int, - #[pyarg(any, default = Some(vm.ctx.empty_str.to_owned()))] - isolation_level: Option, + #[pyarg(any, default = IsolationLevelArg(Some(vm.ctx.empty_str.to_owned())))] + isolation_level: IsolationLevelArg, #[pyarg(any, default = true)] check_same_thread: bool, #[pyarg(any, default = Connection::class(&vm.ctx).to_owned())] @@ -356,7 +379,7 @@ mod _sqlite3 { unsafe impl Traverse for ConnectArgs { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - self.isolation_level.traverse(tracer_fn); + self.isolation_level.0.traverse(tracer_fn); self.factory.traverse(tracer_fn); } } @@ -574,10 +597,10 @@ mod _sqlite3 { ) -> c_int { let (callable, vm) = unsafe { (*data.cast::()).retrieve() }; let f = || -> PyResult { - let arg1 = ptr_to_str(arg1, vm)?; - let arg2 = ptr_to_str(arg2, vm)?; - let db_name = ptr_to_str(db_name, vm)?; - let access = ptr_to_str(access, vm)?; + let arg1 = ptr_to_str_or_none(arg1, vm)?; + let arg2 = ptr_to_str_or_none(arg2, vm)?; + let db_name = ptr_to_str_or_none(db_name, vm)?; + let access = ptr_to_str_or_none(access, vm)?; let val = callable.call((action, arg1, arg2, db_name, access), vm)?; let Some(val) = val.downcast_ref::() else { @@ -914,7 +937,7 @@ mod _sqlite3 { db: PyMutex::new(db), initialized: Radium::new(initialized), detect_types: Radium::new(args.detect_types), - isolation_level: PyAtomicRef::from(args.isolation_level), + isolation_level: PyAtomicRef::from(args.isolation_level.0), check_same_thread: Radium::new(args.check_same_thread), thread_ident: PyMutex::new(std::thread::current().id()), row_factory: PyAtomicRef::from(None), @@ -950,7 +973,7 @@ mod _sqlite3 { zelf.reset_factories(vm); if was_initialized { - zelf.drop_db(); + zelf.drop_db(vm)?; } // Attempt to open the new database before mutating other state so failures leave @@ -970,7 +993,7 @@ mod _sqlite3 { .store(check_same_thread, Ordering::Relaxed); *zelf.autocommit.lock() = autocommit; *zelf.thread_ident.lock() = std::thread::current().id(); - let _ = unsafe { zelf.isolation_level.swap(isolation_level) }; + let _ = unsafe { zelf.isolation_level.swap(isolation_level.0) }; let mut guard = zelf.db.lock(); *guard = Some(db); @@ -981,8 +1004,20 @@ mod _sqlite3 { #[pyclass(with(Constructor, Callable, Initializer), flags(BASETYPE, HAS_WEAKREF))] impl Connection { - fn drop_db(&self) { - self.db.lock().take(); + fn drop_db(&self, vm: &VirtualMachine) -> PyResult<()> { + let mut guard = self.db.lock(); + let rollback_result = if let Some(db) = guard.as_ref() + && *self.autocommit.lock() == AutocommitMode::Disabled + && !db.is_autocommit() + { + db._exec(b"ROLLBACK\0", vm) + } else { + Ok(()) + }; + let db = guard.take(); + drop(guard); + drop(db); + rollback_result } fn reset_factories(&self, vm: &VirtualMachine) { @@ -996,9 +1031,12 @@ mod _sqlite3 { let db = Sqlite::from(SqliteRaw::open(path.as_ptr(), args.uri, vm)?); let timeout = (args.timeout.to_secs_f64() * 1000.0) as c_int; db.busy_timeout(timeout); - if let Some(isolation_level) = &args.isolation_level { + if let Some(isolation_level) = &args.isolation_level.0 { begin_statement_ptr_from_isolation_level(isolation_level, vm)?; } + if args.autocommit == AutocommitMode::Disabled { + db._exec(b"BEGIN\0", vm)?; + } Ok(db) } @@ -1013,6 +1051,11 @@ mod _sqlite3 { Ok(PyMutexGuard::map(guard, |x| unsafe { x.as_mut().unwrap_unchecked() })) + } else if self.initialized.load(Ordering::Acquire) { + Err(new_programming_error( + vm, + "Cannot operate on a closed database.".to_owned(), + )) } else { Err(new_programming_error( vm, @@ -1090,8 +1133,7 @@ mod _sqlite3 { #[pymethod] fn close(&self, vm: &VirtualMachine) -> PyResult<()> { self.check_thread(vm)?; - self.drop_db(); - Ok(()) + self.drop_db(vm) } fn is_closed(&self) -> bool { @@ -1100,16 +1142,35 @@ mod _sqlite3 { #[pymethod] fn commit(&self, vm: &VirtualMachine) -> PyResult<()> { - self.db_lock(vm)?.implicit_commit(vm) + let db = self.db_lock(vm)?; + let mode = *self.autocommit.lock(); + match mode { + AutocommitMode::Legacy => db.implicit_commit(vm), + AutocommitMode::Enabled => Ok(()), + AutocommitMode::Disabled => { + db._exec(b"COMMIT\0", vm)?; + db._exec(b"BEGIN\0", vm) + } + } } #[pymethod] fn rollback(&self, vm: &VirtualMachine) -> PyResult<()> { let db = self.db_lock(vm)?; - if !db.is_autocommit() { - db._exec(b"ROLLBACK\0", vm) - } else { - Ok(()) + let mode = *self.autocommit.lock(); + match mode { + AutocommitMode::Legacy => { + if db.is_autocommit() { + Ok(()) + } else { + db._exec(b"ROLLBACK\0", vm) + } + } + AutocommitMode::Enabled => Ok(()), + AutocommitMode::Disabled => { + db._exec(b"ROLLBACK\0", vm)?; + db._exec(b"BEGIN\0", vm) + } } } @@ -1228,6 +1289,7 @@ mod _sqlite3 { SQLITE_UTF8 }; let db = self.db_lock(vm)?; + check_num_params(&db, args.narg, "narg", vm)?; let Some(data) = CallbackData::new(args.func, vm) else { return db.create_function( name.as_ptr(), @@ -1259,6 +1321,7 @@ mod _sqlite3 { fn create_aggregate(&self, args: CreateAggregateArgs, vm: &VirtualMachine) -> PyResult<()> { let name = args.name.to_cstring(vm)?; let db = self.db_lock(vm)?; + check_num_params(&db, args.narg, "n_arg", vm)?; let Some(data) = CallbackData::new(args.aggregate_class, vm) else { return db.create_function( name.as_ptr(), @@ -1341,6 +1404,7 @@ mod _sqlite3 { ) -> PyResult<()> { let name = name.to_cstring(vm)?; let db = self.db_lock(vm)?; + check_num_params(&db, narg, "num_params", vm)?; let Some(data) = CallbackData::new(aggregate_class, vm) else { unsafe { sqlite3_create_window_function( @@ -1465,6 +1529,39 @@ mod _sqlite3 { self.db_lock(vm)?.limit(category, limit, vm) } + #[pymethod] + fn setconfig( + &self, + op: c_int, + enable: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult<()> { + let db = self.db_lock(vm)?; + if !is_int_dbconfig(op) { + return Err(vm.new_value_error(format!("unknown config 'op': {op}"))); + } + let enable = enable.unwrap_or(true) as c_int; + let mut actual: c_int = 0; + let rc = unsafe { sqlite3_db_config(db.db, op, enable, &mut actual) }; + db.check(rc, vm)?; + if enable != actual { + return Err(new_operational_error(vm, "Unable to set config".to_owned())); + } + Ok(()) + } + + #[pymethod] + fn getconfig(&self, op: c_int, vm: &VirtualMachine) -> PyResult { + let db = self.db_lock(vm)?; + if !is_int_dbconfig(op) { + return Err(vm.new_value_error(format!("unknown config 'op': {op}"))); + } + let mut current: c_int = 0; + let rc = unsafe { sqlite3_db_config(db.db, op, -1, &mut current) }; + db.check(rc, vm)?; + Ok(current != 0) + } + #[pymethod] fn __enter__(zelf: PyRef) -> PyRef { zelf @@ -1492,22 +1589,18 @@ mod _sqlite3 { #[pygetset(setter)] fn set_isolation_level( &self, - value: PySetterValue>, + value: PySetterValue, vm: &VirtualMachine, ) -> PyResult<()> { match value { - PySetterValue::Assign(value) => { + PySetterValue::Assign(IsolationLevelArg(value)) => { if let Some(val_str) = &value { begin_statement_ptr_from_isolation_level(val_str, vm)?; } // If setting isolation_level to None (auto-commit mode), commit any pending transaction if value.is_none() { - let db = self.db_lock(vm)?; - if !db.is_autocommit() { - // Keep the lock and call implicit_commit directly to avoid race conditions - db.implicit_commit(vm)?; - } + self.commit(vm)?; } let _ = unsafe { self.isolation_level.swap(value) }; Ok(()) @@ -1531,6 +1624,7 @@ mod _sqlite3 { fn set_autocommit(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let mode = AutocommitMode::try_from_borrowed_object(vm, &val)?; let db = self.db_lock(vm)?; + *self.autocommit.lock() = mode; // Handle transaction state based on mode change match mode { @@ -1550,9 +1644,6 @@ mod _sqlite3 { // Legacy mode doesn't change transaction state } } - - drop(db); - *self.autocommit.lock() = mode; Ok(()) } @@ -1597,6 +1688,47 @@ mod _sqlite3 { fn total_changes(&self, vm: &VirtualMachine) -> PyResult { self._db_lock(vm).map(|x| x.total_changes()) } + + #[pygetset(name = "Warning")] + fn exc_warning(&self) -> PyTypeRef { + warning_type().to_owned() + } + #[pygetset(name = "Error")] + fn exc_error(&self) -> PyTypeRef { + error_type().to_owned() + } + #[pygetset(name = "InterfaceError")] + fn exc_interface_error(&self) -> PyTypeRef { + interface_error_type().to_owned() + } + #[pygetset(name = "DatabaseError")] + fn exc_database_error(&self) -> PyTypeRef { + database_error_type().to_owned() + } + #[pygetset(name = "DataError")] + fn exc_data_error(&self) -> PyTypeRef { + data_error_type().to_owned() + } + #[pygetset(name = "OperationalError")] + fn exc_operational_error(&self) -> PyTypeRef { + operational_error_type().to_owned() + } + #[pygetset(name = "IntegrityError")] + fn exc_integrity_error(&self) -> PyTypeRef { + integrity_error_type().to_owned() + } + #[pygetset(name = "InternalError")] + fn exc_internal_error(&self) -> PyTypeRef { + internal_error_type().to_owned() + } + #[pygetset(name = "ProgrammingError")] + fn exc_programming_error(&self) -> PyTypeRef { + programming_error_type().to_owned() + } + #[pygetset(name = "NotSupportedError")] + fn exc_not_supported_error(&self) -> PyTypeRef { + not_supported_error_type().to_owned() + } } #[pyattr] @@ -1716,11 +1848,11 @@ mod _sqlite3 { let db = zelf.connection.db_lock(vm)?; - // Start implicit transaction for DML statements unless in autocommit mode + // Only legacy transaction control starts implicit DML transactions. if stmt.is_dml && db.is_autocommit() && zelf.connection.isolation_level.deref().is_some() - && *zelf.connection.autocommit.lock() != AutocommitMode::Enabled + && *zelf.connection.autocommit.lock() == AutocommitMode::Legacy { db.begin_transaction( zelf.connection @@ -1810,11 +1942,11 @@ mod _sqlite3 { let db = zelf.connection.db_lock(vm)?; - // Start implicit transaction for DML statements unless in autocommit mode + // Only legacy transaction control starts implicit DML transactions. if stmt.is_dml && db.is_autocommit() && zelf.connection.isolation_level.deref().is_some() - && *zelf.connection.autocommit.lock() != AutocommitMode::Enabled + && *zelf.connection.autocommit.lock() == AutocommitMode::Legacy { db.begin_transaction( zelf.connection @@ -1864,7 +1996,9 @@ mod _sqlite3 { db.sql_limit(script.byte_len(), vm)?; - db.implicit_commit(vm)?; + if *zelf.connection.autocommit.lock() == AutocommitMode::Legacy { + db.implicit_commit(vm)?; + } let script = script.to_cstring(vm)?; let mut ptr = script.as_ptr(); @@ -2237,7 +2371,7 @@ mod _sqlite3 { return self.data.getitem_by_index(vm, i); } } - Err(vm.new_index_error("No item with that key")) + Err(vm.new_index_error(format!("No item with key '{}'", name.to_string_lossy()))) } else if let Some(slice) = needle.downcast_ref::() { let list = self.data.getitem_by_slice(vm, slice.to_saturated(vm)?)?; Ok(vm.ctx.new_tuple(list).into()) @@ -2259,7 +2393,7 @@ mod _sqlite3 { .inner(vm)? .description .clone() - .ok_or_else(|| vm.new_value_error("no description in Cursor"))?; + .unwrap_or_else(|| vm.ctx.empty_tuple.clone()); Ok(Self { data, description }) } @@ -2751,12 +2885,14 @@ mod _sqlite3 { } let sql_cstr = sql.to_cstring(vm)?; - let db = connection.db_lock(vm)?; - - db.sql_limit(sql.byte_len(), vm)?; + let raw = { + let db = connection.db_lock(vm)?; + db.sql_limit(sql.byte_len(), vm)?; + **db + }; let mut tail = null(); - let st = db.prepare(sql_cstr.as_ptr(), &mut tail, vm)?; + let st = raw.prepare(sql_cstr.as_ptr(), &mut tail, vm)?; let Some(st) = st else { return Ok(None); @@ -3190,6 +3326,16 @@ mod _sqlite3 { } for i in 1..=num_needed { + let name = unsafe { sqlite3_bind_parameter_name(self.st, i) }; + if !name.is_null() && unsafe { *name } != b'?' as libc::c_char { + let name_str = ptr_to_str(name, vm)?; + return Err(new_programming_error( + vm, + format!( + "Binding {i} ('{name_str}') is a named parameter, but you supplied a sequence which requires nameless (qmark) placeholders." + ), + )); + } let val = seq.get_item(i as isize - 1, vm)?; self.bind_parameter(i, &val, vm)?; } @@ -3418,12 +3564,66 @@ mod _sqlite3 { Ok(obj) } + fn check_num_params( + db: &Sqlite, + n: c_int, + param_name: &str, + vm: &VirtualMachine, + ) -> PyResult<()> { + let limit = unsafe { sqlite3_limit(db.db, SQLITE_LIMIT_FUNCTION_ARG, -1) }; + if n < -1 || n > limit { + return Err(new_programming_error( + vm, + format!("'{param_name}' must be between -1 and {limit}, not {n}"), + )); + } + Ok(()) + } + + fn is_int_dbconfig(op: c_int) -> bool { + use libsqlite3_sys::*; + matches!( + op, + SQLITE_DBCONFIG_ENABLE_FKEY + | SQLITE_DBCONFIG_ENABLE_TRIGGER + | SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER + | SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION + | SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE + | SQLITE_DBCONFIG_ENABLE_QPSG + | SQLITE_DBCONFIG_TRIGGER_EQP + | SQLITE_DBCONFIG_RESET_DATABASE + | SQLITE_DBCONFIG_DEFENSIVE + | SQLITE_DBCONFIG_WRITABLE_SCHEMA + | SQLITE_DBCONFIG_LEGACY_ALTER_TABLE + | SQLITE_DBCONFIG_DQS_DDL + | SQLITE_DBCONFIG_DQS_DML + | SQLITE_DBCONFIG_ENABLE_VIEW + | SQLITE_DBCONFIG_LEGACY_FILE_FORMAT + | SQLITE_DBCONFIG_TRUSTED_SCHEMA + | SQLITE_DBCONFIG_STMT_SCANSTATUS + | SQLITE_DBCONFIG_REVERSE_SCANORDER + | SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE + | SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE + | SQLITE_DBCONFIG_ENABLE_COMMENTS + ) + } + fn ptr_to_str<'a>(p: *const libc::c_char, vm: &VirtualMachine) -> PyResult<&'a str> { if p.is_null() { return Err(vm.new_memory_error("string pointer is null")); } unsafe { CStr::from_ptr(p).to_str() } - .map_err(|_| vm.new_value_error("Invalid UIF-8 codepoint")) + .map_err(|_| vm.new_value_error("Invalid UTF-8 codepoint")) + } + + fn ptr_to_str_or_none(p: *const libc::c_char, vm: &VirtualMachine) -> PyResult { + if p.is_null() { + return Ok(vm.ctx.none()); + } + let s = unsafe { CStr::from_ptr(p) } + .to_str() + .map_err(|_| vm.new_value_error("Invalid UTF-8 codepoint".to_owned()))?; + Ok(vm.ctx.new_str(s).into()) } fn ptr_to_string( diff --git a/crates/stdlib/src/_testconsole.rs b/crates/stdlib/src/_testconsole.rs index 78cba3b397d..3b7aad17178 100644 --- a/crates/stdlib/src/_testconsole.rs +++ b/crates/stdlib/src/_testconsole.rs @@ -17,11 +17,11 @@ mod _testconsole { let data = &*data; // Interpret as UTF-16-LE pairs - if !data.len().is_multiple_of(2) { + let (chunks, []) = data.as_chunks::<2>() else { return Err(vm.new_value_error("buffer must contain UTF-16-LE data (even length)")); - } - let wchars: Vec = data - .chunks_exact(2) + }; + let wchars: Vec = chunks + .iter() .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) .collect(); host_testconsole::write_console_input(fd, &wchars).map_err(|e| e.into_pyexception(vm)) diff --git a/crates/stdlib/src/_tokenize.rs b/crates/stdlib/src/_tokenize.rs index 7106c527096..c071a2b62f2 100644 --- a/crates/stdlib/src/_tokenize.rs +++ b/crates/stdlib/src/_tokenize.rs @@ -237,7 +237,9 @@ mod _tokenize { } let raw_type = token_kind_value(kind); - let token_type = if extra_tokens && raw_type > TOKEN_DEDENT && raw_type < TOKEN_OP { + let token_type = if extra_tokens + && (kind == TokenKind::Unknown || (raw_type > TOKEN_DEDENT && raw_type < TOKEN_OP)) + { TOKEN_OP } else { raw_type diff --git a/crates/stdlib/src/array.rs b/crates/stdlib/src/array.rs index e9b389949c1..b1fa925e16d 100644 --- a/crates/stdlib/src/array.rs +++ b/crates/stdlib/src/array.rs @@ -27,8 +27,8 @@ pub mod array { ArgBytesLike, ArgIntoFloat, ArgIterable, KwArgs, OptionalArg, PyComparisonValue, }, protocol::{ - BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, - PyMappingMethods, PySequenceMethods, + BufferDescriptor, BufferFlags, BufferMethods, BufferResizeGuard, PyBuffer, + PyIterReturn, PyMappingMethods, PySequenceMethods, }, sequence::{OptionalRangeArgs, SequenceExt, SequenceMutExt}, sliceable::{ @@ -55,12 +55,17 @@ pub mod array { $($n(Vec<$t>),)* } + /// One item, already converted to the array's element type. + enum ArrayItem { + $($n($t),)* + } + impl ArrayContentType { fn from_char(c: char) -> Result { match c { $($c => Ok(ArrayContentType::$n(Vec::new())),)* _ => Err( - "bad typecode (must be b, B, u, h, H, i, I, l, L, q, Q, f or d)".into() + "bad typecode (must be b, B, u, w, h, H, i, I, l, L, q, Q, f or d)".into() ), } } @@ -303,17 +308,31 @@ pub mod array { } } - fn setitem_by_index( + /// Convert an object to the element type of the array with + /// this typecode. This runs the object's conversion methods, + /// which can reach the array, so it takes the typecode by + /// value and holds no lock on it. + fn item_from_object( + typecode: char, + value: PyObjectRef, + vm: &VirtualMachine + ) -> PyResult { + match typecode { + $($c => Ok(ArrayItem::$n(<$t>::try_into_from_object(vm, value)?)),)* + _ => unreachable!("array has a typecode"), + } + } + + fn setitem_by_item( &mut self, i: isize, - value: PyObjectRef, + item: ArrayItem, vm: &VirtualMachine ) -> PyResult<()> { - match self { - $(ArrayContentType::$n(v) => { - let value = <$t>::try_into_from_object(vm, value)?; - v.setitem_by_index(vm, i, value) - })* + match (self, item) { + $((ArrayContentType::$n(v), ArrayItem::$n(value)) => + v.setitem_by_index(vm, i, value),)* + _ => unreachable!("item was converted for this array"), } } @@ -473,6 +492,7 @@ pub mod array { (SignedByte, i8, 'b', "b"), (UnsignedByte, u8, 'B', "B"), (PyUnicode, WideChar, 'u', "u"), + (PyUcs4, Ucs4Char, 'w', "w"), (SignedShort, raw::c_short, 'h', "h"), (UnsignedShort, raw::c_ushort, 'H', "H"), (SignedInt, raw::c_int, 'i', "i"), @@ -488,6 +508,11 @@ pub mod array { #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct WideChar(wchar_t); + /// Element type for the 'w' typecode: always a 4-byte unicode code point + /// (Py_UCS4), unlike 'u' which is platform-dependent `wchar_t`. + #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] + pub struct Ucs4Char(u32); + trait ArrayElement: Sized { fn try_into_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult; fn byteswap(self) -> Self; @@ -498,7 +523,7 @@ pub mod array { ($($t:ty,)*) => {$( impl ArrayElement for $t { fn try_into_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - obj.try_index(vm)?.try_to_primitive(vm) + obj.try_index(vm)?.try_to_primitive_raw(vm) } fn byteswap(self) -> Self { <$t>::swap_bytes(self) @@ -574,6 +599,47 @@ pub mod array { } } + impl ArrayElement for Ucs4Char { + fn try_into_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + let s = obj.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "array item must be a unicode character, not {}", + obj.class().name() + )) + })?; + s.as_wtf8() + .code_points() + .exactly_one() + .map(|ch| Self(ch.to_u32())) + .map_err(|e| { + vm.new_type_error(format!( + "array item must be a unicode character, not a string of length {}", + e.count() + )) + }) + } + fn byteswap(self) -> Self { + Self(self.0.swap_bytes()) + } + fn to_object(self, _vm: &VirtualMachine) -> PyObjectRef { + unreachable!() + } + } + + impl ToPyResult for Ucs4Char { + fn to_pyresult(self, vm: &VirtualMachine) -> PyResult { + Ok(u32_to_char(self.0) + .map_err(|msg| vm.new_value_error(msg))? + .to_pyobject(vm)) + } + } + + impl fmt::Display for Ucs4Char { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + unreachable!("`repr(array('w'))` calls `PyStr::repr`") + } + } + fn u32_to_char(ch: u32) -> Result { CodePoint::from_u32(ch) .ok_or_else(|| format!("character U+{ch:4x} is not in range [U+0000; U+10ffff]")) @@ -591,7 +657,7 @@ pub mod array { impl ToPyResult for WideChar { fn to_pyresult(self, vm: &VirtualMachine) -> PyResult { Ok(CodePoint::try_from(self) - .map_err(|e| vm.new_unicode_encode_error(e))? + .map_err(|e| vm.new_value_error(e))? .to_pyobject(vm)) } } @@ -663,7 +729,7 @@ pub mod array { let value = init.read().typecode(); match (spec, value) { (spec, ch) if spec == ch => array.frombytes(&init.get_bytes()), - (spec, 'u') => { + (spec, 'u' | 'w') if !matches!(spec, 'u' | 'w') => { return Err(vm.new_type_error(format!( "cannot use a unicode array to initialize an array with typecode '{spec}'" ))) @@ -675,7 +741,7 @@ pub mod array { } } } else if let Some(wtf8) = init.downcast_ref::() { - if spec == 'u' { + if matches!(spec, 'u' | 'w') { let bytes = Self::_unicode_to_wchar_bytes(wtf8.as_wtf8(), array.itemsize()); array.frombytes_move(bytes); } else { @@ -685,12 +751,12 @@ pub mod array { } } else if init.downcastable::() || init.downcastable::() { init.try_bytes_like(vm, |x| array.frombytes(x))?; - } else if let Ok(iter) = ArgIterable::try_from_object(vm, init.clone()) { + } else { + // Everything else is taken item by item, buffer or not. + let iter = ArgIterable::try_from_object(vm, init)?; for obj in iter.iter(vm)? { array.push(obj?, vm)?; } - } else { - init.try_bytes_like(vm, |x| array.frombytes(x))?; } } @@ -824,10 +890,10 @@ pub mod array { obj.class().name() )) })?; - if zelf.read().typecode() != 'u' { - return Err( - vm.new_value_error("fromunicode() may only be called on unicode type arrays") - ); + if !matches!(zelf.read().typecode(), 'u' | 'w') { + return Err(vm.new_value_error( + "fromunicode() may only be called on unicode type arrays ('u' or 'w')", + )); } let mut w = zelf.try_resizable(vm)?; let bytes = Self::_unicode_to_wchar_bytes(wtf8, w.itemsize()); @@ -838,10 +904,10 @@ pub mod array { #[pymethod] fn tounicode(&self, vm: &VirtualMachine) -> PyResult { let array = self.array.read(); - if array.typecode() != 'u' { - return Err( - vm.new_value_error("tounicode() may only be called on unicode type arrays") - ); + if !matches!(array.typecode(), 'u' | 'w') { + return Err(vm.new_value_error( + "tounicode() may only be called on unicode type arrays ('u' or 'w')", + )); } let bytes = array.get_bytes(); Self::_wchar_bytes_to_string(bytes, self.itemsize(), vm) @@ -859,6 +925,11 @@ pub mod array { #[pymethod] fn frombytes(&self, b: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + // The source is read as bytes, so items of any other width would + // be reinterpreted rather than appended. + if b.itemsize() != 1 { + return Err(vm.new_type_error("a bytes-like object is required")); + } let b = b.borrow_buf(); let itemsize = self.read().itemsize(); self._from_bytes(&b, itemsize, vm) @@ -1000,7 +1071,11 @@ pub mod array { vm: &VirtualMachine, ) -> PyResult<()> { match SequenceIndex::try_from_borrowed_object(vm, needle, "array")? { - SequenceIndex::Int(i) => zelf.write().setitem_by_index(i, value, vm), + SequenceIndex::Int(i) => { + let typecode = zelf.read().typecode(); + let item = ArrayContentType::item_from_object(typecode, value, vm)?; + zelf.write().setitem_by_item(i, item, vm) + } SequenceIndex::Slice(slice) => { let cloned; let guard; @@ -1152,7 +1227,7 @@ pub mod array { let array = zelf.read(); let cls = zelf.class().to_owned(); let typecode = vm.ctx.new_str(array.typecode_str()); - let values = if array.typecode() == 'u' { + let values = if matches!(array.typecode(), 'u' | 'w') { let s = Self::_wchar_bytes_to_string(array.get_bytes(), array.itemsize(), vm)?; s.code_points().map(|x| x.to_pyobject(vm)).collect() } else { @@ -1187,7 +1262,7 @@ pub mod array { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -1244,20 +1319,42 @@ pub mod array { } } + impl PyArray { + fn buffer_desc(&self) -> BufferDescriptor { + let array = self.read(); + BufferDescriptor::format( + array.len() * array.itemsize(), + false, + array.itemsize(), + array.typecode_str().into(), + ) + } + } + impl AsBuffer for PyArray { + const RELEASE_BUFFER: bool = true; + + // array_buffer_getbuf, which reports the type code only when the request + // asked for a format. + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + let zelf = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("unexpected payload for as_buffer"))?; + let desc = zelf.buffer_desc().projected(flags); + flags.check_writable(desc.readonly, "Object is not writable.", vm)?; + Ok(PyBuffer::new(zelf.to_owned().into(), desc, &BUFFER_METHODS)) + } + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - let array = zelf.read(); - let buf = PyBuffer::new( + Ok(PyBuffer::new( zelf.to_owned().into(), - BufferDescriptor::format( - array.len() * array.itemsize(), - false, - array.itemsize(), - array.typecode_str().into(), - ), + zelf.buffer_desc(), &BUFFER_METHODS, - ); - Ok(buf) + )) } } @@ -1266,13 +1363,14 @@ pub mod array { fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { let class = zelf.class(); let class_name = class.name(); - if zelf.read().typecode() == 'u' { + let typecode = zelf.read().typecode(); + if matches!(typecode, 'u' | 'w') { if zelf.__len__() == 0 { - return Ok(format!("{class_name}('u')")); + return Ok(format!("{class_name}('{typecode}')")); } let to_unicode = zelf.tounicode(vm)?; let escape = crate::vm::literal::escape::UnicodeEscape::new_repr(&to_unicode); - return Ok(format!("{}('u', {})", class_name, escape.str_repr())); + return Ok(format!("{class_name}('{typecode}', {})", escape.str_repr())); } zelf.read().repr(&class_name, vm) } @@ -1338,7 +1436,9 @@ pub mod array { ass_item: atomic_func!(|seq, i, value, vm| { let zelf = PyArray::sequence_downcast(seq); if let Some(value) = value { - zelf.write().setitem_by_index(i, value, vm) + let typecode = zelf.read().typecode(); + let item = ArrayContentType::item_from_object(typecode, value, vm)?; + zelf.write().setitem_by_item(i, item, vm) } else { zelf.write().delitem_by_index(i, vm) } @@ -1373,8 +1473,15 @@ pub mod array { type Resizable<'a> = PyRwLockWriteGuard<'a, ArrayContentType>; fn try_resizable_opt(&self) -> Option> { - let w = self.write(); - (self.exports.load(atomic::Ordering::SeqCst) == 0).then_some(w) + // An export is a borrow someone else still holds, so it is + // answered before the lock rather than by waiting on it. + (self.exports.load(atomic::Ordering::SeqCst) == 0).then(|| self.write()) + } + + fn try_resizable(&self, vm: &VirtualMachine) -> PyResult> { + self.try_resizable_opt().ok_or_else(|| { + vm.new_buffer_error("cannot resize an array that is exporting buffers") + }) } } @@ -1524,6 +1631,7 @@ pub mod array { _ => None, }; } + 'w' => return Some(Self::Utf32 { big_endian }), 'f' => { // Copied from CPython const Y: f32 = 16711938.0; @@ -1647,8 +1755,17 @@ pub mod array { })?, MachineFormatCode::Utf16 { big_endian } => { let utf16: Vec<_> = chunks.map(|b| chunk_to_obj!(b, u16, big_endian)).collect(); - let s = String::from_utf16(&utf16) - .map_err(|_| vm.new_unicode_encode_error("items cannot decode as utf16"))?; + let s = String::from_utf16(&utf16).map_err(|_| { + let (index, reason) = invalid_utf16(&utf16).unwrap(); + vm.new_unicode_decode_error( + vm.ctx + .new_str(if big_endian { "utf-16-be" } else { "utf-16-le" }), + args.items.clone(), + index * 2, + index * 2 + 2, + vm.ctx.new_str(reason), + ) + })?; let bytes = PyArray::_unicode_to_wchar_bytes((*s).as_ref(), array.itemsize()); array.frombytes_move(bytes); } @@ -1664,6 +1781,25 @@ pub mod array { PyArray::from(array).into_ref_with_type(vm, cls) } + fn invalid_utf16(units: &[u16]) -> Option<(usize, &'static str)> { + let mut index = 0; + while index < units.len() { + let unit = units[index]; + if (0xd800..=0xdbff).contains(&unit) { + match units.get(index + 1) { + Some(next) if (0xdc00..=0xdfff).contains(next) => index += 2, + Some(_) => return Some((index, "illegal UTF-16 surrogate")), + None => return Some((index, "unexpected end of data")), + } + } else if (0xdc00..=0xdfff).contains(&unit) { + return Some((index, "illegal encoding")); + } else { + index += 1; + } + } + None + } + // Register array.array as collections.abc.MutableSequence pub(crate) fn module_exec( vm: &VirtualMachine, diff --git a/crates/stdlib/src/binascii.rs b/crates/stdlib/src/binascii.rs index 30e9b379ad9..d0cdc2148e7 100644 --- a/crates/stdlib/src/binascii.rs +++ b/crates/stdlib/src/binascii.rs @@ -386,18 +386,18 @@ mod decl { } #[inline] - fn uu_a2b_read(c: &u8, vm: &VirtualMachine) -> PyResult { + fn uu_a2b_read(c: u8, vm: &VirtualMachine) -> PyResult { // Check the character for legality // The 64 instead of the expected 63 is because // there are a few uuencodes out there that use // '`' as zero instead of space. - if !(b' '..=(b' ' + 64)).contains(c) { - if [b'\r', b'\n'].contains(c) { + if !(b' '..=(b' ' + 64)).contains(&c) { + if b"\r\n".contains(&c) { return Ok(0); } return Err(super::new_binascii_error("Illegal char", vm)); } - Ok((*c - b' ') & 0x3f) + Ok((c - b' ') & 0x3f) } #[derive(FromArgs)] @@ -407,6 +407,7 @@ mod decl { #[pyarg(named, default = false)] header: bool, } + #[pyfunction] fn a2b_qp(args: A2bQpArgs) -> PyResult> { let s = args.data; @@ -503,7 +504,7 @@ mod decl { } in_idx += 1; } - if buflen > 0 && in_idx < buflen && buf[in_idx - 1] == b'\r' { + if in_idx > 0 && in_idx < buflen && buf[in_idx - 1] == b'\r' { crlf = true; } @@ -745,12 +746,12 @@ mod decl { #[pyfunction] fn a2b_uu(s: ArgAsciiBuffer, vm: &VirtualMachine) -> PyResult> { s.with_ref(|b| { + if b.is_empty() { + return Err(super::new_binascii_error("Missing length byte", vm)); + } + // First byte: binary data length (in bytes) - let length = if b.is_empty() { - ((-0x20i32) & 0x3fi32) as usize - } else { - ((b[0] - b' ') & 0x3f) as usize - }; + let length = ((b[0] - b' ') & 0x3f) as usize; // Allocate the buffer let mut res = Vec::::with_capacity(length); @@ -760,7 +761,7 @@ mod decl { let (char_a, char_b, char_c, char_d) = { let mut chunk = chunk .iter() - .map(|x| uu_a2b_read(x, vm)) + .map(|&x| uu_a2b_read(x, vm)) .collect::, _>>()?; while chunk.len() < 4 { chunk.push(0); diff --git a/crates/stdlib/src/blake2.rs b/crates/stdlib/src/blake2.rs index 382aec826b1..83504435674 100644 --- a/crates/stdlib/src/blake2.rs +++ b/crates/stdlib/src/blake2.rs @@ -5,7 +5,7 @@ pub(crate) use _blake2::module_def; #[pymodule] mod _blake2 { use crate::hashlib::_hashlib::{BlakeHashArgs, local_blake2b, local_blake2s}; - use crate::vm::{PyPayload, PyResult, VirtualMachine}; + use crate::vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule}; #[pyattr(name = "_GIL_MINSIZE")] const GIL_MINSIZE: u16 = 2048; @@ -43,4 +43,11 @@ mod _blake2 { fn blake2s(args: BlakeHashArgs, vm: &VirtualMachine) -> PyResult { Ok(local_blake2s(args, vm)?.into_pyobject(vm)) } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + let _ = vm.import("_hashlib", 0); + __module_exec(vm, module); + Ok(()) + } } diff --git a/crates/stdlib/src/bz2.rs b/crates/stdlib/src/bz2.rs index 575f33c4b8f..fcde54057e5 100644 --- a/crates/stdlib/src/bz2.rs +++ b/crates/stdlib/src/bz2.rs @@ -17,6 +17,7 @@ mod _bz2 { }; use alloc::fmt; use bzip2::{Decompress, Status, write::BzEncoder}; + use core::mem; use rustpython_vm::convert::ToPyException; use std::io::Write; @@ -155,7 +156,6 @@ mod _bz2 { } } - // TODO: return partial results from compress() instead of returning everything in flush() #[pyclass(with(Constructor))] impl BZ2Compressor { #[pymethod] @@ -165,12 +165,12 @@ mod _bz2 { return Err(vm.new_value_error("Compressor has been flushed")); } - // let CompressorState { flushed, encoder } = &mut *state; let CompressorState { encoder, .. } = &mut *state; - - // TODO: handle Err - data.with_ref(|input_bytes| encoder.as_mut().unwrap().write_all(input_bytes).unwrap()); - Ok(vm.ctx.new_bytes(Vec::new())) + let encoder = encoder.as_mut().unwrap(); + data.with_ref(|input_bytes| encoder.write_all(input_bytes).unwrap()); + // BzEncoder writes its pending output at the start of the next write. + assert_eq!(encoder.write(&[]).unwrap(), 0); + Ok(vm.ctx.new_bytes(mem::take(encoder.get_mut()))) } #[pymethod] diff --git a/crates/stdlib/src/contextvars.rs b/crates/stdlib/src/contextvars.rs index 0a6e0f12314..e3823f6ac59 100644 --- a/crates/stdlib/src/contextvars.rs +++ b/crates/stdlib/src/contextvars.rs @@ -15,16 +15,16 @@ mod _contextvars { AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, builtins::{PyGenericAlias, PyList, PyStrRef, PyType, PyTypeRef}, class::StaticType, - common::{hash::PyHash, lock::LazyLock, wtf8::Wtf8Buf}, + common::{ + hash::PyHash, + lock::{LazyLock, PyMutex}, + wtf8::Wtf8Buf, + }, function::{ArgCallable, FuncArgs, OptionalArg}, protocol::{PyMappingMethods, PySequenceMethods}, types::{AsMapping, AsSequence, Constructor, Hashable, Iterable, Representable}, }; - use core::{ - cell::{Cell, RefCell, UnsafeCell}, - sync::atomic::Ordering, - }; - use crossbeam_utils::atomic::AtomicCell; + use core::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use indexmap::IndexMap; // TODO: Real hamt implementation @@ -33,7 +33,7 @@ mod _contextvars { #[pyclass(no_attr, name = "Hamt", module = "contextvars")] #[derive(Debug, PyPayload)] pub(crate) struct HamtObject { - hamt: RefCell, + hamt: PyMutex, } #[pyclass] @@ -42,23 +42,19 @@ mod _contextvars { impl Default for HamtObject { fn default() -> Self { Self { - hamt: RefCell::new(Hamt::default()), + hamt: PyMutex::new(Hamt::default()), } } } - unsafe impl Sync for HamtObject {} - #[derive(Debug)] struct ContextInner { - idx: Cell, + idx: AtomicUsize, vars: PyRef, // PyObject *ctx_weakreflist; - entered: Cell, + entered: AtomicBool, } - unsafe impl Sync for ContextInner {} - #[pyattr] #[pyclass(name = "Context")] #[derive(Debug, PyPayload)] @@ -71,23 +67,30 @@ mod _contextvars { fn empty(vm: &VirtualMachine) -> Self { Self { inner: ContextInner { - idx: Cell::new(usize::MAX), + idx: AtomicUsize::new(usize::MAX), vars: HamtObject::default().into_ref(&vm.ctx), - entered: Cell::new(false), + entered: AtomicBool::new(false), }, } } - fn borrow_vars(&self) -> impl core::ops::Deref + '_ { - self.inner.vars.hamt.borrow() + fn borrow_vars(&self) -> impl core::ops::DerefMut + '_ { + self.inner.vars.hamt.lock() } fn borrow_vars_mut(&self) -> impl core::ops::DerefMut + '_ { - self.inner.vars.hamt.borrow_mut() + self.inner.vars.hamt.lock() } fn enter(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { - if zelf.inner.entered.get() { + // A context is entered by one thread at a time, so the check and the + // claim have to be a single step. + if zelf + .inner + .entered + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { return Err(vm.new_runtime_error(format!( "cannot enter context: {} is already entered", zelf.as_object().repr(vm)? @@ -95,16 +98,15 @@ mod _contextvars { } super::CONTEXTS.with_borrow_mut(|ctxs| { - zelf.inner.idx.set(ctxs.len()); + zelf.inner.idx.store(ctxs.len(), Ordering::Relaxed); ctxs.push(zelf.to_owned()); }); - zelf.inner.entered.set(true); Ok(()) } fn exit(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { - if !zelf.inner.entered.get() { + if !zelf.inner.entered.load(Ordering::Acquire) { return Err(vm.new_runtime_error(format!( "cannot exit context: {} is not entered", zelf.as_object().repr(vm)? @@ -120,7 +122,7 @@ mod _contextvars { ) }) })?; - zelf.inner.entered.set(false); + zelf.inner.entered.store(false, Ordering::Release); Ok(()) } @@ -131,8 +133,8 @@ mod _contextvars { ctx.clone() } else { let ctx = Self::empty(vm); - ctx.inner.idx.set(0); - ctx.inner.entered.set(true); + ctx.inner.idx.store(0, Ordering::Relaxed); + ctx.inner.entered.store(true, Ordering::Release); let ctx = ctx.into_ref(&vm.ctx); ctxs.push(ctx); ctxs[0].clone() @@ -170,13 +172,13 @@ mod _contextvars { fn copy(&self, vm: &VirtualMachine) -> Self { // Deep copy the vars - clone the underlying Hamt data, not just the PyRef let vars_copy = HamtObject { - hamt: RefCell::new(self.inner.vars.hamt.borrow().clone()), + hamt: PyMutex::new(self.inner.vars.hamt.lock().clone()), }; Self { inner: ContextInner { - idx: Cell::new(usize::MAX), + idx: AtomicUsize::new(usize::MAX), vars: vars_copy.into_ref(&vm.ctx), - entered: Cell::new(false), + entered: AtomicBool::new(false), }, } } @@ -186,11 +188,8 @@ mod _contextvars { var: PyRef, vm: &VirtualMachine, ) -> PyResult { - let vars = self.borrow_vars(); - let item = vars - .get(&*var) - .ok_or_else(|| vm.new_key_error(var.into()))?; - Ok(item.to_owned()) + let item = self.borrow_vars().get(&*var).map(|item| item.to_owned()); + item.ok_or_else(|| vm.new_key_error(var.into())) } fn __len__(&self) -> usize { @@ -290,11 +289,11 @@ mod _contextvars { name: String, default: Option, #[pytraverse(skip)] - cached: AtomicCell>, + cached: PyMutex>, #[pytraverse(skip)] - cached_id: core::sync::atomic::AtomicUsize, // cached_tsid in CPython + cached_id: AtomicUsize, // cached_tsid in CPython #[pytraverse(skip)] - hash: UnsafeCell, + hash: AtomicI64, } impl core::fmt::Debug for ContextVar { @@ -303,8 +302,6 @@ mod _contextvars { } } - unsafe impl Sync for ContextVar {} - impl PartialEq for ContextVar { fn eq(&self, other: &Self) -> bool { core::ptr::eq(self, other) @@ -320,12 +317,15 @@ mod _contextvars { impl ContextVar { fn delete(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { - zelf.cached.store(None); + let cached = zelf.cached.lock().take(); + drop(cached); let ctx = PyContext::current(vm); - let mut vars = ctx.borrow_vars_mut(); - if vars.swap_remove(zelf).is_none() { + let removed = ctx.borrow_vars_mut().swap_remove(zelf); + let existed = removed.is_some(); + drop(removed); + if !existed { // TODO: // PyErr_SetObject(PyExc_LookupError, (PyObject *)var); return Err(vm.new_lookup_error(zelf.as_object().repr(vm)?.as_wtf8().to_owned())); @@ -338,16 +338,17 @@ mod _contextvars { fn set_inner(zelf: &Py, value: PyObjectRef, vm: &VirtualMachine) { let ctx = PyContext::current(vm); - let mut vars = ctx.borrow_vars_mut(); - vars.insert(zelf.to_owned(), value.clone()); + let replaced = ctx.borrow_vars_mut().insert(zelf.to_owned(), value.clone()); + drop(replaced); zelf.cached_id.store(ctx.get_id(), Ordering::SeqCst); let cache = ContextVarCache { object: value, - idx: ctx.inner.idx.get(), + idx: ctx.inner.idx.load(Ordering::Relaxed), }; - zelf.cached.store(Some(cache)); + let replaced = zelf.cached.lock().replace(cache); + drop(replaced); } fn generate_hash(zelf: &Py, vm: &VirtualMachine) -> PyHash { @@ -370,28 +371,32 @@ mod _contextvars { default: OptionalArg, vm: &VirtualMachine, ) -> PyResult> { - let found = super::CONTEXTS.with_borrow(|ctxs| { - let ctx = ctxs.last()?; - let cached_ptr = zelf.cached.as_ptr(); - debug_assert!(!cached_ptr.is_null()); - if let Some(cached) = unsafe { &*cached_ptr } + // The replaced cache entry comes back out so that dropping it, which + // can run a __del__ that calls back in, happens with no lock held. + let (found, replaced) = super::CONTEXTS.with_borrow(|ctxs| { + let Some(ctx) = ctxs.last() else { + return (None, None); + }; + let mut cached = zelf.cached.lock(); + if let Some(cached) = &*cached && zelf.cached_id.load(Ordering::SeqCst) == ctx.get_id() && cached.idx + 1 == ctxs.len() { - return Some(cached.object.clone()); + return (Some(cached.object.clone()), None); } - let vars = ctx.borrow_vars(); - let obj = vars.get(zelf)?; + let Some(obj) = ctx.borrow_vars().get(zelf).map(|obj| obj.to_owned()) else { + return (None, None); + }; zelf.cached_id.store(ctx.get_id(), Ordering::SeqCst); - // TODO: ensure cached is not changed - let _removed = zelf.cached.swap(Some(ContextVarCache { + let replaced = cached.replace(ContextVarCache { object: obj.clone(), idx: ctxs.len() - 1, - })); + }); - Some(obj.clone()) + (Some(obj), replaced) }); + drop(replaced); let value = if let Some(value) = found { value @@ -425,7 +430,7 @@ mod _contextvars { #[pymethod] fn reset(zelf: &Py, token: PyRef, vm: &VirtualMachine) -> PyResult<()> { - if token.used.get() { + if token.used.load(Ordering::Acquire) { return Err(vm.new_runtime_error(format!( "{} has already been used once", token.as_object().repr(vm)? @@ -447,7 +452,7 @@ mod _contextvars { ))); } - token.used.set(true); + token.used.store(true, Ordering::Release); if let Some(old_value) = &token.old_value { Self::set_inner(zelf, old_value.clone(), vm); @@ -462,7 +467,7 @@ mod _contextvars { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -484,15 +489,13 @@ mod _contextvars { name: args.name.to_string(), default: args.default.into_option(), cached_id: 0.into(), - cached: AtomicCell::new(None), - hash: UnsafeCell::new(0), + cached: PyMutex::new(None), + hash: AtomicI64::new(0), }; let py_var = var.into_ref_with_type(vm, cls)?; - unsafe { - // SAFETY: py_var is not exposed to python memory model yet - *py_var.hash.get() = Self::generate_hash(&py_var, vm) - }; + let hash = Self::generate_hash(&py_var, vm); + py_var.hash.store(hash, Ordering::Relaxed); Ok(py_var.into()) } @@ -504,14 +507,14 @@ mod _contextvars { impl core::hash::Hash for ContextVar { #[inline] fn hash(&self, state: &mut H) { - unsafe { *self.hash.get() }.hash(state) + self.hash.load(Ordering::Relaxed).hash(state) } } impl Hashable for ContextVar { #[inline] fn hash(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok(unsafe { *zelf.hash.get() }) + Ok(zelf.hash.load(Ordering::Relaxed)) } } @@ -537,11 +540,9 @@ mod _contextvars { ctx: PyRef, // tok_ctx in CPython var: PyRef, // tok_var in CPython old_value: Option, // tok_oldval in CPython - used: Cell, + used: AtomicBool, } - unsafe impl Sync for ContextToken {} - #[pyclass(with(Constructor, Representable))] impl ContextToken { #[pygetset] @@ -562,7 +563,7 @@ mod _contextvars { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } @@ -598,7 +599,11 @@ mod _contextvars { impl Representable for ContextToken { #[inline] fn repr_wtf8(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let used = if zelf.used.get() { " used" } else { "" }; + let used = if zelf.used.load(Ordering::Acquire) { + " used" + } else { + "" + }; let var = Representable::repr_wtf8(&zelf.var, vm)?; let ptr = zelf.as_object().get_id() as *const u8; let mut result = Wtf8Buf::from(format!(", escapechar: Option, doublequote: bool, skipinitialspace: bool, - lineterminator: csv_core::Terminator, + lineterminator: String, quoting: QuoteStyle, strict: bool, } + /// Placeholder single-byte terminator for the csv-core writer paths + /// (`QUOTE_ALL` / `QUOTE_NONNUMERIC`). csv-core can only emit a single byte + /// for the record terminator, but its `terminator()` call also performs + /// essential bookkeeping — closing the final quote and emitting `""` for an + /// empty record — that must not be bypassed. So the writer emits this + /// sentinel byte, and `writerow` strips it and appends the real (possibly + /// multi-character) line terminator afterwards. + const CSV_CORE_TERMINATOR_SENTINEL: u8 = b'\n'; + impl Constructor for PyDialect { type Args = PyObjectRef; @@ -106,11 +129,7 @@ mod _csv { #[pygetset] fn lineterminator(&self, vm: &VirtualMachine) -> PyRef { - match self.lineterminator { - Terminator::CRLF => vm.ctx.new_str("\r\n".to_string()), - Terminator::Any(t) => vm.ctx.new_str(format!("{}", t as char)), - _ => unreachable!(), - } + vm.ctx.new_str(self.lineterminator.clone()) } #[pygetset] @@ -215,19 +234,39 @@ mod _csv { }) } - fn prase_lineterminator_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult { + /// Validate that a line terminator is ASCII and return it as a `str`. + /// + /// The writer's quoting and escaping predicates compare raw bytes, so a + /// non-ASCII terminator would either quote a field that merely shares a + /// UTF-8 lead byte or splice an escape character into the middle of a + /// multi-byte sequence. Reject those here. + /// + /// The ASCII check must come before any UTF-8 conversion so that lone + /// surrogates are reported as this `csv.Error` too. + /// + /// TODO: RUSTPYTHON; handle non-ASCII terminators code-point-wise as part + /// of full Unicode dialect support. + fn ascii_lineterminator<'a>(vm: &VirtualMachine, s: &'a PyStr) -> PyResult<&'a str> { + if !s.as_wtf8().is_ascii() { + return Err(new_csv_error( + vm, + r#""lineterminator" must be an ASCII string"#, + )); + } + // An ASCII string is always valid UTF-8. + s.to_str() + .ok_or_else(|| new_csv_error(vm, r#""lineterminator" must be a string"#)) + } + + fn prase_lineterminator_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult { match_class!(match obj.get_attr("lineterminator", vm)? { s @ PyStr => { - Ok(if s.as_bytes().eq(b"\r\n") { - csv_core::Terminator::CRLF - } else if let Some(t) = s.as_bytes().first() { - // Due to limitations in the current implementation within csv_core - // the support for multiple characters in lineterminator is not complete. - // only capture the first character - csv_core::Terminator::Any(*t) - } else { - return Err(new_csv_error(vm, r#""lineterminator" must be a string"#)); - }) + // Store the full line terminator string. CPython accepts an + // arbitrary-length terminator; the manual writer paths emit it + // verbatim and the csv-core writer path appends it after a + // sentinel terminator (see `writerow`). + let value = ascii_lineterminator(vm, &s)?; + Ok(value.to_owned()) } attr => { Err(vm.new_type_error(format!( @@ -329,7 +368,7 @@ mod _csv { let g = GLOBAL_HASHMAP.lock(); if let Some(dialect) = g.get(name.as_str()) { - return Ok(*dialect); + return Ok(dialect.clone()); } Err(new_csv_error(vm, "unknown dialect")) @@ -405,12 +444,8 @@ mod _csv { Ok(Reader { iter, state: PyMutex::new(ReadState { - buffer: vec![0; 1024], - output_ends: vec![0; 16], - reader: options.to_reader(), - skipinitialspace: options.get_skipinitialspace(), - delimiter: options.get_delimiter(), line_num: 0, + generation: 0, }), dialect: options.result(vm)?, }) @@ -462,12 +497,11 @@ mod _csv { impl From for csv_core::QuoteStyle { fn from(val: QuoteStyle) -> Self { match val { - QuoteStyle::Minimal => Self::Always, + QuoteStyle::Minimal => Self::Necessary, QuoteStyle::All => Self::Always, QuoteStyle::Nonnumeric => Self::NonNumeric, QuoteStyle::None => Self::Never, - QuoteStyle::Strings => todo!(), - QuoteStyle::Notnull => todo!(), + QuoteStyle::Strings | QuoteStyle::Notnull => Self::Necessary, } } } @@ -526,7 +560,7 @@ mod _csv { escapechar: Option, doublequote: Option, skipinitialspace: Option, - lineterminator: Option, + lineterminator: Option, quoting: Option, strict: Option, } @@ -615,15 +649,14 @@ mod _csv { }; if let Some(lineterminator) = args.kwargs.swap_remove("lineterminator") { - res.lineterminator = Some(csv_core::Terminator::Any( - lineterminator - .try_to_value::<&str>(vm)? - .bytes() - .exactly_one() - .map_err(|_| { - vm.new_type_error(r#""lineterminator" must be a 1-character string"#) - })?, - )) + let s = lineterminator.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + r#""lineterminator" must be a string, not {}"#, + lineterminator.class().name() + )) + })?; + let value = ascii_lineterminator(vm, s)?; + res.lineterminator = Some(value.to_owned()); }; if let Some(doublequote) = args.kwargs.swap_remove("doublequote") { @@ -661,7 +694,10 @@ mod _csv { |_| { vm.new_type_error(r#""quotechar" must be a 1-character string"#) } )?)), PyNone => { - if let Some(QuoteStyle::All) = res.quoting { + if res + .quoting + .is_some_and(|quoting| quoting != QuoteStyle::None) + { return Err(ArgumentError::Exception( vm.new_type_error("quotechar must be set if quoting enabled"), )); @@ -700,7 +736,7 @@ mod _csv { } impl FormatOptions { - const fn update_py_dialect(&self, mut res: PyDialect) -> PyDialect { + fn update_py_dialect(&self, mut res: PyDialect) -> PyDialect { macro_rules! check_and_fill { ($res:ident, $e:ident) => {{ if let Some(t) = self.$e { @@ -724,7 +760,9 @@ mod _csv { }; check_and_fill!(res, quoting); - check_and_fill!(res, lineterminator); + if let Some(t) = &self.lineterminator { + res.lineterminator.clone_from(t); + }; check_and_fill!(res, strict); res } @@ -734,137 +772,45 @@ mod _csv { DialectItem::Str(name) => { let g = GLOBAL_HASHMAP.lock(); if let Some(dialect) = g.get(name) { - Ok(self.update_py_dialect(*dialect)) + Ok(self.update_py_dialect(dialect.clone())) } else { Err(new_csv_error(vm, format!("{name} is not registered."))) } // TODO: Maybe need to update the obj from HashMap } - DialectItem::Obj(o) => Ok(self.update_py_dialect(*o)), - DialectItem::None => { - let g = GLOBAL_HASHMAP.lock(); - let res = *g.get("excel").unwrap(); - Ok(self.update_py_dialect(res)) - } - } - } - - fn get_skipinitialspace(&self) -> bool { - let mut skipinitialspace = match &self.dialect { - DialectItem::Str(name) => { - let g = GLOBAL_HASHMAP.lock(); - if let Some(dialect) = g.get(name) { - dialect.skipinitialspace - // TODO: RUSTPYTHON; Perfecting the remaining attributes. - } else { - false - } - } - DialectItem::Obj(obj) => obj.skipinitialspace, - _ => false, - }; - - if let Some(attr) = self.skipinitialspace { - skipinitialspace = attr + DialectItem::Obj(o) => Ok(self.update_py_dialect(o.clone())), + DialectItem::None => Ok(self.update_py_dialect(PyDialect { + delimiter: b',', + quotechar: Some(b'"'), + escapechar: None, + doublequote: true, + skipinitialspace: false, + lineterminator: "\r\n".to_owned(), + quoting: QuoteStyle::Minimal, + strict: false, + })), } - - skipinitialspace } - fn get_delimiter(&self) -> u8 { - let mut delimiter = match &self.dialect { + fn get_quoting(&self) -> QuoteStyle { + let mut quoting = match &self.dialect { DialectItem::Str(name) => { let g = GLOBAL_HASHMAP.lock(); if let Some(dialect) = g.get(name) { - dialect.delimiter - // RustPython todo - // todo! Perfecting the remaining attributes. + dialect.quoting } else { - b',' + QuoteStyle::Minimal } } - DialectItem::Obj(obj) => obj.delimiter, - _ => b',', + DialectItem::Obj(obj) => obj.quoting, + _ => QuoteStyle::Minimal, }; - if let Some(attr) = self.delimiter { - delimiter = attr + if let Some(attr) = self.quoting { + quoting = attr } - delimiter - } - - fn to_reader(&self) -> csv_core::Reader { - let mut builder = csv_core::ReaderBuilder::new(); - let mut reader = match &self.dialect { - DialectItem::Str(name) => { - let g = GLOBAL_HASHMAP.lock(); - if let Some(dialect) = g.get(name) { - let mut builder = builder - .delimiter(dialect.delimiter) - .double_quote(dialect.doublequote); - if let Some(t) = dialect.quotechar { - builder = builder.quote(t); - } - builder - // RustPython todo - // todo! Perfecting the remaining attributes. - } else { - &mut builder - } - } - DialectItem::Obj(obj) => { - let mut builder = builder - .delimiter(obj.delimiter) - .double_quote(obj.doublequote); - if let Some(t) = obj.quotechar { - builder = builder.quote(t); - } - builder - } - _ => { - let name = "excel"; - let g = GLOBAL_HASHMAP.lock(); - let dialect = g.get(name).unwrap(); - let mut builder = builder - .delimiter(dialect.delimiter) - .double_quote(dialect.doublequote); - if let Some(quotechar) = dialect.quotechar { - builder = builder.quote(quotechar); - } - builder - } - }; - - if let Some(t) = self.delimiter { - reader = reader.delimiter(t); - } - - if let Some(t) = self.quotechar { - reader = if let Some(u) = t { - reader.quote(u) - } else { - reader.quoting(false) - } - } else { - reader = reader.quoting(self.quoting != Some(QuoteStyle::None)); - } - - if let Some(t) = self.lineterminator { - reader = reader.terminator(t); - } - - if let Some(t) = self.doublequote { - reader = reader.double_quote(t); - } - - if self.escapechar.is_some() { - reader = reader.escape(self.escapechar); - } - - reader = reader.terminator(self.lineterminator.unwrap_or(Terminator::CRLF)); - - reader.build() + quoting } fn to_writer(&self) -> csv_core::Writer { @@ -875,8 +821,7 @@ mod _csv { if let Some(dialect) = g.get(name) { let mut builder = builder .delimiter(dialect.delimiter) - .double_quote(dialect.doublequote) - .terminator(dialect.lineterminator); + .double_quote(dialect.doublequote); if let Some(t) = dialect.quotechar { builder = builder.quote(t); @@ -892,8 +837,7 @@ mod _csv { DialectItem::Obj(obj) => { let mut builder = builder .delimiter(obj.delimiter) - .double_quote(obj.doublequote) - .terminator(obj.lineterminator); + .double_quote(obj.doublequote); if let Some(t) = obj.quotechar { builder = builder.quote(t); @@ -908,39 +852,29 @@ mod _csv { writer = writer.delimiter(t); } - if let Some(t) = self.quotechar { - if let Some(u) = t { - writer = writer.quote(u); - } else { - todo!() - } + if let Some(Some(t)) = self.quotechar { + writer = writer.quote(t); } if let Some(t) = self.doublequote { writer = writer.double_quote(t); } - writer = writer.terminator(self.lineterminator.unwrap_or(Terminator::CRLF)); + writer = writer.terminator(Terminator::Any(CSV_CORE_TERMINATOR_SENTINEL)); if let Some(e) = self.escapechar { writer = writer.escape(e); } - if let Some(e) = self.quoting { - writer = writer.quote_style(e.into()); - } + writer = writer.quote_style(self.get_quoting().into()); writer.build() } } struct ReadState { - buffer: Vec, - output_ends: Vec, - reader: csv_core::Reader, - skipinitialspace: bool, - delimiter: u8, line_num: u64, + generation: u64, } #[pyclass(no_attr, module = "_csv", name = "reader", traverse)] @@ -967,129 +901,325 @@ mod _csv { } #[pygetset] - const fn dialect(&self, _vm: &VirtualMachine) -> PyDialect { - self.dialect + fn dialect(&self, _vm: &VirtualMachine) -> PyDialect { + self.dialect.clone() } } impl SelfIter for Reader {} - impl IterNext for Reader { - fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let string = raise_if_stop!(zelf.iter.next(vm)?); - let string = string.downcast::().map_err(|obj| { - new_csv_error( - vm, - format!( - "iterator should return strings, not {} (the file should be opened in text mode)", - obj.class().name() - ), - ) - })?; - let input = string.as_bytes(); - if input.is_empty() || input.starts_with(b"\n") { - return Ok(PyIterReturn::Return(vm.ctx.new_list(vec![]).into())); + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum ParserState { + StartRecord, + StartField, + EscapedChar, + InField, + InQuotedField, + EscapeInQuotedField, + QuoteInQuotedField, + EatCrnl, + AfterEscapedCrnl, + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum ParserInput { + Byte(u8), + Eol, + } + + const EOL: ParserInput = ParserInput::Eol; + + struct CsvParser { + state: ParserState, + fields: Vec, + field: Vec, + unquoted_field: bool, + field_limit: isize, + } + + impl CsvParser { + fn new(field_limit: isize) -> Self { + Self { + state: ParserState::StartRecord, + fields: Vec::new(), + field: Vec::new(), + unquoted_field: false, + field_limit, } - let mut state = zelf.state.lock(); - let ReadState { - buffer, - output_ends, - reader, - skipinitialspace, - delimiter, - line_num, - } = &mut *state; - - let mut input_offset = 0; - let mut output_offset = 0; - let mut output_ends_offset = 0; - let field_limit = GLOBAL_FIELD_LIMIT.lock().to_owned(); - - #[inline] - fn trim_spaces(input: &[u8]) -> &[u8] { - let trimmed_start = input.iter().position(|&x| x != b' ').unwrap_or(input.len()); - let trimmed_end = input.iter().rposition(|&x| x != b' ').map_or(0, |i| i + 1); - &input[trimmed_start..trimmed_end] + } + + fn into_result(self, vm: &VirtualMachine) -> PyIterReturn { + PyIterReturn::Return(vm.ctx.new_list(self.fields).into()) + } + + fn add_byte(&mut self, byte: u8, vm: &VirtualMachine) -> PyResult<()> { + if self.field_limit < 0 || self.field.len() >= self.field_limit as usize { + return Err(new_csv_error( + vm, + format!("field larger than field limit ({})", self.field_limit), + )); } + self.field.push(byte); + Ok(()) + } - let input = if *skipinitialspace { - let t = input.split(|x| x == delimiter); - t.map(|x| { - let trimmed = trim_spaces(x); - String::from_utf8(trimmed.to_vec()).unwrap() - }) - .join(format!("{}", *delimiter as char).as_str()) + fn save_field(&mut self, quoting: QuoteStyle, vm: &VirtualMachine) -> PyResult<()> { + let field = if self.unquoted_field + && self.field.is_empty() + && matches!(quoting, QuoteStyle::Notnull | QuoteStyle::Strings) + { + vm.ctx.none() } else { - String::from_utf8(input.to_vec()).unwrap() + let value = core::str::from_utf8(&self.field) + .map_err(|e| new_not_utf8_error(vm, &self.field, e))?; + let field: PyObjectRef = vm.ctx.new_str(value).into(); + if self.unquoted_field + && !self.field.is_empty() + && matches!(quoting, QuoteStyle::Nonnumeric | QuoteStyle::Strings) + { + PyType::call(vm.ctx.types.float_type, vec![field].into(), vm)? + } else { + field + } }; + self.fields.push(field); + self.field.clear(); + Ok(()) + } - loop { - let (res, n_read, n_written, n_ends) = reader.read_record( - &input.as_bytes()[input_offset..], - &mut buffer[output_offset..], - &mut output_ends[output_ends_offset..], - ); - input_offset += n_read; - output_offset += n_written; - output_ends_offset += n_ends; - match res { - csv_core::ReadRecordResult::InputEmpty => {} - csv_core::ReadRecordResult::OutputFull => resize_buf(buffer), - csv_core::ReadRecordResult::OutputEndsFull => resize_buf(output_ends), - csv_core::ReadRecordResult::Record => break, - csv_core::ReadRecordResult::End => { - return Ok(PyIterReturn::StopIteration(None)); + fn process_parser_input( + &mut self, + input: ParserInput, + dialect: &PyDialect, + vm: &VirtualMachine, + ) -> PyResult<()> { + match self.state { + ParserState::StartRecord => match input { + ParserInput::Eol => {} + ParserInput::Byte(b'\r' | b'\n') => self.state = ParserState::EatCrnl, + _ => { + self.state = ParserState::StartField; + return self.process_parser_input(input, dialect, vm); + } + }, + ParserState::StartField => { + self.unquoted_field = true; + match input { + ParserInput::Eol | ParserInput::Byte(b'\r' | b'\n') => { + self.save_field(dialect.quoting, vm)?; + self.state = state_after_record_end(input); + } + ParserInput::Byte(byte) + if dialect.quoting != QuoteStyle::None + && dialect.quotechar == Some(byte) => + { + self.unquoted_field = false; + self.state = ParserState::InQuotedField; + } + ParserInput::Byte(byte) if dialect.escapechar == Some(byte) => { + self.state = ParserState::EscapedChar; + } + ParserInput::Byte(b' ') if dialect.skipinitialspace => {} + ParserInput::Byte(byte) if byte == dialect.delimiter => { + self.save_field(dialect.quoting, vm)?; + } + ParserInput::Byte(byte) => { + self.add_byte(byte, vm)?; + self.state = ParserState::InField; + } + } + } + ParserState::EscapedChar => match input { + ParserInput::Byte(byte @ (b'\r' | b'\n')) => { + self.add_byte(byte, vm)?; + self.state = ParserState::AfterEscapedCrnl; + } + ParserInput::Eol => { + self.add_byte(b'\n', vm)?; + self.state = ParserState::InField; + } + ParserInput::Byte(byte) => { + self.add_byte(byte, vm)?; + self.state = ParserState::InField; + } + }, + ParserState::AfterEscapedCrnl => { + if input != ParserInput::Eol { + self.state = ParserState::InField; + return self.process_parser_input(input, dialect, vm); + } + } + ParserState::InField => match input { + ParserInput::Eol | ParserInput::Byte(b'\r' | b'\n') => { + self.save_field(dialect.quoting, vm)?; + self.state = state_after_record_end(input); + } + ParserInput::Byte(byte) if dialect.escapechar == Some(byte) => { + self.state = ParserState::EscapedChar; + } + ParserInput::Byte(byte) if byte == dialect.delimiter => { + self.save_field(dialect.quoting, vm)?; + self.state = ParserState::StartField; } + ParserInput::Byte(byte) => self.add_byte(byte, vm)?, + }, + ParserState::InQuotedField => match input { + ParserInput::Eol => {} + ParserInput::Byte(byte) if dialect.escapechar == Some(byte) => { + self.state = ParserState::EscapeInQuotedField; + } + ParserInput::Byte(byte) + if dialect.quoting != QuoteStyle::None + && dialect.quotechar == Some(byte) => + { + self.state = if dialect.doublequote { + ParserState::QuoteInQuotedField + } else { + ParserState::InField + }; + } + ParserInput::Byte(byte) => self.add_byte(byte, vm)?, + }, + ParserState::EscapeInQuotedField => { + let byte = match input { + ParserInput::Eol => b'\n', + ParserInput::Byte(byte) => byte, + }; + self.add_byte(byte, vm)?; + self.state = ParserState::InQuotedField; } + ParserState::QuoteInQuotedField => match input { + ParserInput::Byte(byte) + if dialect.quoting != QuoteStyle::None + && dialect.quotechar == Some(byte) => + { + self.add_byte(byte, vm)?; + self.state = ParserState::InQuotedField; + } + ParserInput::Byte(byte) if byte == dialect.delimiter => { + self.save_field(dialect.quoting, vm)?; + self.state = ParserState::StartField; + } + ParserInput::Eol | ParserInput::Byte(b'\r' | b'\n') => { + self.save_field(dialect.quoting, vm)?; + self.state = state_after_record_end(input); + } + ParserInput::Byte(byte) if !dialect.strict => { + self.add_byte(byte, vm)?; + self.state = ParserState::InField; + } + ParserInput::Byte(_) => { + return Err(new_csv_error( + vm, + format!( + "'{}' expected after '{}'", + dialect.delimiter as char, + dialect.quotechar.unwrap_or_default() as char, + ), + )); + } + }, + ParserState::EatCrnl => match input { + ParserInput::Byte(b'\r' | b'\n') => {} + ParserInput::Eol => self.state = ParserState::StartRecord, + ParserInput::Byte(_) => { + return Err(new_csv_error( + vm, + concat!( + "new-line character seen in unquoted field - ", + "do you need to open the file with newline=''?" + ), + )); + } + }, } + Ok(()) + } + } - let rest = &input.as_bytes()[input_offset..]; - if !rest.iter().all(|&c| matches!(c, b'\r' | b'\n')) { - return Err(new_csv_error( - vm, - concat!( - "new-line character seen in unquoted field", - " - do you need to open the file in universal-newline mode?" - ), - )); - } + fn state_after_record_end(input: ParserInput) -> ParserState { + if input == ParserInput::Eol { + ParserState::StartRecord + } else { + ParserState::EatCrnl + } + } - let mut prev_end = 0; - let out: Vec = output_ends[..output_ends_offset] - .iter() - .map(|&end| { - let range = prev_end..end; - if range.len() > field_limit as usize { - return Err(new_csv_error(vm, "filed too long to read")); - } + fn next_input_item(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let generation = zelf.state.lock().generation; + // Advancing user code may re-enter this reader, so do not hold its lock here. + let result = zelf.iter.next(vm)?; + let mut state = zelf.state.lock(); + if state.generation != generation { + return Err(new_csv_error( + vm, + "iterator has already advanced the reader", + )); + } + if matches!(result, PyIterReturn::Return(_)) { + state.generation += 1; + } + Ok(result) + } - prev_end = end; - let s = core::str::from_utf8(&buffer[range.clone()]) - // not sure if this is possible - the input was all strings - .map_err(|_e| vm.new_unicode_decode_error("csv not utf8"))?; + fn finish_at_true_eof( + mut parser: CsvParser, + dialect: &PyDialect, + vm: &VirtualMachine, + ) -> PyResult { + let has_unfinished_record = + !parser.field.is_empty() || parser.state == ParserState::InQuotedField; + if !has_unfinished_record { + return Ok(PyIterReturn::StopIteration(None)); + } + if dialect.strict { + return Err(new_csv_error(vm, "unexpected end of data")); + } + parser.save_field(dialect.quoting, vm)?; + Ok(parser.into_result(vm)) + } - // TODO: RUSTPYTHON; Incomplete implementation - if let QuoteStyle::Nonnumeric = zelf.dialect.quoting { - if let Ok(t) = String::from_utf8(trim_spaces(&buffer[range]).to_vec()) - .unwrap() - .parse::() - { - Ok(vm.ctx.new_int(t).into()) - } else { - Ok(vm.ctx.new_str(s).into()) + impl IterNext for Reader { + fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let mut parser = CsvParser::new(*GLOBAL_FIELD_LIMIT.lock()); + + loop { + match next_input_item(zelf, vm)? { + PyIterReturn::Return(obj) => { + let string = obj.downcast::().map_err(|obj| { + new_csv_error( + vm, + format!( + concat!( + "iterator should return strings, not {} ", + "(the file should be opened in text mode)" + ), + obj.class().name() + ), + ) + })?; + + zelf.state.lock().line_num += 1; + parser.field_limit = *GLOBAL_FIELD_LIMIT.lock(); + for &byte in string.as_bytes() { + parser.process_parser_input( + ParserInput::Byte(byte), + &zelf.dialect, + vm, + )?; + } + + // Virtual EOL marks an iterator-item boundary, not true EOF. + parser.process_parser_input(EOL, &zelf.dialect, vm)?; + if parser.state == ParserState::StartRecord { + return Ok(parser.into_result(vm)); } - } else { - Ok(vm.ctx.new_str(s).into()) } - }) - .collect::>()?; - // Removes the last null item before the line terminator, if there is a separator before the line terminator, - // todo! - // if out.last().unwrap().length(vm).unwrap() == 0 { - // out.pop(); - // } - *line_num += 1; - Ok(PyIterReturn::Return(vm.ctx.new_list(out).into())) + PyIterReturn::StopIteration(_) => { + return finish_at_true_eof(parser, &zelf.dialect, vm); + } + } + } } } @@ -1114,15 +1244,247 @@ mod _csv { } } + fn write_quoted_field( + output: &mut Vec, + data: &[u8], + dialect: &PyDialect, + vm: &VirtualMachine, + ) -> PyResult<()> { + let quotechar = dialect + .quotechar + .ok_or_else(|| vm.new_type_error("quotechar must be set if quoting enabled"))?; + output.push(quotechar); + for &byte in data { + if byte == quotechar { + if dialect.doublequote { + output.push(quotechar); + output.push(quotechar); + } else if let Some(escapechar) = dialect.escapechar { + output.push(escapechar); + output.push(byte); + } else { + return Err(new_csv_error(vm, "need to escape, but no escapechar set")); + } + } else { + if dialect.escapechar == Some(byte) { + output.push(byte); + } + output.push(byte); + } + } + output.push(quotechar); + Ok(()) + } + + fn write_unquoted_field( + output: &mut Vec, + data: &[u8], + dialect: &PyDialect, + vm: &VirtualMachine, + ) -> PyResult<()> { + for &byte in data { + if field_needs_escape(byte, dialect) { + let escapechar = dialect + .escapechar + .ok_or_else(|| new_csv_error(vm, "need to escape, but no escapechar set"))?; + output.push(escapechar); + } + output.push(byte); + } + Ok(()) + } + + fn field_needs_quotes(data: &[u8], dialect: &PyDialect) -> bool { + data.iter().any(|&byte| { + byte == dialect.delimiter + || dialect.quotechar == Some(byte) + || matches!(byte, b'\r' | b'\n') + // CPython quotes a field containing any character of the line + // terminator. The terminator is ASCII-validated at parse time, so + // comparing raw bytes cannot match part of a multi-byte character. + // TODO: RUSTPYTHON; supporting non-ASCII terminators needs + // code-point-wise quoting and escaping as part of full + // Unicode dialect support. + || dialect.lineterminator.as_bytes().contains(&byte) + }) + } + + fn field_needs_escape(byte: u8, dialect: &PyDialect) -> bool { + byte == dialect.delimiter + || dialect.quotechar == Some(byte) + || dialect.escapechar == Some(byte) + || matches!(byte, b'\r' | b'\n') + || dialect.lineterminator.as_bytes().contains(&byte) + } + + fn write_lineterminator(output: &mut Vec, terminator: &str) { + output.extend_from_slice(terminator.as_bytes()); + } + #[pyclass(flags(DISALLOW_INSTANTIATION))] impl Writer { #[pygetset(name = "dialect")] - const fn get_dialect(&self, _vm: &VirtualMachine) -> PyDialect { - self.dialect + fn get_dialect(&self, _vm: &VirtualMachine) -> PyDialect { + self.dialect.clone() + } + + fn writerow_quoted_strings(&self, row: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let _state = self.state.lock(); + let row: ArgIterable = ArgIterable::try_from_object(vm, row.clone()).map_err(|_e| { + new_csv_error( + vm, + format!("'{}' object is not iterable", row.class().name()), + ) + })?; + let fields = row.iter(vm)?.collect::>>()?; + let single_field = fields.len() == 1; + let mut output = Vec::new(); + + for (index, field) in fields.into_iter().enumerate() { + if index > 0 { + output.push(self.dialect.delimiter); + } + + let stringified; + let (data, is_str, is_none): (&[u8], bool, bool) = match_class!(match field { + ref s @ PyStr => (s.as_bytes(), true, false), + crate::builtins::PyNone => (b"", false, true), + ref obj => { + stringified = obj.str(vm)?; + (stringified.as_bytes(), false, false) + } + }); + + let should_quote = match self.dialect.quoting { + QuoteStyle::Strings => is_str || field_needs_quotes(data, &self.dialect), + QuoteStyle::Notnull => !is_none, + _ => unreachable!(), + }; + if should_quote { + write_quoted_field(&mut output, data, &self.dialect, vm)?; + } else if single_field && data.is_empty() { + return Err(new_csv_error( + vm, + "single empty field record must be quoted", + )); + } else { + output.extend_from_slice(data); + } + } + + write_lineterminator(&mut output, &self.dialect.lineterminator); + let s = + core::str::from_utf8(&output).map_err(|e| new_not_utf8_error(vm, &output, e))?; + self.write.call((s,), vm) + } + + fn writerow_quote_none(&self, row: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let _state = self.state.lock(); + + let row: ArgIterable = ArgIterable::try_from_object(vm, row.clone()).map_err(|_e| { + new_csv_error( + vm, + format!("'{}' object is not iterable", row.class().name()), + ) + })?; + + let fields = row.iter(vm)?.collect::>>()?; + let single_field = fields.len() == 1; + let mut output = Vec::new(); + + for (index, field) in fields.into_iter().enumerate() { + if index > 0 { + output.push(self.dialect.delimiter); + } + + let stringified; + let data: &[u8] = match_class!(match field { + ref s @ PyStr => s.as_bytes(), + crate::builtins::PyNone => b"", + ref obj => { + stringified = obj.str(vm)?; + stringified.as_bytes() + } + }); + + if single_field && data.is_empty() { + return Err(new_csv_error( + vm, + "single empty field record must be quoted", + )); + } + + write_unquoted_field(&mut output, data, &self.dialect, vm)?; + } + + write_lineterminator(&mut output, &self.dialect.lineterminator); + + let s = + core::str::from_utf8(&output).map_err(|e| new_not_utf8_error(vm, &output, e))?; + + self.write.call((s,), vm) + } + + fn writerow_minimal(&self, row: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let _state = self.state.lock(); + + let row: ArgIterable = ArgIterable::try_from_object(vm, row.clone()).map_err(|_e| { + new_csv_error( + vm, + format!("'{}' object is not iterable", row.class().name()), + ) + })?; + + let fields = row.iter(vm)?.collect::>>()?; + let single_field = fields.len() == 1; + let mut output = Vec::new(); + + for (index, field) in fields.into_iter().enumerate() { + if index > 0 { + output.push(self.dialect.delimiter); + } + + let stringified; + let data: &[u8] = match_class!(match field { + ref s @ PyStr => s.as_bytes(), + crate::builtins::PyNone => b"", + ref obj => { + stringified = obj.str(vm)?; + stringified.as_bytes() + } + }); + + // CPython quotes a QUOTE_MINIMAL field if it contains the + // delimiter, the quote character, '\r', '\n', or the line + // terminator, regardless of which line terminator is + // configured. A row with a single empty field is also quoted + // so that it is not read back as an empty line. + if field_needs_quotes(data, &self.dialect) || (single_field && data.is_empty()) { + write_quoted_field(&mut output, data, &self.dialect, vm)?; + } else { + output.extend_from_slice(data); + } + } + + write_lineterminator(&mut output, &self.dialect.lineterminator); + + let s = + core::str::from_utf8(&output).map_err(|e| new_not_utf8_error(vm, &output, e))?; + + self.write.call((s,), vm) } #[pymethod] fn writerow(&self, row: PyObjectRef, vm: &VirtualMachine) -> PyResult { + match self.dialect.quoting { + QuoteStyle::None => return self.writerow_quote_none(row, vm), + QuoteStyle::Strings | QuoteStyle::Notnull => { + return self.writerow_quoted_strings(row, vm); + } + QuoteStyle::Minimal => return self.writerow_minimal(row, vm), + _ => {} + } + let mut state = self.state.lock(); let WriteState { buffer, writer } = &mut *state; @@ -1180,8 +1542,19 @@ mod _csv { handle_res!(writer.terminator(&mut buffer[buffer_offset..])); } - let s = core::str::from_utf8(&buffer[..buffer_offset]) - .map_err(|_| vm.new_unicode_decode_error("csv not utf8"))?; + // csv-core just emitted the single-byte sentinel terminator (after + // closing the final quote / emitting an empty record as needed). + // Drop that sentinel byte and append the real, possibly + // multi-character, line terminator. + let emitted = &buffer[..buffer_offset]; + let body = emitted + .strip_suffix(&[CSV_CORE_TERMINATOR_SENTINEL]) + .ok_or_else(|| new_csv_error(vm, "internal error: missing record terminator"))?; + let mut output = body.to_vec(); + output.extend_from_slice(self.dialect.lineterminator.as_bytes()); + + let s = + core::str::from_utf8(&output).map_err(|e| new_not_utf8_error(vm, &output, e))?; self.write.call((s,), vm) } diff --git a/crates/stdlib/src/faulthandler.rs b/crates/stdlib/src/faulthandler.rs index b3af501d83a..3fbb8391bec 100644 --- a/crates/stdlib/src/faulthandler.rs +++ b/crates/stdlib/src/faulthandler.rs @@ -3,8 +3,6 @@ pub(crate) use decl::module_def; #[allow(static_mut_refs)] // TODO: group code only with static mut refs #[pymodule(name = "faulthandler")] mod decl { - #[cfg(any(unix, windows))] - use crate::vm::frame::Frame; use crate::vm::{ PyObjectRef, PyResult, VirtualMachine, function::{ArgIntoFloat, OptionalArg}, @@ -119,43 +117,45 @@ mod decl { } /// Dump the current thread's live frame chain to fd (signal-safe). - /// Walks the `Frame.previous` pointer chain starting from the - /// thread-local current frame pointer. + /// Walks the InterpreterFrame chain directly. #[cfg(any(unix, windows))] fn dump_live_frames(fd: i32) { const MAX_FRAME_DEPTH: usize = 100; - let mut frame_ptr = crate::vm::vm::thread::get_current_frame(); - if frame_ptr.is_null() { + let mut cur = crate::vm::vm::thread::get_current_frame(); + if cur.is_null() { puts(fd, " \n"); return; } let mut depth = 0; - while !frame_ptr.is_null() && depth < MAX_FRAME_DEPTH { - let frame = unsafe { &*frame_ptr }; - dump_frame_from_raw(fd, frame); - frame_ptr = frame.previous_frame(); + while !cur.is_null() && depth < MAX_FRAME_DEPTH { + let iframe = unsafe { &*cur }; + dump_iframe(fd, iframe); depth += 1; + cur = iframe.previous(); } - if depth >= MAX_FRAME_DEPTH && !frame_ptr.is_null() { + if depth == 0 { + puts(fd, " \n"); + } else if depth >= MAX_FRAME_DEPTH && !cur.is_null() { puts(fd, " ...\n"); } } - /// Dump a single frame's info to fd (signal-safe), reading live data. + /// Dump a single InterpreterFrame's info to fd (signal-safe). #[cfg(any(unix, windows))] - fn dump_frame_from_raw(fd: i32, frame: &Frame) { - let filename = frame.code.source_path().as_str(); - let funcname = frame.code.obj_name.as_str(); - let lasti = frame.lasti(); + fn dump_iframe(fd: i32, iframe: &rustpython_vm::frame::InterpreterFrame) { + let code = iframe.code(); + let filename = code.source_path().as_str(); + let funcname = code.obj_name.as_str(); + let lasti = iframe.get_lasti(); let lineno = if lasti == 0 { - frame.code.first_line_number.map_or(1, |n| n.get()) as u32 + code.first_line_number.map_or(1, |n| n.get()) as u32 } else { let idx = (lasti as usize).saturating_sub(1); - if idx < frame.code.locations.len() { - frame.code.locations[idx].0.line.get() as u32 + if idx < code.locations.len() { + code.locations[idx].0.line.get() as u32 } else { - frame.code.first_line_number.map_or(0, |n| n.get()) as u32 + code.first_line_number.map_or(0, |n| n.get()) as u32 } }; @@ -218,48 +218,6 @@ mod decl { } } - /// Write a frame's info to an fd using signal-safe I/O. - #[cfg(any(unix, windows))] - fn dump_frame_from_ref(fd: i32, frame: &crate::vm::Py) { - let funcname = frame.code.obj_name.as_str(); - let filename = frame.code.source_path().as_str(); - let lineno = if frame.lasti() == 0 { - frame.code.first_line_number.map_or(1, |n| n.get()) as u32 - } else { - frame.current_location().line.get() as u32 - }; - - puts(fd, " File \""); - dump_ascii(fd, filename); - puts(fd, "\", line "); - dump_decimal(fd, lineno as usize); - puts(fd, " in "); - dump_ascii(fd, funcname); - puts(fd, "\n"); - } - - /// Dump traceback for a thread given its frame stack (for cross-thread dumping). - /// # Safety - /// Each `FramePtr` must point to a live frame (caller holds the Mutex). - #[cfg(all(any(unix, windows), feature = "threading"))] - fn dump_traceback_thread_frames( - fd: i32, - thread_id: u64, - is_current: bool, - frames: &[rustpython_vm::vm::FramePtr], - ) { - write_thread_id(fd, thread_id, is_current); - - if frames.is_empty() { - puts(fd, " \n"); - } else { - for fp in frames.iter().rev() { - // SAFETY: caller holds the Mutex, so the owning thread can't pop. - dump_frame_from_ref(fd, unsafe { fp.as_ref() }); - } - } - } - #[derive(FromArgs)] struct DumpTracebackArgs { #[pyarg(any, default)] @@ -278,11 +236,7 @@ mod decl { dump_all_threads(fd, vm); } else { puts(fd, "Stack (most recent call first):\n"); - let frames = vm.frames.borrow(); - for fp in frames.iter().rev() { - // SAFETY: the frame is alive while it's in the Vec - dump_frame_from_ref(fd, unsafe { fp.as_ref() }); - } + dump_live_frames(fd); } } @@ -298,40 +252,104 @@ mod decl { #[cfg(any(unix, windows))] fn dump_all_threads(fd: i32, vm: &VirtualMachine) { // Get all threads' frame stacks from the shared registry - #[cfg(feature = "threading")] + // unix: stop-the-world so every other thread is parked at a safepoint + // and its frame chain is quiescent and alive while we walk it (matches + // faulthandler.dump_traceback running with the GIL held). + #[cfg(all(unix, feature = "threading"))] + { + use core::sync::atomic::Ordering; + let current_tid = rustpython_vm::stdlib::_thread::get_ident(); + { + vm.state.stop_the_world.stop_the_world(&vm.state); + scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); } + let registry = vm.state.thread_frames.lock(); + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for (&tid, slot) in registry.iter() { + if tid == current_tid { + continue; + } + // Under STW, all other threads are suspended so their + // stack-allocated iframes are stable. Walk via top_iframe + // which covers both FrameObject and stack-allocated paths. + let iframe_ptr = slot.top_iframe.load(Ordering::Relaxed) + as *const rustpython_vm::frame::InterpreterFrame; + write_thread_id(fd, tid, false); + if iframe_ptr.is_null() { + puts(fd, " \n"); + } else { + let mut cur = iframe_ptr; + let mut depth = 0; + while !cur.is_null() && depth < 100 { + let iframe = unsafe { &*cur }; + dump_iframe(fd, iframe); + depth += 1; + cur = iframe.previous(); + } + if depth >= 100 && !cur.is_null() { + puts(fd, " ...\n"); + } + } + puts(fd, "\n"); + } + } + + // Now dump current thread from its live frame chain + // (includes stack-allocated iframes without FrameObject). + write_thread_id(fd, current_tid, true); + dump_live_frames(fd); + } + + #[cfg(all(not(unix), feature = "threading"))] { let current_tid = rustpython_vm::stdlib::_thread::get_ident(); let registry = vm.state.thread_frames.lock(); - // First dump non-current threads, then current thread last + // Dump non-current threads using top_iframe, which includes + // both FrameObject and stack-allocated frames. + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&tid, slot) in registry.iter() { if tid == current_tid { continue; } - let frames_guard = slot.frames.lock(); - dump_traceback_thread_frames(fd, tid, false, &frames_guard); + + let iframe_ptr = slot.top_iframe.load(core::sync::atomic::Ordering::Relaxed) + as *const rustpython_vm::frame::InterpreterFrame; + write_thread_id(fd, tid, false); + if iframe_ptr.is_null() { + puts(fd, " \n"); + } else { + let mut cur = iframe_ptr; + let mut depth = 0; + while !cur.is_null() && depth < 100 { + let iframe = unsafe { &*cur }; + dump_iframe(fd, iframe); + depth += 1; + cur = iframe.previous(); + } + if depth >= 100 && !cur.is_null() { + puts(fd, " ...\n"); + } + } puts(fd, "\n"); } - // Now dump current thread (use vm.frames for most up-to-date data) + // Now dump current thread from its live frame chain + // (includes stack-allocated iframes without FrameObject). write_thread_id(fd, current_tid, true); - let frames = vm.frames.borrow(); - if frames.is_empty() { - puts(fd, " \n"); - } else { - for fp in frames.iter().rev() { - dump_frame_from_ref(fd, unsafe { fp.as_ref() }); - } - } + dump_live_frames(fd); } #[cfg(not(feature = "threading"))] { + let _ = vm; write_thread_id(fd, current_thread_id(), true); - let frames = vm.frames.borrow(); - for fp in frames.iter().rev() { - dump_frame_from_ref(fd, unsafe { fp.as_ref() }); - } + dump_live_frames(fd); } } @@ -368,7 +386,7 @@ mod decl { // faulthandler_fatal_error #[cfg(unix)] - extern "C" fn faulthandler_fatal_error(signum: libc::c_int) { + extern "C" fn faulthandler_fatal_error(signum: core::ffi::c_int) { let save_errno = get_errno(); if !FATAL_ERROR.enabled.load(Ordering::Relaxed) { @@ -405,7 +423,7 @@ mod decl { // faulthandler_fatal_error for Windows #[cfg(windows)] - extern "C" fn faulthandler_fatal_error(signum: libc::c_int) { + extern "C" fn faulthandler_fatal_error(signum: core::ffi::c_int) { let save_errno = get_errno(); if !FATAL_ERROR.enabled.load(Ordering::Relaxed) { @@ -474,7 +492,7 @@ mod decl { // Disable SIGSEGV handler for access violations to avoid double output if host_faulthandler::is_access_violation(code) { - host_faulthandler::disable_fatal_signal(libc::SIGSEGV); + host_faulthandler::disable_fatal_signal(host_faulthandler::SIGSEGV); } let all_threads = FATAL_ERROR.all_threads.load(Ordering::Relaxed); @@ -490,7 +508,10 @@ mod decl { return true; } - if !host_faulthandler::enable_fatal_handlers(faulthandler_fatal_error, libc::SA_NODEFER) { + if !host_faulthandler::enable_fatal_handlers( + faulthandler_fatal_error, + host_faulthandler::SA_NODEFER, + ) { return false; } @@ -649,10 +670,61 @@ mod decl { // Use thread frame slots when threading is enabled (includes all threads). // Fall back to live frame walking for non-threaded builds. cfg_select! { - feature = "threading" => { + all(unix, feature = "threading") => { + // The watchdog is a plain OS thread, not attached to + // the VM, so it cannot stop-the-world. Walk each + // published top frame lock-free and best-effort, like + // the faulthandler watchdog thread. + // Walk via top_iframe for all threads. The watchdog + // cannot stop-the-world, so this is best-effort + // (like CPython's _Py_DumpTracebackThreads). Stack + // frames are still alive because the target thread + // is executing (inside a blocking call). + for (tid, slot) in &thread_frame_slots { + let iframe_ptr = slot + .top_iframe + .load(core::sync::atomic::Ordering::Relaxed) + as *const rustpython_vm::frame::InterpreterFrame; + write_thread_id(fd, *tid, false); + if iframe_ptr.is_null() { + puts(fd, " \n"); + } else { + let mut cur = iframe_ptr; + let mut depth = 0; + while !cur.is_null() && depth < 100 { + let iframe = unsafe { &*cur }; + dump_iframe(fd, iframe); + depth += 1; + cur = iframe.previous(); + } + if depth >= 100 && !cur.is_null() { + puts(fd, " ...\n"); + } + } + } + } + all(not(unix), feature = "threading") => { for (tid, slot) in &thread_frame_slots { - let frames = slot.frames.lock(); - dump_traceback_thread_frames(fd, *tid, false, &frames); + let iframe_ptr = slot + .top_iframe + .load(core::sync::atomic::Ordering::Relaxed) + as *const rustpython_vm::frame::InterpreterFrame; + write_thread_id(fd, *tid, false); + if iframe_ptr.is_null() { + puts(fd, " \n"); + } else { + let mut cur = iframe_ptr; + let mut depth = 0; + while !cur.is_null() && depth < 100 { + let iframe = unsafe { &*cur }; + dump_iframe(fd, iframe); + depth += 1; + cur = iframe.previous(); + } + if depth >= 100 && !cur.is_null() { + puts(fd, " ...\n"); + } + } } } _ => { @@ -764,7 +836,7 @@ mod decl { } #[cfg(unix)] - extern "C" fn faulthandler_user_signal(signum: libc::c_int) { + extern "C" fn faulthandler_user_signal(signum: core::ffi::c_int) { let save_errno = get_errno(); let user = match host_faulthandler::get_user_signal(signum as usize) { @@ -895,7 +967,7 @@ mod decl { #[cfg(not(target_arch = "wasm32"))] { suppress_crash_report(); - host_faulthandler::raise_signal(libc::SIGFPE); + host_faulthandler::raise_signal(host_faulthandler::SIGFPE); } } diff --git a/crates/stdlib/src/fcntl.rs b/crates/stdlib/src/fcntl.rs index 5081c9e9c14..8e24f2b6e4a 100644 --- a/crates/stdlib/src/fcntl.rs +++ b/crates/stdlib/src/fcntl.rs @@ -25,38 +25,40 @@ mod fcntl { // I_LINK, I_UNLINK, I_PLINK, I_PUNLINK #[pyattr] - use libc::{F_GETFD, F_GETFL, F_SETFD, F_SETFL, FD_CLOEXEC}; + use host_fcntl::{F_GETFD, F_GETFL, F_SETFD, F_SETFL, FD_CLOEXEC}; #[cfg(not(target_os = "wasi"))] #[pyattr] - use libc::{F_DUPFD, F_DUPFD_CLOEXEC, F_GETLK, F_SETLK, F_SETLKW}; + use host_fcntl::{F_DUPFD, F_DUPFD_CLOEXEC, F_GETLK, F_SETLK, F_SETLKW}; #[cfg(not(any(target_os = "wasi", target_os = "redox")))] #[pyattr] - use libc::{F_GETOWN, F_RDLCK, F_SETOWN, F_UNLCK, F_WRLCK, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN}; + use host_fcntl::{ + F_GETOWN, F_RDLCK, F_SETOWN, F_UNLCK, F_WRLCK, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN, + }; #[cfg(target_vendor = "apple")] #[pyattr] - use libc::{F_FULLFSYNC, F_NOCACHE}; + use host_fcntl::{F_FULLFSYNC, F_NOCACHE}; #[cfg(target_os = "freebsd")] #[pyattr] - use libc::{F_DUP2FD, F_DUP2FD_CLOEXEC}; + use host_fcntl::{F_DUP2FD, F_DUP2FD_CLOEXEC}; #[cfg(any(target_os = "android", target_os = "linux"))] #[pyattr] - use libc::{F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW}; + use host_fcntl::{F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW}; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] #[pyattr] - use libc::{ + use host_fcntl::{ F_ADD_SEALS, F_GET_SEALS, F_GETLEASE, F_GETPIPE_SZ, F_NOTIFY, F_SEAL_GROW, F_SEAL_SEAL, F_SEAL_SHRINK, F_SEAL_WRITE, F_SETLEASE, F_SETPIPE_SZ, }; #[cfg(any(target_os = "dragonfly", target_os = "netbsd", target_vendor = "apple"))] #[pyattr] - use libc::F_GETPATH; + use host_fcntl::F_GETPATH; #[pyfunction] fn fcntl( diff --git a/crates/stdlib/src/grp.rs b/crates/stdlib/src/grp.rs index 34e9929d2c4..a237bd71043 100644 --- a/crates/stdlib/src/grp.rs +++ b/crates/stdlib/src/grp.rs @@ -10,6 +10,7 @@ mod grp { exceptions, types::PyStructSequence, }; + use core::hint::cold_path; use rustpython_host_env::grp as host_grp; #[pystruct_sequence_data] @@ -43,7 +44,7 @@ mod grp { #[pyfunction] fn getgrgid(gid: PyIntRef, vm: &VirtualMachine) -> PyResult { let gr_gid = gid.as_bigint(); - let gid = libc::gid_t::try_from(gr_gid).ok(); + let gid = host_grp::gid_t::try_from(gr_gid).ok(); let group = gid .map(host_grp::getgrgid) .transpose() @@ -61,10 +62,11 @@ mod grp { #[pyfunction] fn getgrnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - let gr_name = name.as_str(); - if gr_name.contains('\0') { - return Err(exceptions::cstring_error(vm)); + if name.as_pystr().contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } + let gr_name = name.as_str(); let group = host_grp::getgrnam(gr_name).map_err(|err| err.into_pyexception(vm))?; let group = group.ok_or_else(|| { vm.new_key_error( diff --git a/crates/stdlib/src/hashlib.rs b/crates/stdlib/src/hashlib.rs index c2153b08a59..d7f94cc2796 100644 --- a/crates/stdlib/src/hashlib.rs +++ b/crates/stdlib/src/hashlib.rs @@ -847,15 +847,15 @@ pub(crate) mod _hashlib { if len < 1 { return Err(vm.new_value_error("key length must be greater than 0.")); } - usize::try_from(len) - .map_err(|_| vm.new_overflow_error("key length is too great."))? + i32::try_from(len).map_err(|_| vm.new_overflow_error("key length is too great."))? + as usize } None => hash_digest_size(&name).ok_or_else(|| unsupported_hash(&name, vm))?, }; let password_buf = args.password.borrow_buf(); let salt_buf = args.salt.borrow_buf(); - let mut dk = vec![0u8; dklen]; + let mut dk = vm.new_zeroed_bytes(dklen)?; macro_rules! do_pbkdf2 { ($hash_ty:ty) => {{ diff --git a/crates/stdlib/src/locale.rs b/crates/stdlib/src/locale.rs index 74e9053fbfb..2f929e8b57f 100644 --- a/crates/stdlib/src/locale.rs +++ b/crates/stdlib/src/locale.rs @@ -18,7 +18,7 @@ mod _locale { not(any(target_os = "ios", target_os = "android", target_os = "redox")) ))] #[pyattr] - use libc::{ + use rustpython_host_env::locale::{ ABDAY_1, ABDAY_2, ABDAY_3, ABDAY_4, ABDAY_5, ABDAY_6, ABDAY_7, ABMON_1, ABMON_2, ABMON_3, ABMON_4, ABMON_5, ABMON_6, ABMON_7, ABMON_8, ABMON_9, ABMON_10, ABMON_11, ABMON_12, ALT_DIGITS, AM_STR, CODESET, CRNCYSTR, D_FMT, D_T_FMT, DAY_1, DAY_2, DAY_3, DAY_4, DAY_5, @@ -29,17 +29,19 @@ mod _locale { #[cfg(all(unix, not(any(target_os = "ios", target_os = "redox"))))] #[pyattr] - use libc::LC_MESSAGES; + use rustpython_host_env::locale::LC_MESSAGES; #[pyattr] - use libc::{LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME}; + use rustpython_host_env::locale::{ + LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME, + }; #[pyattr(name = "CHAR_MAX")] fn char_max(vm: &VirtualMachine) -> PyIntRef { - vm.ctx.new_int(libc::c_char::MAX) + vm.ctx.new_int(core::ffi::c_char::MAX) } - fn copy_grouping(group: &[libc::c_char], vm: &VirtualMachine) -> PyListRef { + fn copy_grouping(group: &[core::ffi::c_char], vm: &VirtualMachine) -> PyListRef { let mut group_vec: Vec = Vec::new(); for &value in group { let val = vm.ctx.new_int(value); diff --git a/crates/stdlib/src/lzma.rs b/crates/stdlib/src/lzma.rs index 0b699baddbb..6e8a913abaa 100644 --- a/crates/stdlib/src/lzma.rs +++ b/crates/stdlib/src/lzma.rs @@ -337,40 +337,43 @@ mod _lzma { } fn parse_filter_chain_spec( - filter_specs: Vec, + filter_specs: PyObjectRef, vm: &VirtualMachine, ) -> PyResult { const LZMA_FILTERS_MAX: usize = 4; - if filter_specs.len() > LZMA_FILTERS_MAX { + let filter_specs_len = filter_specs.length(vm)?; + if filter_specs_len > LZMA_FILTERS_MAX { return Err(new_lzma_error( format!("Too many filters - liblzma supports a maximum of {LZMA_FILTERS_MAX}"), vm, )); } + let filter_specs = filter_specs.try_sequence(vm)?; let mut filters = Filters::new(); - for spec in &filter_specs { - let filter_id = get_dict_opt_u64(spec, "id", vm)? + for i in 0..filter_specs_len { + let spec = filter_specs.get_item(i as isize, vm)?; + let filter_id = get_dict_opt_u64(&spec, "id", vm)? .ok_or_else(|| vm.new_value_error("Filter specifier must have an \"id\" entry"))?; match filter_id { FILTER_LZMA1 => { - let opts = parse_filter_spec_lzma(spec, vm)?; + let opts = parse_filter_spec_lzma(&spec, vm)?; filters.lzma1(&opts); } FILTER_LZMA2 => { - let opts = parse_filter_spec_lzma(spec, vm)?; + let opts = parse_filter_spec_lzma(&spec, vm)?; filters.lzma2(&opts); } FILTER_DELTA => { - let dist = parse_filter_spec_delta(spec, vm)?; + let dist = parse_filter_spec_delta(&spec, vm)?; filters .delta_properties(&[(dist - 1) as u8]) .map_err(|e| catch_lzma_error(e, vm))?; } FILTER_X86 | FILTER_POWERPC | FILTER_IA64 | FILTER_ARM | FILTER_ARMTHUMB | FILTER_SPARC => { - let start_offset = parse_filter_spec_bcj(spec, vm)?; + let start_offset = parse_filter_spec_bcj(&spec, vm)?; add_bcj_filter(&mut filters, filter_id, start_offset) .map_err(|e| catch_lzma_error(e, vm))?; } @@ -570,7 +573,7 @@ mod _lzma { #[pyarg(any, optional)] memlimit: Option, #[pyarg(any, optional)] - filters: Option>, + filters: Option, } impl Constructor for LZMADecompressor { @@ -735,7 +738,7 @@ mod _lzma { fn init_xz( check: i32, preset: u32, - filters: Option>, + filters: Option, vm: &VirtualMachine, ) -> PyResult { let real_check = @@ -751,10 +754,11 @@ mod _lzma { fn init_alone( preset: u32, - filter_specs: Option>, + filter_specs: Option, vm: &VirtualMachine, ) -> PyResult { - if let Some(_filter_specs) = filter_specs { + if let Some(filter_specs) = filter_specs { + filter_specs.length(vm)?; // TODO: validate single LZMA1 filter and use its options let options = LzmaOptions::new_preset(preset).map_err(|_| { new_lzma_error(format!("Invalid compression preset: {preset}"), vm) @@ -768,10 +772,7 @@ mod _lzma { } } - fn init_raw( - filter_specs: Option>, - vm: &VirtualMachine, - ) -> PyResult { + fn init_raw(filter_specs: Option, vm: &VirtualMachine) -> PyResult { let filter_specs = filter_specs .ok_or_else(|| vm.new_value_error("Must specify filters for FORMAT_RAW"))?; let filters = parse_filter_chain_spec(filter_specs, vm)?; @@ -788,7 +789,7 @@ mod _lzma { #[pyarg(any, optional)] preset: Option, #[pyarg(any, optional)] - filters: Option>, + filters: Option, } impl Constructor for LZMACompressor { diff --git a/crates/stdlib/src/math.rs b/crates/stdlib/src/math.rs index 92c2a66e93e..3fe1ffd3e63 100644 --- a/crates/stdlib/src/math.rs +++ b/crates/stdlib/src/math.rs @@ -727,25 +727,20 @@ mod math { } // Generic Python path - let (p_i, q_i) = (p_i.unwrap(), q_i.unwrap()); - - // Collect current + remaining elements - let p_remaining: Result, _> = - core::iter::once(Ok(p_i)).chain(p_iter).collect(); - let q_remaining: Result, _> = - core::iter::once(Ok(q_i)).chain(q_iter).collect(); - let (p_vec, q_vec) = (p_remaining?, q_remaining?); - - if p_vec.len() != q_vec.len() { - return Err(vm.new_value_error("Inputs are not the same length")); - } - + let (mut p_i, mut q_i) = (p_i.unwrap(), q_i.unwrap()); let mut total = obj_total.unwrap_or_else(|| vm.ctx.new_int(0).into()); - for (p_item, q_item) in p_vec.into_iter().zip(q_vec) { - let prod = vm._mul(&p_item, &q_item)?; + loop { + let prod = vm._mul(&p_i, &q_i)?; total = vm._add(&total, &prod)?; + + let next_p = p_iter.next().transpose()?; + let next_q = q_iter.next().transpose()?; + match (next_p, next_q) { + (Some(next_p), Some(next_q)) => (p_i, q_i) = (next_p, next_q), + (None, None) => return Ok(total), + _ => return Err(vm.new_value_error("Inputs are not the same length")), + } } - return Ok(total); } Ok(obj_total.unwrap_or_else(|| vm.ctx.new_int(0).into())) diff --git a/crates/stdlib/src/md5.rs b/crates/stdlib/src/md5.rs index 2ff6cd24ff7..0339bf8ace7 100644 --- a/crates/stdlib/src/md5.rs +++ b/crates/stdlib/src/md5.rs @@ -3,10 +3,17 @@ pub(crate) use _md5::module_def; #[pymodule] mod _md5 { use crate::hashlib::_hashlib::{HashArgs, local_md5}; - use crate::vm::{PyPayload, PyResult, VirtualMachine}; + use crate::vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule}; #[pyfunction] fn md5(args: HashArgs, vm: &VirtualMachine) -> PyResult { Ok(local_md5(args, vm)?.into_pyobject(vm)) } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + let _ = vm.import("_hashlib", 0); + __module_exec(vm, module); + Ok(()) + } } diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index 2332ee0e1ce..14957ad904e 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -24,9 +24,9 @@ mod mmap { use core::ops::{Deref, DerefMut}; use crossbeam_utils::atomic::AtomicCell; use num_traits::Signed; - #[cfg(windows)] - use std::io; use std::io::Write; + #[cfg(windows)] + use {core::hint::cold_path, memchr::memchr, rustpython_vm::exceptions, std::io}; #[cfg(unix)] use rustpython_host_env::crt_fd; @@ -60,14 +60,14 @@ mod mmap { #[cfg(unix)] #[pyattr] - use libc::{ + use host_mmap::{ MADV_DONTNEED, MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL, MADV_WILLNEED, MAP_ANON, MAP_ANONYMOUS, MAP_PRIVATE, MAP_SHARED, PROT_EXEC, PROT_READ, PROT_WRITE, }; #[cfg(target_os = "macos")] #[pyattr] - use libc::{MADV_FREE_REUSABLE, MADV_FREE_REUSE}; + use host_mmap::{MADV_FREE_REUSABLE, MADV_FREE_REUSE}; #[cfg(any( target_os = "android", @@ -80,11 +80,11 @@ mod mmap { target_vendor = "apple" ))] #[pyattr] - use libc::MADV_FREE; + use host_mmap::MADV_FREE; #[cfg(target_os = "linux")] #[pyattr] - use libc::{ + use host_mmap::{ MADV_DODUMP, MADV_DOFORK, MADV_DONTDUMP, MADV_DONTFORK, MADV_HUGEPAGE, MADV_HWPOISON, MADV_MERGEABLE, MADV_NOHUGEPAGE, MADV_REMOVE, MADV_UNMERGEABLE, }; @@ -106,21 +106,21 @@ mod mmap { ) ))] #[pyattr] - use libc::MADV_SOFT_OFFLINE; + use host_mmap::MADV_SOFT_OFFLINE; #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] #[pyattr] - use libc::{MAP_DENYWRITE, MAP_EXECUTABLE, MAP_POPULATE}; + use host_mmap::{MAP_DENYWRITE, MAP_EXECUTABLE, MAP_POPULATE}; // MAP_STACK is available on Linux, OpenBSD, and NetBSD #[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "netbsd"))] #[pyattr] - use libc::MAP_STACK; + use host_mmap::MAP_STACK; // FreeBSD-specific MADV constants #[cfg(target_os = "freebsd")] #[pyattr] - use libc::{MADV_AUTOSYNC, MADV_CORE, MADV_NOCORE, MADV_NOSYNC, MADV_PROTECT}; + use host_mmap::{MADV_AUTOSYNC, MADV_CORE, MADV_NOCORE, MADV_NOSYNC, MADV_PROTECT}; #[pyattr] const ACCESS_DEFAULT: u32 = AccessMode::Default as u32; @@ -212,10 +212,10 @@ mod mmap { fileno: i32, #[pyarg(any)] length: isize, - #[pyarg(any, default = libc::MAP_SHARED)] - flags: libc::c_int, - #[pyarg(any, default = libc::PROT_WRITE | libc::PROT_READ)] - prot: libc::c_int, + #[pyarg(any, default = host_mmap::MAP_SHARED)] + flags: core::ffi::c_int, + #[pyarg(any, default = host_mmap::PROT_WRITE | host_mmap::PROT_READ)] + prot: core::ffi::c_int, #[pyarg(any, default = AccessMode::Default)] access: AccessMode, #[pyarg(any, default = 0)] @@ -294,7 +294,7 @@ mod mmap { #[derive(FromArgs)] pub(super) struct AdviseOptions { #[pyarg(positional)] - option: libc::c_int, + option: core::ffi::c_int, #[pyarg(positional, default)] start: Option, #[pyarg(positional, default)] @@ -303,7 +303,11 @@ mod mmap { #[cfg(all(unix, not(target_os = "redox")))] impl AdviseOptions { - fn values(self, len: usize, vm: &VirtualMachine) -> PyResult<(libc::c_int, usize, usize)> { + fn values( + self, + len: usize, + vm: &VirtualMachine, + ) -> PyResult<(core::ffi::c_int, usize, usize)> { let start = self .start .map(|s| { @@ -342,7 +346,7 @@ mod mmap { #[cfg(unix)] fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { - use libc::{MAP_PRIVATE, MAP_SHARED, PROT_READ, PROT_WRITE}; + use host_mmap::{MAP_PRIVATE, MAP_SHARED, PROT_READ, PROT_WRITE}; let mut map_size = args.validate_new_args(vm)?; let MmapNewArgs { @@ -456,8 +460,9 @@ mod mmap { let s = obj .try_to_value::(vm) .map_err(|_| vm.new_type_error("tagname must be a string or None"))?; - if s.contains('\0') { - return Err(vm.new_value_error("tagname must not contain null characters")); + if memchr(b'\0', s.as_bytes()).is_some() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } Some(s) } @@ -552,7 +557,7 @@ mod mmap { map_size, ) .map_err(|err| { - if err.raw_os_error() == Some(libc::EOVERFLOW) { + if err.raw_os_error() == Some(host_mmap::EOVERFLOW) { vm.new_overflow_error("mmap offset plus size would overflow") } else { err.to_pyexception(vm) @@ -606,6 +611,8 @@ mod mmap { }; impl AsBuffer for PyMmap { + const RELEASE_BUFFER: bool = true; + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { let readonly = matches!(zelf.access, AccessMode::Read); let buf = PyBuffer::new( @@ -772,7 +779,10 @@ mod mmap { let start = options .start .map_or_else(|| self.pos(), |start| start.saturated_at(size)); - let end = options.end.map_or(size, |end| end.saturated_at(size)); + let end = options + .end + .map_or(size, |end| end.saturated_at(size)) + .max(start); (start, end) } @@ -881,7 +891,7 @@ mod mmap { let dest = dest.try_to_primitive(vm).ok()?; let src = src.try_to_primitive(vm).ok()?; let cnt = cnt.try_to_primitive(vm).ok()?; - if size - dest < cnt || size - src < cnt { + if dest > size || src > size || size - dest < cnt || size - src < cnt { return None; } Some((dest, src, cnt)) @@ -1074,7 +1084,7 @@ mod mmap { fn seek( &self, dist: isize, - whence: OptionalArg, + whence: OptionalArg, vm: &VirtualMachine, ) -> PyResult<()> { let how = whence.unwrap_or(0); @@ -1137,24 +1147,40 @@ mod mmap { } #[pymethod] - fn write(&self, bytes: ArgBytesLike, vm: &VirtualMachine) -> PyResult { - let pos = self.pos(); - let size = self.__len__(); + fn seekable(&self) -> bool { + true + } - let data = bytes.borrow_buf(); + #[pymethod] + fn write(zelf: &Py, bytes: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + let self_ = &**zelf; + let pos = self_.pos(); + let size = self_.__len__(); + + // Writing locks the map, and reading a source that views this same + // map locks it too, so such a source is copied out first. + let copied; + let borrowed; + let data: &[u8] = if bytes.source_object().is(zelf.as_object()) { + copied = bytes.borrow_buf().to_vec(); + &copied + } else { + borrowed = bytes.borrow_buf(); + &borrowed + }; if pos > size || size - pos < data.len() { return Err(vm.new_value_error("data out of range")); } - let len = self.try_writable(vm, |mmap| { + let len = self_.try_writable(vm, |mmap| { (&mut mmap[pos..(pos + data.len())]) - .write(&data) + .write(data) .map_err(|err| err.to_pyexception(vm))?; Ok(data.len()) })??; - self.advance_pos(len); + self_.advance_pos(len); Ok(PyInt::from(len).into_ref(&vm.ctx)) } diff --git a/crates/stdlib/src/multiprocessing.rs b/crates/stdlib/src/multiprocessing.rs index 78debdc7813..c79af002b25 100644 --- a/crates/stdlib/src/multiprocessing.rs +++ b/crates/stdlib/src/multiprocessing.rs @@ -333,7 +333,7 @@ mod _multiprocessing { } #[pyfunction] - fn send(socket: usize, buf: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + fn send(socket: usize, buf: ArgBytesLike, vm: &VirtualMachine) -> PyResult { buf.with_ref(|b| { host_multiprocessing::send_socket(socket as host_multiprocessing::RawSocket, b) }) @@ -355,10 +355,11 @@ mod _multiprocessing { }; use core::sync::atomic::{AtomicI32, AtomicU64, Ordering}; #[cfg(target_vendor = "apple")] - use libc::sem_t; + use rustpython_host_env::multiprocessing::sem_t; use rustpython_host_env::multiprocessing::{ self as host_multiprocessing, SemError, TryAcquireStatus, WaitStatus, }; + use rustpython_vm::exceptions; /// Error type for sem_timedwait operations #[cfg(target_vendor = "apple")] @@ -373,7 +374,7 @@ mod _multiprocessing { #[cfg(target_vendor = "apple")] fn sem_timedwait_polled( sem: *mut sem_t, - deadline: &libc::timespec, + deadline: &host_multiprocessing::timespec, vm: &VirtualMachine, ) -> Result<(), SemWaitError> { let mut delay: u64 = 0; @@ -810,8 +811,8 @@ mod _multiprocessing { let value = args.value as u32; let (handle, name) = SemHandle::create(&args.name, value, args.unlink).map_err(|err| { - if err == SemError::InvalidInput && args.name.contains('\0') { - vm.new_value_error("embedded null character") + if err == SemError::InteriorNul { + exceptions::nul_char_error(vm) } else { os_error(vm, err) } @@ -834,8 +835,8 @@ mod _multiprocessing { #[pyfunction] fn sem_unlink(name: String, vm: &VirtualMachine) -> PyResult<()> { host_multiprocessing::sem_unlink(&name).map_err(|err| { - if err == SemError::InvalidInput && name.contains('\0') { - vm.new_value_error("embedded null character") + if err == SemError::InteriorNul { + exceptions::nul_char_error(vm) } else { os_error(vm, err) } diff --git a/crates/stdlib/src/openssl.rs b/crates/stdlib/src/openssl.rs index 76309d3c21d..fe4a5298d12 100644 --- a/crates/stdlib/src/openssl.rs +++ b/crates/stdlib/src/openssl.rs @@ -53,6 +53,8 @@ fn probe() -> &'static ProbeResult { #[cfg(ossl111)] ossl111, #[cfg(windows)] windows))] mod _ssl { + use core::hint::cold_path; + use super::{bio, probe}; // Import error types and helpers used in this module (others are exposed via pymodule(with(...))) @@ -79,12 +81,14 @@ mod _ssl { ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath, OptionalArg, PyComparisonValue, }, + stdlib::_warnings, types::{Comparable, Constructor, PyComparisonOp}, utils::ToCString, }, }; use crossbeam_utils::atomic::AtomicCell; use foreign_types_shared::{ForeignType, ForeignTypeRef}; + use memchr::memchr; use openssl::{ asn1::{Asn1Object, Asn1ObjectRef}, error::ErrorStack, @@ -154,63 +158,63 @@ mod _ssl { // SSL Alert Descriptions (RFC 5246 and extensions) // Hybrid approach: use openssl_sys constants where available, hardcode others #[pyattr] - const ALERT_DESCRIPTION_CLOSE_NOTIFY: libc::c_int = 0; + const ALERT_DESCRIPTION_CLOSE_NOTIFY: core::ffi::c_int = 0; #[pyattr] - const ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: libc::c_int = 10; + const ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: core::ffi::c_int = 10; #[pyattr] - const ALERT_DESCRIPTION_BAD_RECORD_MAC: libc::c_int = 20; + const ALERT_DESCRIPTION_BAD_RECORD_MAC: core::ffi::c_int = 20; #[pyattr] - const ALERT_DESCRIPTION_RECORD_OVERFLOW: libc::c_int = 22; + const ALERT_DESCRIPTION_RECORD_OVERFLOW: core::ffi::c_int = 22; #[pyattr] - const ALERT_DESCRIPTION_DECOMPRESSION_FAILURE: libc::c_int = 30; + const ALERT_DESCRIPTION_DECOMPRESSION_FAILURE: core::ffi::c_int = 30; #[pyattr] - const ALERT_DESCRIPTION_HANDSHAKE_FAILURE: libc::c_int = 40; + const ALERT_DESCRIPTION_HANDSHAKE_FAILURE: core::ffi::c_int = 40; #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE: libc::c_int = 42; + const ALERT_DESCRIPTION_BAD_CERTIFICATE: core::ffi::c_int = 42; #[pyattr] - const ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE: libc::c_int = 43; + const ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE: core::ffi::c_int = 43; #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_REVOKED: libc::c_int = 44; + const ALERT_DESCRIPTION_CERTIFICATE_REVOKED: core::ffi::c_int = 44; #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: libc::c_int = 45; + const ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: core::ffi::c_int = 45; #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN: libc::c_int = 46; + const ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN: core::ffi::c_int = 46; #[pyattr] - const ALERT_DESCRIPTION_ILLEGAL_PARAMETER: libc::c_int = SSL_AD_ILLEGAL_PARAMETER; + const ALERT_DESCRIPTION_ILLEGAL_PARAMETER: core::ffi::c_int = SSL_AD_ILLEGAL_PARAMETER; #[pyattr] - const ALERT_DESCRIPTION_UNKNOWN_CA: libc::c_int = 48; + const ALERT_DESCRIPTION_UNKNOWN_CA: core::ffi::c_int = 48; #[pyattr] - const ALERT_DESCRIPTION_ACCESS_DENIED: libc::c_int = 49; + const ALERT_DESCRIPTION_ACCESS_DENIED: core::ffi::c_int = 49; #[pyattr] - const ALERT_DESCRIPTION_DECODE_ERROR: libc::c_int = SSL_AD_DECODE_ERROR; + const ALERT_DESCRIPTION_DECODE_ERROR: core::ffi::c_int = SSL_AD_DECODE_ERROR; #[pyattr] - const ALERT_DESCRIPTION_DECRYPT_ERROR: libc::c_int = 51; + const ALERT_DESCRIPTION_DECRYPT_ERROR: core::ffi::c_int = 51; #[pyattr] - const ALERT_DESCRIPTION_PROTOCOL_VERSION: libc::c_int = 70; + const ALERT_DESCRIPTION_PROTOCOL_VERSION: core::ffi::c_int = 70; #[pyattr] - const ALERT_DESCRIPTION_INSUFFICIENT_SECURITY: libc::c_int = 71; + const ALERT_DESCRIPTION_INSUFFICIENT_SECURITY: core::ffi::c_int = 71; #[pyattr] - const ALERT_DESCRIPTION_INTERNAL_ERROR: libc::c_int = 80; + const ALERT_DESCRIPTION_INTERNAL_ERROR: core::ffi::c_int = 80; #[pyattr] - const ALERT_DESCRIPTION_USER_CANCELLED: libc::c_int = 90; + const ALERT_DESCRIPTION_USER_CANCELLED: core::ffi::c_int = 90; #[pyattr] - const ALERT_DESCRIPTION_NO_RENEGOTIATION: libc::c_int = 100; + const ALERT_DESCRIPTION_NO_RENEGOTIATION: core::ffi::c_int = 100; #[pyattr] - const ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION: libc::c_int = 110; + const ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION: core::ffi::c_int = 110; #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE: libc::c_int = 111; + const ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE: core::ffi::c_int = 111; #[pyattr] - const ALERT_DESCRIPTION_UNRECOGNIZED_NAME: libc::c_int = SSL_AD_UNRECOGNIZED_NAME; + const ALERT_DESCRIPTION_UNRECOGNIZED_NAME: core::ffi::c_int = SSL_AD_UNRECOGNIZED_NAME; #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE: libc::c_int = 113; + const ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE: core::ffi::c_int = 113; #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE: libc::c_int = 114; + const ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE: core::ffi::c_int = 114; #[pyattr] - const ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY: libc::c_int = 115; + const ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY: core::ffi::c_int = 115; // CRL verification constants #[pyattr] - const VERIFY_CRL_CHECK_CHAIN: libc::c_ulong = + const VERIFY_CRL_CHECK_CHAIN: core::ffi::c_ulong = sys::X509_V_FLAG_CRL_CHECK | sys::X509_V_FLAG_CRL_CHECK_ALL; // taken from CPython, should probably be kept up to date with their version if it ever changes @@ -248,7 +252,8 @@ mod _ssl { #[pyattr] const PROTO_MAXIMUM_SUPPORTED: i32 = ProtoVersion::MaxSupported as i32; #[pyattr] - const OP_ALL: libc::c_ulong = (sys::SSL_OP_ALL & !sys::SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) as _; + const OP_ALL: core::ffi::c_ulong = + (sys::SSL_OP_ALL & !sys::SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) as _; #[pyattr] const HAS_TLS_UNIQUE: bool = true; #[pyattr] @@ -298,7 +303,7 @@ mod _ssl { // SSL_VERIFY constants for post-handshake authentication #[cfg(ossl111)] - const SSL_VERIFY_POST_HANDSHAKE: libc::c_int = 0x20; + const SSL_VERIFY_POST_HANDSHAKE: core::ffi::c_int = 0x20; // the openssl version from the API headers @@ -393,7 +398,7 @@ mod _ssl { unsafe { ptr2obj(sys::OBJ_nid2obj(nid.as_raw())) } } - type PyNid = (libc::c_int, String, String, Option); + type PyNid = (core::ffi::c_int, String, String, Option); fn obj2py(obj: &Asn1ObjectRef, vm: &VirtualMachine) -> PyResult { let nid = obj.nid(); let short_name = nid @@ -428,7 +433,7 @@ mod _ssl { } #[pyfunction] - fn nid2obj(nid: libc::c_int, vm: &VirtualMachine) -> PyResult { + fn nid2obj(nid: core::ffi::c_int, vm: &VirtualMachine) -> PyResult { _nid2obj(Nid::from_raw(nid)) .as_deref() .ok_or_else(|| vm.new_value_error(format!("unknown NID {nid}"))) @@ -508,7 +513,7 @@ mod _ssl { #[pyfunction(name = "RAND_add")] fn rand_add(string: ArgStrOrBytesLike, entropy: f64) { let f = |b: &[u8]| { - for buf in b.chunks(libc::c_int::MAX as usize) { + for buf in b.chunks(core::ffi::c_int::MAX as usize) { unsafe { sys::RAND_add(buf.as_ptr() as *const _, buf.len() as _, entropy) } } }; @@ -573,9 +578,9 @@ mod _ssl { } // Get or create an ex_data index for SNI callback data - fn get_sni_ex_data_index() -> libc::c_int { + fn get_sni_ex_data_index() -> core::ffi::c_int { use rustpython_common::lock::LazyLock; - static SNI_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { + static SNI_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { sys::SSL_get_ex_new_index( 0, core::ptr::null_mut(), @@ -591,12 +596,12 @@ mod _ssl { // NOTE: We don't free the data here because it's managed manually in do_handshake // to avoid use-after-free when the SSL object is dropped after timeout unsafe extern "C" fn sni_callback_data_free( - _parent: *mut libc::c_void, - _ptr: *mut libc::c_void, + _parent: *mut core::ffi::c_void, + _ptr: *mut core::ffi::c_void, _ad: *mut sys::CRYPTO_EX_DATA, - _idx: libc::c_int, - _argl: libc::c_long, - _argp: *mut libc::c_void, + _idx: core::ffi::c_int, + _argl: core::ffi::c_long, + _argp: *mut core::ffi::c_void, ) { // Intentionally empty - data is freed in cleanup_sni_ex_data() } @@ -617,9 +622,9 @@ mod _ssl { } // Get or create an ex_data index for msg_callback data - fn get_msg_callback_ex_data_index() -> libc::c_int { + fn get_msg_callback_ex_data_index() -> core::ffi::c_int { use rustpython_common::lock::LazyLock; - static MSG_CB_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { + static MSG_CB_EX_DATA_IDX: LazyLock = LazyLock::new(|| unsafe { sys::SSL_get_ex_new_index( 0, core::ptr::null_mut(), @@ -633,12 +638,12 @@ mod _ssl { // Free function for msg_callback data - called by OpenSSL when SSL is freed unsafe extern "C" fn msg_callback_data_free( - _parent: *mut libc::c_void, - ptr: *mut libc::c_void, + _parent: *mut core::ffi::c_void, + ptr: *mut core::ffi::c_void, _ad: *mut sys::CRYPTO_EX_DATA, - _idx: libc::c_int, - _argl: libc::c_long, - _argp: *mut libc::c_void, + _idx: core::ffi::c_int, + _argl: core::ffi::c_long, + _argp: *mut core::ffi::c_void, ) { if !ptr.is_null() { unsafe { @@ -652,13 +657,13 @@ mod _ssl { // SNI callback function called by OpenSSL unsafe extern "C" fn _servername_callback( ssl_ptr: *mut sys::SSL, - al: *mut libc::c_int, - arg: *mut libc::c_void, - ) -> libc::c_int { - const SSL_TLSEXT_ERR_OK: libc::c_int = 0; - const SSL_TLSEXT_ERR_ALERT_FATAL: libc::c_int = 2; - const SSL_AD_INTERNAL_ERROR: libc::c_int = 80; - const TLSEXT_NAMETYPE_host_name: libc::c_int = 0; + al: *mut core::ffi::c_int, + arg: *mut core::ffi::c_void, + ) -> core::ffi::c_int { + const SSL_TLSEXT_ERR_OK: core::ffi::c_int = 0; + const SSL_TLSEXT_ERR_ALERT_FATAL: core::ffi::c_int = 2; + const SSL_AD_INTERNAL_ERROR: core::ffi::c_int = 80; + const TLSEXT_NAMETYPE_host_name: core::ffi::c_int = 0; if arg.is_null() { return SSL_TLSEXT_ERR_OK; @@ -766,13 +771,13 @@ mod _ssl { // Called during SSL operations to report protocol messages. // debughelpers.c:_PySSL_msg_callback unsafe extern "C" fn _msg_callback( - write_p: libc::c_int, - mut version: libc::c_int, - content_type: libc::c_int, - buf: *const libc::c_void, + write_p: core::ffi::c_int, + mut version: core::ffi::c_int, + content_type: core::ffi::c_int, + buf: *const core::ffi::c_void, len: usize, ssl_ptr: *mut sys::SSL, - _arg: *mut libc::c_void, + _arg: *mut core::ffi::c_void, ) { if ssl_ptr.is_null() { return; @@ -910,16 +915,24 @@ mod _ssl { ) -> PyResult { let proto = SslVersion::try_from(proto_version) .map_err(|_| vm.new_value_error("invalid protocol version"))?; - let method = match proto { + let (method, deprecated_protocol) = match proto { // SslVersion::Ssl3 => unsafe { ssl::SslMethod::from_ptr(sys::SSLv3_method()) }, - SslVersion::Tls => ssl::SslMethod::tls(), - SslVersion::Tls1 => ssl::SslMethod::tls(), - SslVersion::Tls1_1 => ssl::SslMethod::tls(), - SslVersion::Tls1_2 => ssl::SslMethod::tls(), - SslVersion::TlsClient => ssl::SslMethod::tls_client(), - SslVersion::TlsServer => ssl::SslMethod::tls_server(), + SslVersion::Tls => (ssl::SslMethod::tls(), Some("PROTOCOL_TLS")), + SslVersion::Tls1 => (ssl::SslMethod::tls(), Some("PROTOCOL_TLSv1")), + SslVersion::Tls1_1 => (ssl::SslMethod::tls(), Some("PROTOCOL_TLSv1_1")), + SslVersion::Tls1_2 => (ssl::SslMethod::tls(), Some("PROTOCOL_TLSv1_2")), + SslVersion::TlsClient => (ssl::SslMethod::tls_client(), None), + SslVersion::TlsServer => (ssl::SslMethod::tls_server(), None), _ => return Err(vm.new_value_error("invalid protocol version")), }; + if let Some(protocol_name) = deprecated_protocol { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!("ssl.{protocol_name} is deprecated"), + 2, + vm, + )?; + } let mut builder = SslContextBuilder::new(method).map_err(|e| convert_openssl_error(vm, e))?; @@ -1008,6 +1021,24 @@ mod _ssl { #[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))] impl PySslContext { + fn warn_deprecated_tls_version(version: i32, vm: &VirtualMachine) -> PyResult<()> { + let version_name = match version { + PROTO_SSLv3 => Some("SSLv3"), + PROTO_TLSv1 => Some("TLSv1"), + PROTO_TLSv1_1 => Some("TLSv1_1"), + _ => None, + }; + if let Some(version_name) = version_name { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!("ssl.TLSVersion.{version_name} is deprecated"), + 2, + vm, + )?; + } + Ok(()) + } + fn builder(&self) -> PyRwLockWriteGuard<'_, SslContextBuilder> { self.ctx.write() } @@ -1038,12 +1069,13 @@ mod _ssl { #[pymethod] fn set_ciphers(&self, cipherlist: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { - let ciphers: &str = cipherlist.as_ref(); - if ciphers.contains('\0') { - return Err(exceptions::cstring_error(vm)); + if cipherlist.contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } + self.builder() - .set_cipher_list(ciphers) + .set_cipher_list(cipherlist.as_ref()) .map_err(|_| new_ssl_error(vm, "No cipher can be selected.")) } @@ -1095,13 +1127,10 @@ mod _ssl { let name_cstr = match name { Either::A(s) => { let s: &str = s.as_ref(); - if s.contains('\0') { - return Err(exceptions::cstring_error(vm)); - } s.to_cstring(vm)? } Either::B(b) => std::ffi::CString::new(b.borrow_buf().to_vec()) - .map_err(|_| exceptions::cstring_error(vm))?, + .map_err(|_| exceptions::nul_char_error(vm))?, }; // Find the NID for the curve name using OBJ_sn2nid @@ -1122,7 +1151,7 @@ mod _ssl { } #[pygetset] - fn options(&self) -> libc::c_ulong { + fn options(&self) -> core::ffi::c_ulong { self.ctx.read().options().bits() as _ } #[pygetset(setter)] @@ -1130,15 +1159,33 @@ mod _ssl { if new_opts < 0 { return Err(vm.new_value_error("invalid options value")); } - let new_opts = new_opts as libc::c_ulong; - let mut ctx = self.builder(); - // Get current options - let current = ctx.options().bits() as libc::c_ulong; + let new_opts = new_opts as core::ffi::c_ulong; + let current = { + let ctx = self.ctx(); + unsafe { sys::SSL_CTX_get_options(ctx.as_ptr()) } + }; // Calculate options to clear and set let clear = current & !new_opts; let set = !current & new_opts; + let opt_no = sys::SSL_OP_NO_SSLv2 + | sys::SSL_OP_NO_SSLv3 + | sys::SSL_OP_NO_TLSv1 + | sys::SSL_OP_NO_TLSv1_1 + | sys::SSL_OP_NO_TLSv1_2; + #[cfg(ossl111)] + let opt_no = opt_no | sys::SSL_OP_NO_TLSv1_3; + if (set & opt_no) != 0 { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "ssl.OP_NO_SSL*/ssl.OP_NO_TLS* options are deprecated".to_owned(), + 2, + vm, + )?; + } + + let mut ctx = self.builder(); // Clear options first (using raw FFI since openssl crate doesn't expose clear_options) if clear != 0 { unsafe { @@ -1190,7 +1237,7 @@ mod _ssl { Ok(()) } #[pygetset] - fn verify_flags(&self) -> libc::c_ulong { + fn verify_flags(&self) -> core::ffi::c_ulong { unsafe { let ctx_ptr = self.ctx().as_ptr(); let param = sys::SSL_CTX_get0_param(ctx_ptr); @@ -1198,7 +1245,11 @@ mod _ssl { } } #[pygetset(setter)] - fn set_verify_flags(&self, new_flags: libc::c_ulong, vm: &VirtualMachine) -> PyResult<()> { + fn set_verify_flags( + &self, + new_flags: core::ffi::c_ulong, + vm: &VirtualMachine, + ) -> PyResult<()> { unsafe { let ctx_ptr = self.ctx().as_ptr(); let param = sys::SSL_CTX_get0_param(ctx_ptr); @@ -1241,6 +1292,8 @@ mod _ssl { } #[pygetset(setter)] fn set_minimum_version(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { + Self::warn_deprecated_tls_version(value, vm)?; + // Handle special values let proto_version = match value { -2 => { @@ -1275,6 +1328,8 @@ mod _ssl { } #[pygetset(setter)] fn set_maximum_version(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { + Self::warn_deprecated_tls_version(value, vm)?; + // Handle special values let proto_version = match value { -1 => { @@ -1359,10 +1414,10 @@ mod _ssl { { let mut ctx = self.builder(); let server = protos.with_ref(|pbuf| { - if pbuf.len() > libc::c_uint::MAX as usize { + if pbuf.len() > core::ffi::c_uint::MAX as usize { return Err(vm.new_overflow_error(format!( "protocols longer than {} bytes", - libc::c_uint::MAX + core::ffi::c_uint::MAX ))); } ctx.set_alpn_protos(pbuf) @@ -1523,7 +1578,7 @@ mod _ssl { return Err(vm .new_os_subtype_error( vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(libc::ENOENT), + Some(rustpython_host_env::errno::errors::ENOENT), format!("No such file or directory: '{}'", path.display()), ) .upcast()); @@ -1534,7 +1589,7 @@ mod _ssl { return Err(vm .new_os_subtype_error( vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(libc::ENOENT), + Some(rustpython_host_env::errno::errors::ENOENT), format!("No such file or directory: '{}'", path.display()), ) .upcast()); @@ -1667,7 +1722,7 @@ mod _ssl { std::io::ErrorKind::NotFound => vm .new_os_subtype_error( vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(libc::ENOENT), + Some(rustpython_host_env::errno::errors::ENOENT), e.to_string(), ) .upcast(), @@ -1684,7 +1739,7 @@ mod _ssl { ) }; unsafe { - libc::fclose(fp); + rustpython_host_env::fileutils::fclose(fp); } if dh.is_null() { @@ -1842,7 +1897,7 @@ mod _ssl { return Err(vm .new_os_subtype_error( vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(libc::ENOENT), + Some(rustpython_host_env::errno::errors::ENOENT), format!("No such file or directory: '{}'", cert_path.display()), ) .upcast()); @@ -1853,7 +1908,7 @@ mod _ssl { return Err(vm .new_os_subtype_error( vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(libc::ENOENT), + Some(rustpython_host_env::errno::errors::ENOENT), format!("No such file or directory: '{}'", kp.display()), ) .upcast()); @@ -2013,7 +2068,7 @@ mod _ssl { let ret = SSL_set_session_id_context( ssl.as_ptr(), SID_CTX.as_ptr(), - SID_CTX.len() as libc::c_uint, + SID_CTX.len() as core::ffi::c_uint, ); if ret == 0 { return Err(convert_openssl_error(vm, ErrorStack::get())); @@ -2026,15 +2081,16 @@ mod _ssl { // Configure server hostname if let Some(hostname) = &server_hostname { + if hostname.contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_type_error(vm)); + } let hostname_str: &str = hostname.as_ref(); if hostname_str.is_empty() || hostname_str.starts_with('.') { return Err(vm.new_value_error( "server_hostname cannot be an empty string or start with a leading dot.", )); } - if hostname_str.contains('\0') { - return Err(vm.new_type_error("embedded null character")); - } let ip = hostname_str.parse::(); if ip.is_err() { ssl.set_hostname(hostname_str) @@ -2061,7 +2117,7 @@ mod _ssl { // Server socket: add SSL_VERIFY_POST_HANDSHAKE flag // Only in combination with SSL_VERIFY_PEER let mode = sys::SSL_get_verify_mode(ssl.as_ptr()); - if (mode & sys::SSL_VERIFY_PEER as libc::c_int) != 0 { + if (mode & sys::SSL_VERIFY_PEER as core::ffi::c_int) != 0 { sys::SSL_set_verify( ssl.as_ptr(), mode | SSL_VERIFY_POST_HANDSHAKE, @@ -2553,7 +2609,7 @@ mod _ssl { unsafe { let ssl_ctx = sys::SSL_get_SSL_CTX(stream.ssl().as_ptr()); let verify_mode = sys::SSL_CTX_get_verify_mode(ssl_ctx); - if (verify_mode & sys::SSL_VERIFY_PEER as libc::c_int) == 0 { + if (verify_mode & sys::SSL_VERIFY_PEER as core::ffi::c_int) == 0 { // Return empty dict when SSL_VERIFY_PEER is not set Ok(Some(vm.ctx.new_dict().into())) } else { @@ -2723,8 +2779,8 @@ mod _ssl { // Use thread-local SSL pointer during handshake to avoid deadlock let ssl_ptr = get_ssl_ptr_for_context_change(&self.connection); unsafe { - let mut out: *const libc::c_uchar = core::ptr::null(); - let mut outlen: libc::c_uint = 0; + let mut out: *const core::ffi::c_uchar = core::ptr::null(); + let mut outlen: core::ffi::c_uint = 0; sys::SSL_get0_alpn_selected(ssl_ptr, &mut out, &mut outlen); @@ -3308,8 +3364,8 @@ mod _ssl { return Ok(PyComparisonValue::NotImplemented); } let mut eq = unsafe { - let mut self_len: libc::c_uint = 0; - let mut other_len: libc::c_uint = 0; + let mut self_len: core::ffi::c_uint = 0; + let mut other_len: core::ffi::c_uint = 0; let self_id = sys::SSL_SESSION_get_id(zelf.session, &mut self_len); let other_id = sys::SSL_SESSION_get_id(other.session, &mut other_len); @@ -3359,7 +3415,7 @@ mod _ssl { unsafe extern "C" { // X509_check_ca returns 1 for CA certificates, 0 otherwise - fn X509_check_ca(x: *const sys::X509) -> libc::c_int; + fn X509_check_ca(x: *const sys::X509) -> core::ffi::c_int; } unsafe extern "C" { @@ -3373,13 +3429,13 @@ mod _ssl { #[cfg(ossl111)] unsafe extern "C" { - fn SSL_verify_client_post_handshake(ssl: *const sys::SSL) -> libc::c_int; - fn SSL_set_post_handshake_auth(ssl: *mut sys::SSL, val: libc::c_int); + fn SSL_verify_client_post_handshake(ssl: *const sys::SSL) -> core::ffi::c_int; + fn SSL_set_post_handshake_auth(ssl: *mut sys::SSL, val: core::ffi::c_int); } #[cfg(ossl110)] unsafe extern "C" { - fn SSL_CTX_get_security_level(ctx: *const sys::SSL_CTX) -> libc::c_int; + fn SSL_CTX_get_security_level(ctx: *const sys::SSL_CTX) -> core::ffi::c_int; } unsafe extern "C" { @@ -3390,13 +3446,13 @@ mod _ssl { #[allow(non_camel_case_types)] type SSL_CTX_msg_callback = Option< unsafe extern "C" fn( - write_p: libc::c_int, - version: libc::c_int, - content_type: libc::c_int, - buf: *const libc::c_void, + write_p: core::ffi::c_int, + version: core::ffi::c_int, + content_type: core::ffi::c_int, + buf: *const core::ffi::c_void, len: usize, ssl: *mut sys::SSL, - arg: *mut libc::c_void, + arg: *mut core::ffi::c_void, ), >; @@ -3406,40 +3462,42 @@ mod _ssl { #[cfg(ossl110)] unsafe extern "C" { - fn SSL_SESSION_has_ticket(session: *const sys::SSL_SESSION) -> libc::c_int; - fn SSL_SESSION_get_ticket_lifetime_hint(session: *const sys::SSL_SESSION) -> libc::c_ulong; + fn SSL_SESSION_has_ticket(session: *const sys::SSL_SESSION) -> core::ffi::c_int; + fn SSL_SESSION_get_ticket_lifetime_hint( + session: *const sys::SSL_SESSION, + ) -> core::ffi::c_ulong; } // X509 object types - const X509_LU_X509: libc::c_int = 1; - const X509_LU_CRL: libc::c_int = 2; + const X509_LU_X509: core::ffi::c_int = 1; + const X509_LU_CRL: core::ffi::c_int = 2; unsafe extern "C" { - fn X509_OBJECT_get_type(obj: *const sys::X509_OBJECT) -> libc::c_int; + fn X509_OBJECT_get_type(obj: *const sys::X509_OBJECT) -> core::ffi::c_int; fn SSL_set_session_id_context( ssl: *mut sys::SSL, - sid_ctx: *const libc::c_uchar, - sid_ctx_len: libc::c_uint, - ) -> libc::c_int; + sid_ctx: *const core::ffi::c_uchar, + sid_ctx_len: core::ffi::c_uint, + ) -> core::ffi::c_int; fn SSL_get1_session(ssl: *const sys::SSL) -> *mut sys::SSL_SESSION; } // SSL session statistics constants (used with SSL_CTX_ctrl) - const SSL_CTRL_SESS_NUMBER: libc::c_int = 20; - const SSL_CTRL_SESS_CONNECT: libc::c_int = 21; - const SSL_CTRL_SESS_CONNECT_GOOD: libc::c_int = 22; - const SSL_CTRL_SESS_CONNECT_RENEGOTIATE: libc::c_int = 23; - const SSL_CTRL_SESS_ACCEPT: libc::c_int = 24; - const SSL_CTRL_SESS_ACCEPT_GOOD: libc::c_int = 25; - const SSL_CTRL_SESS_ACCEPT_RENEGOTIATE: libc::c_int = 26; - const SSL_CTRL_SESS_HIT: libc::c_int = 27; - const SSL_CTRL_SESS_MISSES: libc::c_int = 29; - const SSL_CTRL_SESS_TIMEOUTS: libc::c_int = 30; - const SSL_CTRL_SESS_CACHE_FULL: libc::c_int = 31; + const SSL_CTRL_SESS_NUMBER: core::ffi::c_int = 20; + const SSL_CTRL_SESS_CONNECT: core::ffi::c_int = 21; + const SSL_CTRL_SESS_CONNECT_GOOD: core::ffi::c_int = 22; + const SSL_CTRL_SESS_CONNECT_RENEGOTIATE: core::ffi::c_int = 23; + const SSL_CTRL_SESS_ACCEPT: core::ffi::c_int = 24; + const SSL_CTRL_SESS_ACCEPT_GOOD: core::ffi::c_int = 25; + const SSL_CTRL_SESS_ACCEPT_RENEGOTIATE: core::ffi::c_int = 26; + const SSL_CTRL_SESS_HIT: core::ffi::c_int = 27; + const SSL_CTRL_SESS_MISSES: core::ffi::c_int = 29; + const SSL_CTRL_SESS_TIMEOUTS: core::ffi::c_int = 30; + const SSL_CTRL_SESS_CACHE_FULL: core::ffi::c_int = 31; // SSL session statistics functions (implemented as macros in OpenSSL) #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_number(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_number(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3451,7 +3509,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_connect(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_connect(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3463,7 +3521,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_connect_good(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_connect_good(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3475,7 +3533,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_connect_renegotiate(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_connect_renegotiate(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3487,7 +3545,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_accept(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_accept(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3499,7 +3557,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_accept_good(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_accept_good(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3511,7 +3569,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_accept_renegotiate(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_accept_renegotiate(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3523,12 +3581,12 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_hits(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_hits(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl(ctx as *mut _, SSL_CTRL_SESS_HIT, 0, core::ptr::null_mut()) } } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_misses(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_misses(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3540,7 +3598,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_timeouts(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_timeouts(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3552,7 +3610,7 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn SSL_CTX_sess_cache_full(ctx: *const sys::SSL_CTX) -> libc::c_long { + unsafe fn SSL_CTX_sess_cache_full(ctx: *const sys::SSL_CTX) -> core::ffi::c_long { unsafe { sys::SSL_CTX_ctrl( ctx as *mut _, @@ -3566,17 +3624,17 @@ mod _ssl { // DH parameters functions unsafe extern "C" { fn PEM_read_DHparams( - fp: *mut libc::FILE, + fp: *mut rustpython_host_env::fileutils::CFile, x: *mut *mut sys::DH, - cb: *mut libc::c_void, - u: *mut libc::c_void, + cb: *mut core::ffi::c_void, + u: *mut core::ffi::c_void, ) -> *mut sys::DH; } // OpenSSL BIO helper functions // These are typically macros in OpenSSL, implemented via BIO_ctrl - const BIO_CTRL_PENDING: libc::c_int = 10; - const BIO_CTRL_SET_EOF: libc::c_int = 2; + const BIO_CTRL_PENDING: core::ffi::c_int = 10; + const BIO_CTRL_SET_EOF: core::ffi::c_int = 2; #[allow(non_snake_case)] unsafe fn BIO_ctrl_pending(bio: *mut sys::BIO) -> usize { @@ -3584,14 +3642,17 @@ mod _ssl { } #[allow(non_snake_case)] - unsafe fn BIO_set_mem_eof_return(bio: *mut sys::BIO, eof: libc::c_int) -> libc::c_int { + unsafe fn BIO_set_mem_eof_return( + bio: *mut sys::BIO, + eof: core::ffi::c_int, + ) -> core::ffi::c_int { unsafe { sys::BIO_ctrl( bio, BIO_CTRL_SET_EOF, - eof as libc::c_long, + eof as core::ffi::c_long, core::ptr::null_mut(), - ) as libc::c_int + ) as core::ffi::c_int } } @@ -3721,7 +3782,7 @@ mod _ssl { #[pygetset] fn id(&self, vm: &VirtualMachine) -> PyBytesRef { unsafe { - let mut len: libc::c_uint = 0; + let mut len: core::ffi::c_uint = 0; let id_ptr = sys::SSL_SESSION_get_id(self.session, &mut len); let id_slice = core::slice::from_raw_parts(id_ptr, len as usize); vm.ctx.new_bytes(id_slice.to_vec()) @@ -3976,7 +4037,7 @@ mod _ssl { let mut buf = vec![0u8; 256]; let result = sys::SSL_CIPHER_description( cipher, - buf.as_mut_ptr() as *mut libc::c_char, + buf.as_mut_ptr() as *mut core::ffi::c_char, buf.len() as i32, ); if result.is_null() { @@ -4142,7 +4203,7 @@ mod windows { mod bio { //! based off rust-openssl's private `bio` module - use libc::c_int; + use core::ffi::c_int; use openssl::error::ErrorStack; use openssl_sys as sys; use std::marker::PhantomData; diff --git a/crates/stdlib/src/openssl/cert.rs b/crates/stdlib/src/openssl/cert.rs index e18e7feb9f0..4e42c50f67e 100644 --- a/crates/stdlib/src/openssl/cert.rs +++ b/crates/stdlib/src/openssl/cert.rs @@ -39,7 +39,7 @@ pub(crate) mod ssl_cert { let buflen = buflen as usize; let mut buf = Vec::::with_capacity(buflen + 1); let ret = sys::OBJ_obj2txt( - buf.as_mut_ptr() as *mut libc::c_char, + buf.as_mut_ptr() as *mut core::ffi::c_char, buf.capacity() as _, ptr, no_name, @@ -67,7 +67,10 @@ pub(crate) mod ssl_cert { } } - #[pyclass(with(Comparable, Hashable, Representable))] + #[pyclass( + flags(IMMUTABLETYPE, DISALLOW_INSTANTIATION), + with(Comparable, Hashable, Representable) + )] impl PySSLCertificate { #[pymethod] fn public_bytes( diff --git a/crates/stdlib/src/overlapped.rs b/crates/stdlib/src/overlapped.rs index 4ce3d3ba830..6cdd0014604 100644 --- a/crates/stdlib/src/overlapped.rs +++ b/crates/stdlib/src/overlapped.rs @@ -12,7 +12,7 @@ mod _overlapped { builtins::{PyBaseExceptionRef, PyBytesRef, PyModule, PyStrRef, PyTupleRef, PyType}, common::lock::PyMutex, convert::{ToPyException, ToPyObject}, - function::OptionalArg, + function::{ArgBytesLike, ArgMemoryBuffer, OptionalArg}, object::{Traverse, TraverseFn}, protocol::PyBuffer, types::{Constructor, Destructor}, @@ -309,8 +309,9 @@ mod _overlapped { return Err(vm.new_value_error("operation failed to start")); } - let result = - host_overlapped::get_overlapped_result(inner.handle, &inner.overlapped, wait); + let result = vm.allow_threads(|| { + host_overlapped::get_overlapped_result(inner.handle, &inner.overlapped, wait) + }); let transferred = result.transferred; let err = result.error; inner.error = err; @@ -427,12 +428,14 @@ mod _overlapped { fn ReadFileInto( zelf: &Py, handle: isize, - buf: PyBuffer, + // w*, as _overlapped.Overlapped.ReadFileInto takes + buf: ArgMemoryBuffer, vm: &VirtualMachine, ) -> PyResult { use host_winapi::{ ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_SUCCESS, }; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -529,13 +532,15 @@ mod _overlapped { fn WSARecvInto( zelf: &Py, handle: isize, - buf: PyBuffer, + // w*, as _overlapped.Overlapped.WSARecvInto takes + buf: ArgMemoryBuffer, flags: u32, vm: &VirtualMachine, ) -> PyResult { use host_winapi::{ ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_SUCCESS, }; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -582,10 +587,12 @@ mod _overlapped { fn WriteFile( zelf: &Py, handle: isize, - buf: PyBuffer, + // y*, as _overlapped.Overlapped.WriteFile takes + buf: ArgBytesLike, vm: &VirtualMachine, ) -> PyResult { use host_winapi::{ERROR_IO_PENDING, ERROR_SUCCESS}; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -628,11 +635,13 @@ mod _overlapped { fn WSASend( zelf: &Py, handle: isize, - buf: PyBuffer, + // y*, as _overlapped.Overlapped.WSASend takes + buf: ArgBytesLike, flags: u32, vm: &VirtualMachine, ) -> PyResult { use host_winapi::{ERROR_IO_PENDING, ERROR_SUCCESS}; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -869,12 +878,14 @@ mod _overlapped { fn WSASendTo( zelf: &Py, handle: isize, - buf: PyBuffer, + // y*, as _overlapped.Overlapped.WSASendTo takes + buf: ArgBytesLike, flags: u32, address: PyTupleRef, vm: &VirtualMachine, ) -> PyResult { use host_winapi::{ERROR_IO_PENDING, ERROR_SUCCESS}; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -1000,7 +1011,8 @@ mod _overlapped { fn WSARecvFromInto( zelf: &Py, handle: isize, - buf: PyBuffer, + // w*, as _overlapped.Overlapped.WSARecvFromInto takes + buf: ArgMemoryBuffer, size: u32, flags: OptionalArg, vm: &VirtualMachine, @@ -1008,6 +1020,7 @@ mod _overlapped { use host_winapi::{ ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_SUCCESS, }; + let buf: PyBuffer = buf.into(); let mut inner = zelf.inner.lock(); if !matches!(inner.data, OverlappedData::None) { @@ -1162,7 +1175,8 @@ mod _overlapped { #[pyfunction] fn GetQueuedCompletionStatus(port: isize, msecs: u32, vm: &VirtualMachine) -> PyResult { - match host_overlapped::get_queued_completion_status(port, msecs) + match vm + .allow_threads(|| host_overlapped::get_queued_completion_status(port, msecs)) .map_err(|err| set_from_windows_err(err.raw_os_error().unwrap_or(0) as u32, vm))? { host_overlapped::WaitResult::Timeout => Ok(vm.ctx.none()), diff --git a/crates/stdlib/src/posixshmem.rs b/crates/stdlib/src/posixshmem.rs index 91fdf4aafbc..6bc1c006513 100644 --- a/crates/stdlib/src/posixshmem.rs +++ b/crates/stdlib/src/posixshmem.rs @@ -16,15 +16,15 @@ mod _posixshmem { #[pyarg(any)] name: PyUtf8StrRef, #[pyarg(any)] - flags: libc::c_int, + flags: core::ffi::c_int, #[pyarg(any, default = 0o600)] - mode: libc::mode_t, + mode: shm::mode_t, } #[pyfunction] - fn shm_open(args: ShmOpenArgs, vm: &VirtualMachine) -> PyResult { + fn shm_open(args: ShmOpenArgs, vm: &VirtualMachine) -> PyResult { let name = CString::new(args.name.as_str()).map_err(|e| e.into_pyexception(vm))?; - let mode: libc::c_uint = args.mode as _; + let mode: core::ffi::c_uint = args.mode as _; shm::shm_open(name.as_c_str(), args.flags, mode).map_err(|e| e.into_pyexception(vm)) } diff --git a/crates/stdlib/src/posixsubprocess.rs b/crates/stdlib/src/posixsubprocess.rs index 0371d12e3c2..080c0646845 100644 --- a/crates/stdlib/src/posixsubprocess.rs +++ b/crates/stdlib/src/posixsubprocess.rs @@ -26,7 +26,7 @@ mod _posixsubprocess { use crate::vm::{PyResult, VirtualMachine, convert::IntoPyException}; #[pyfunction] - fn fork_exec(args: ForkExecArgs<'_>, vm: &VirtualMachine) -> PyResult { + fn fork_exec(args: ForkExecArgs<'_>, vm: &VirtualMachine) -> PyResult { // Check for interpreter shutdown when preexec_fn is used if args.preexec_fn.is_some() && vm @@ -85,7 +85,7 @@ impl AsRef for CStrPathLike { #[derive(Default)] struct CharPtrVec<'a> { - vec: Vec<*const libc::c_char>, + vec: Vec<*const host_posix::c_char>, marker: PhantomData>, } @@ -107,7 +107,7 @@ impl<'a> Deref for CharPtrVec<'a> { type Target = CharPtrSlice<'a>; fn deref(&self) -> &Self::Target { unsafe { - &*(self.vec.as_slice() as *const [*const libc::c_char] as *const CharPtrSlice<'a>) + &*(self.vec.as_slice() as *const [*const host_posix::c_char] as *const CharPtrSlice<'a>) } } } @@ -115,11 +115,11 @@ impl<'a> Deref for CharPtrVec<'a> { #[repr(transparent)] struct CharPtrSlice<'a> { marker: PhantomData<[&'a CStr]>, - slice: [*const libc::c_char], + slice: [*const host_posix::c_char], } impl CharPtrSlice<'_> { - const fn as_ptr(&self) -> *const *const libc::c_char { + const fn as_ptr(&self) -> *const *const host_posix::c_char { self.slice.as_ptr() } } @@ -254,7 +254,7 @@ gen_args! { errpipe_write: Fd, restore_signals: bool, call_setsid: bool, - pgid_to_set: libc::pid_t, + pgid_to_set: host_posix::pid_t, gid: Option, groups_list: Option, uid: Option, diff --git a/crates/stdlib/src/pyexpat.rs b/crates/stdlib/src/pyexpat.rs index 8323a4ea106..9b55bbb22f3 100644 --- a/crates/stdlib/src/pyexpat.rs +++ b/crates/stdlib/src/pyexpat.rs @@ -1,5 +1,8 @@ //! Pyexpat builtin module +// false positive: core::io::Cursor is unstable (core_io), unusable on stable +#![expect(clippy::std_instead_of_core)] + // spell-checker: ignore libexpat pub(crate) use _pyexpat::module_def; @@ -40,10 +43,11 @@ macro_rules! create_bool_property { #[pymodule(name = "pyexpat")] mod _pyexpat { use crate::vm::{ - Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, + AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, + VirtualMachine, builtins::{PyBytesRef, PyException, PyModule, PyStr, PyStrRef, PyType, PyUtf8StrRef}, extend_module, - function::{ArgBytesLike, Either, IntoFuncArgs, OptionalArg}, + function::{ArgBytesLike, ArgPrimitiveIndex, Either, IntoFuncArgs, OptionalArg}, types::Constructor, }; use rustpython_common::lock::PyRwLock; @@ -70,6 +74,13 @@ mod _pyexpat { #[pyattr(name = "version_info")] pub(super) const VERSION_INFO: (u32, u32, u32) = (2, 7, 1); + #[pyattr] + const XML_PARAM_ENTITY_PARSING_NEVER: i32 = 0; + #[pyattr] + const XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE: i32 = 1; + #[pyattr] + const XML_PARAM_ENTITY_PARSING_ALWAYS: i32 = 2; + #[pyattr] #[pyattr(name = "XMLParserType")] #[pyclass(name = "xmlparser", module = false, traverse)] @@ -77,6 +88,8 @@ mod _pyexpat { pub(super) struct PyExpatLikeXmlParser { #[pytraverse(skip)] namespace_separator: Option, + #[pytraverse(skip)] + base: PyRwLock>, start_element: MutableObject, end_element: MutableObject, character_data: MutableObject, @@ -128,6 +141,7 @@ mod _pyexpat { let intern_dict = intern.unwrap_or_else(|| vm.ctx.new_dict().into()); Self { namespace_separator, + base: PyRwLock::new(None), start_element: MutableObject::new(vm.ctx.none()), end_element: MutableObject::new(vm.ctx.none()), character_data: MutableObject::new(vm.ctx.none()), @@ -292,11 +306,35 @@ mod _pyexpat { fn create_config(&self) -> xml::ParserConfig { xml::ParserConfig::new() - .cdata_to_characters(true) + .cdata_to_characters(false) .coalesce_characters(false) + .ignore_comments(false) .whitespace_to_characters(true) } + #[pymethod(name = "SetParamEntityParsing")] + fn set_param_entity_parsing(&self, _flag: ArgPrimitiveIndex) -> i32 { + // Compatibility shim: xml.sax requires this setup API, but xml-rs + // does not expose Expat parameter entity parsing configuration. + 1 + } + + #[pymethod(name = "SetBase")] + fn set_base(&self, base: PyStrRef) { + // Store-only compatibility state for xml.sax locator APIs. The + // xml-rs backend still does not perform Expat-style base URI + // resolution for external entities. + *self.base.write() = Some(AsRef::::as_ref(&base).to_owned()); + } + + #[pymethod(name = "GetBase")] + fn get_base(&self, vm: &VirtualMachine) -> PyObjectRef { + self.base.read().as_ref().map_or_else( + || vm.ctx.none(), + |base| vm.ctx.new_str(base.as_str()).into(), + ) + } + /// Construct element name with namespace if separator is set fn make_name(&self, name: &xml::name::OwnedName) -> String { match (&self.namespace_separator, &name.namespace) { @@ -314,33 +352,58 @@ mod _pyexpat { T: std::io::Read, { for e in parser { - match e { - Ok(XmlEvent::StartElement { + match e? { + XmlEvent::StartElement { name, attributes, .. - }) => { - let dict = vm.ctx.new_dict(); - for attribute in attributes { - let attr_name = self.make_name(&attribute.name); - dict.set_item( - attr_name.as_str(), - vm.ctx.new_str(attribute.value).into(), - vm, - ) - .unwrap(); - } + } => { + let ordered = self.ordered_attributes.read().is(&vm.ctx.true_value); + // Build the container. + let attrs: PyObjectRef = if ordered { + let mut items = Vec::with_capacity(attributes.len() * 2); + for attribute in attributes { + items.push(vm.ctx.new_str(self.make_name(&attribute.name)).into()); + items.push(vm.ctx.new_str(attribute.value).into()); + } + vm.ctx.new_list(items).into() + } else { + let dict = vm.ctx.new_dict(); + for attribute in attributes { + dict.set_item( + self.make_name(&attribute.name).as_str(), + vm.ctx.new_str(attribute.value).into(), + vm, + ) + .unwrap(); + } + dict.into() + }; let name_str = PyStr::from(self.make_name(&name)).into_ref(&vm.ctx); - invoke_handler(vm, &self.start_element, (name_str, dict)); + invoke_handler(vm, &self.start_element, (name_str, attrs)); } - Ok(XmlEvent::EndElement { name, .. }) => { + XmlEvent::EndElement { name, .. } => { let name_str = PyStr::from(self.make_name(&name)).into_ref(&vm.ctx); invoke_handler(vm, &self.end_element, (name_str,)); } - Ok(XmlEvent::Characters(chars)) => { + XmlEvent::Characters(chars) => { + let str = PyStr::from(chars).into_ref(&vm.ctx); + invoke_handler(vm, &self.character_data, (str,)); + } + XmlEvent::ProcessingInstruction { name, data } => { + let name = PyStr::from(name).into_ref(&vm.ctx); + let data = PyStr::from(data.unwrap_or_default()).into_ref(&vm.ctx); + invoke_handler(vm, &self.processing_instruction, (name, data)); + } + XmlEvent::Comment(comment) => { + let comment = PyStr::from(comment).into_ref(&vm.ctx); + invoke_handler(vm, &self.comment, (comment,)); + } + XmlEvent::CData(chars) => { + invoke_handler(vm, &self.start_cdata_section, ()); let str = PyStr::from(chars).into_ref(&vm.ctx); invoke_handler(vm, &self.character_data, (str,)); + invoke_handler(vm, &self.end_cdata_section, ()); } - Err(e) => return Err(e), _ => {} } } diff --git a/crates/stdlib/src/pystruct.rs b/crates/stdlib/src/pystruct.rs index 8cf1023c8ca..496b448e5e8 100644 --- a/crates/stdlib/src/pystruct.rs +++ b/crates/stdlib/src/pystruct.rs @@ -10,13 +10,14 @@ pub(crate) use _struct::module_def; #[pymodule] pub(crate) mod _struct { use crate::vm::{ - AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, + AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, buffer::{FormatSpec, new_struct_error, struct_error_type}, builtins::{PyBytes, PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef}, - function::{ArgBytesLike, ArgMemoryBuffer, PosArgs}, + common::lock::{PyMappedRwLockReadGuard, PyRwLock, PyRwLockReadGuard}, + function::{ArgBytesLike, ArgMemoryBuffer, FuncArgs, PosArgs}, match_class, protocol::PyIterReturn, - types::{Constructor, IterNext, Iterable, Representable, SelfIter}, + types::{Constructor, Initializer, IterNext, Iterable, Representable, SelfIter}, }; use crossbeam_utils::atomic::AtomicCell; use rustpython_common::wtf8::{Wtf8Buf, wtf8_concat}; @@ -51,9 +52,8 @@ pub(crate) mod _struct { s } b @ PyBytes => { - let ascii_str = ascii::AsciiStr::from_ascii(&b).map_err(|_| { - new_struct_error(vm, "bad char in struct format".to_owned()) - })?; + let ascii_str = ascii::AsciiStr::from_ascii(&b) + .map_err(|_| new_struct_error(vm, "bad char in struct format"))?; vm.ctx.new_str(ascii_str) } other => @@ -192,7 +192,7 @@ pub(crate) mod _struct { if format_spec.size == 0 { Err(new_struct_error( vm, - "cannot iteratively unpack with a struct of length 0".to_owned(), + "cannot iteratively unpack with a struct of length 0", )) } else if !buffer.len().is_multiple_of(format_spec.size) { Err(new_struct_error( @@ -252,41 +252,76 @@ pub(crate) mod _struct { Ok(fmt.format_spec(vm)?.size) } + /// What a `Struct` is once a format has been read into it. Held apart + /// from the object because `__new__` hands out a `Struct` that `__init__` + /// has not filled in yet, and `__init__` may be called again on one that + /// already holds a format. + #[derive(Debug)] + struct StructSpec { + spec: FormatSpec, + format: PyStrRef, + } + #[pyattr] #[pyclass(name = "Struct", traverse)] #[derive(Debug, PyPayload)] struct PyStruct { #[pytraverse(skip)] - spec: FormatSpec, - format: PyStrRef, + inner: PyRwLock>, } impl Constructor for PyStruct { + type Args = FuncArgs; + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + Ok(Self { + inner: PyRwLock::new(None), + }) + } + } + + impl Initializer for PyStruct { type Args = IntoStructFormatBytes; - fn py_new(_cls: &Py, fmt: Self::Args, vm: &VirtualMachine) -> PyResult { + fn init(zelf: PyRef, fmt: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + // The format is read before anything is replaced, so a format that + // cannot be read leaves the object as it was. let spec = fmt.format_spec(vm)?; - let format = fmt.0; - Ok(Self { spec, format }) + *zelf.inner.write() = Some(StructSpec { + spec, + format: fmt.0, + }); + Ok(()) } } - #[pyclass(with(Constructor, Representable))] + #[pyclass(with(Constructor, Initializer, Representable), flags(BASETYPE))] impl PyStruct { + /// The format this was initialized with, or an error if `__init__` + /// never ran. + fn ready(&self, vm: &VirtualMachine) -> PyResult> { + PyRwLockReadGuard::try_map(self.inner.read(), Option::as_ref) + .map_err(|_| vm.new_runtime_error("Struct object is not initialized")) + } + #[pygetset] - fn format(&self) -> PyStrRef { - self.format.clone() + fn format(&self, vm: &VirtualMachine) -> PyResult { + Ok(self.ready(vm)?.format.clone()) } + /// The size an uninitialized `Struct` reports, which no format has + /// yet given a value. #[pygetset] - #[inline] - const fn size(&self) -> usize { - self.spec.size + fn size(&self) -> isize { + self.inner + .read() + .as_ref() + .map_or(-1, |inner| inner.spec.size as isize) } #[pymethod] fn pack(&self, args: PosArgs, vm: &VirtualMachine) -> PyResult> { - self.spec.pack(args.into_vec(), vm) + self.ready(vm)?.spec.pack(args.into_vec(), vm) } #[pymethod] @@ -297,23 +332,28 @@ pub(crate) mod _struct { args: PosArgs, vm: &VirtualMachine, ) -> PyResult<()> { - let offset = get_buffer_offset(buffer.len(), offset, self.size(), true, vm)?; + let inner = self.ready(vm)?; + let offset = get_buffer_offset(buffer.len(), offset, inner.spec.size, true, vm)?; buffer.with_ref(|data| { - self.spec + inner + .spec .pack_into(&mut data[offset..], args.into_vec(), vm) }) } #[pymethod] fn unpack(&self, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { - data.with_ref(|buf| self.spec.unpack(buf, vm)) + let inner = self.ready(vm)?; + data.with_ref(|buf| inner.spec.unpack(buf, vm)) } #[pymethod] fn unpack_from(&self, args: UpdateFromArgs, vm: &VirtualMachine) -> PyResult { - let offset = get_buffer_offset(args.buffer.len(), args.offset, self.size(), false, vm)?; + let inner = self.ready(vm)?; + let size = inner.spec.size; + let offset = get_buffer_offset(args.buffer.len(), args.offset, size, false, vm)?; args.buffer - .with_ref(|buf| self.spec.unpack(&buf[offset..][..self.size()], vm)) + .with_ref(|buf| inner.spec.unpack(&buf[offset..][..size], vm)) } #[pymethod] @@ -322,14 +362,19 @@ pub(crate) mod _struct { buffer: ArgBytesLike, vm: &VirtualMachine, ) -> PyResult { - UnpackIterator::with_buffer(vm, self.spec.clone(), buffer) + let spec = self.ready(vm)?.spec.clone(); + UnpackIterator::with_buffer(vm, spec, buffer) } } impl Representable for PyStruct { #[inline] - fn repr_wtf8(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok(wtf8_concat!("Struct('", zelf.format.as_wtf8(), "')")) + fn repr_wtf8(zelf: &Py, vm: &VirtualMachine) -> PyResult { + Ok(wtf8_concat!( + "Struct('", + zelf.ready(vm)?.format.as_wtf8(), + "')" + )) } } diff --git a/crates/stdlib/src/resource.rs b/crates/stdlib/src/resource.rs index bac708435c9..cf7fe23d8cc 100644 --- a/crates/stdlib/src/resource.rs +++ b/crates/stdlib/src/resource.rs @@ -16,7 +16,7 @@ mod resource { #[cfg_attr(target_os = "android", expect(deprecated))] const RLIM_NLIMITS: i32 = cfg_select! { target_os = "android" => { - libc::RLIM_NLIMITS + host_resource::RLIM_NLIMITS } _ => { // This constant isn't abi-stable across os versions, so we just @@ -28,18 +28,18 @@ mod resource { // TODO: RLIMIT_OFILE, #[pyattr] - use libc::{ + use host_resource::{ RLIM_INFINITY, RLIMIT_AS, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA, RLIMIT_FSIZE, RLIMIT_MEMLOCK, RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_RSS, RLIMIT_STACK, }; #[cfg(any(target_os = "linux", target_os = "android", target_os = "emscripten"))] #[pyattr] - use libc::{RLIMIT_MSGQUEUE, RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_SIGPENDING}; + use host_resource::{RLIMIT_MSGQUEUE, RLIMIT_NICE, RLIMIT_RTPRIO, RLIMIT_SIGPENDING}; // TODO: I think this is supposed to be defined for all linux_like? #[cfg(target_os = "linux")] #[pyattr] - use libc::RLIMIT_RTTIME; + use host_resource::RLIMIT_RTTIME; #[cfg(any( target_os = "freebsd", @@ -48,41 +48,41 @@ mod resource { target_os = "illumos" ))] #[pyattr] - use libc::RLIMIT_SBSIZE; + use host_resource::RLIMIT_SBSIZE; #[cfg(any(target_os = "freebsd", target_os = "solaris", target_os = "illumos"))] #[pyattr] - use libc::{RLIMIT_NPTS, RLIMIT_SWAP}; + use host_resource::{RLIMIT_NPTS, RLIMIT_SWAP}; #[cfg(any(target_os = "solaris", target_os = "illumos"))] #[pyattr] - use libc::RLIMIT_VMEM; + use host_resource::RLIMIT_VMEM; #[cfg(any(target_os = "linux", target_os = "emscripten", target_os = "freebsd"))] #[pyattr] - use libc::RUSAGE_THREAD; + use host_resource::RUSAGE_THREAD; #[cfg(not(any(target_os = "windows", target_os = "redox")))] #[pyattr] - use libc::{RUSAGE_CHILDREN, RUSAGE_SELF}; + use host_resource::{RUSAGE_CHILDREN, RUSAGE_SELF}; #[pystruct_sequence_data] struct RUsageData { ru_utime: f64, ru_stime: f64, - ru_maxrss: libc::c_long, - ru_ixrss: libc::c_long, - ru_idrss: libc::c_long, - ru_isrss: libc::c_long, - ru_minflt: libc::c_long, - ru_majflt: libc::c_long, - ru_nswap: libc::c_long, - ru_inblock: libc::c_long, - ru_oublock: libc::c_long, - ru_msgsnd: libc::c_long, - ru_msgrcv: libc::c_long, - ru_nsignals: libc::c_long, - ru_nvcsw: libc::c_long, - ru_nivcsw: libc::c_long, + ru_maxrss: host_resource::c_long, + ru_ixrss: host_resource::c_long, + ru_idrss: host_resource::c_long, + ru_isrss: host_resource::c_long, + ru_minflt: host_resource::c_long, + ru_majflt: host_resource::c_long, + ru_nswap: host_resource::c_long, + ru_inblock: host_resource::c_long, + ru_oublock: host_resource::c_long, + ru_msgsnd: host_resource::c_long, + ru_msgrcv: host_resource::c_long, + ru_nsignals: host_resource::c_long, + ru_nvcsw: host_resource::c_long, + ru_nivcsw: host_resource::c_long, } #[pyattr] @@ -94,7 +94,8 @@ mod resource { impl From for RUsageData { fn from(rusage: host_resource::RUsage) -> Self { - let tv = |tv: libc::timeval| tv.tv_sec as f64 + (tv.tv_usec as f64 / 1_000_000.0); + let tv = + |tv: host_resource::timeval| tv.tv_sec as f64 + (tv.tv_usec as f64 / 1_000_000.0); Self { ru_utime: tv(rusage.ru_utime), ru_stime: tv(rusage.ru_stime), @@ -128,13 +129,13 @@ mod resource { }) } - struct Limits(libc::rlimit); + struct Limits(host_resource::rlimit); impl<'a> TryFromBorrowedObject<'a> for Limits { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { - let seq: Vec = obj.try_to_value(vm)?; + let seq: Vec = obj.try_to_value(vm)?; match *seq { - [cur, max] => Ok(Self(libc::rlimit { + [cur, max] => Ok(Self(host_resource::rlimit { rlim_cur: cur & RLIM_INFINITY, rlim_max: max & RLIM_INFINITY, })), @@ -149,14 +150,14 @@ mod resource { } } - fn py2rlim(obj: PyIntRef, vm: &VirtualMachine) -> PyResult { + fn py2rlim(obj: PyIntRef, vm: &VirtualMachine) -> PyResult { let value = obj.try_to_primitive::(vm)?; if value.is_negative() { return Err(vm.new_value_error("Cannot convert negative int")); } - libc::rlim_t::try_from(value) + host_resource::rlim_t::try_from(value) .map_err(|_| vm.new_overflow_error("Python int too large to convert to C rlim_t")) } @@ -164,7 +165,7 @@ mod resource { fn getrlimit(resource: PyIntRef, vm: &VirtualMachine) -> PyResult { let resource = py2rlim(resource, vm)?; - if resource >= RLIM_NLIMITS as libc::rlim_t { + if resource >= RLIM_NLIMITS as host_resource::rlim_t { return Err(vm.new_value_error("invalid resource specified")); } @@ -176,7 +177,7 @@ mod resource { fn setrlimit(resource: PyIntRef, limits: Limits, vm: &VirtualMachine) -> PyResult<()> { let resource = py2rlim(resource, vm)?; - if resource >= RLIM_NLIMITS as libc::rlim_t { + if resource >= RLIM_NLIMITS as host_resource::rlim_t { return Err(vm.new_value_error("invalid resource specified")); } diff --git a/crates/stdlib/src/scproxy.rs b/crates/stdlib/src/scproxy.rs index 09e7cdc6046..f31432cbb51 100644 --- a/crates/stdlib/src/scproxy.rs +++ b/crates/stdlib/src/scproxy.rs @@ -9,14 +9,14 @@ mod _scproxy { builtins::{PyDict, PyDictRef, PyStr}, convert::ToPyObject, }; - use system_configuration::core_foundation::{ + use rustpython_host_env::system_configuration::core_foundation::{ array::CFArray, base::{CFType, FromVoid, TCFType}, dictionary::CFDictionary, number::CFNumber, string::{CFString, CFStringRef}, }; - use system_configuration::sys::{ + use rustpython_host_env::system_configuration::sys::{ dynamic_store_copy_specific::SCDynamicStoreCopyProxies, schema_definitions::*, }; diff --git a/crates/stdlib/src/select.rs b/crates/stdlib/src/select.rs index f8125ea375f..84ec92927e8 100644 --- a/crates/stdlib/src/select.rs +++ b/crates/stdlib/src/select.rs @@ -79,16 +79,26 @@ mod decl { } let deadline = timeout.map(|s| time::time(vm).unwrap() + s); + let max_fds: usize = cfg_select! { + windows => FD_SETSIZE as usize, + _ => FD_SETSIZE, + }; + let seq2set = |list: &PyObject| -> PyResult<(Vec, FdSet)> { - let v: Vec = list.try_to_value(vm)?; - - let too_many_fds = cfg_select! { - windows => v.len() > FD_SETSIZE as usize, - _ => v.len() > FD_SETSIZE, - }; - if too_many_fds { - return Err(vm.new_value_error("too many file descriptors in select()")); - } + // The limit is answered while the sequence is walked rather than + // from the length of the result. fileno() runs Python and can + // append to the very list being walked, and a walk that re-reads + // the list each step -- which is what `seq2set` does -- then never + // reaches a length to check. + let seen = core::cell::Cell::new(0usize); + let v: Vec = vm.extract_elements_with(list, |obj| { + let selectable = Selectable::try_from_object(vm, obj)?; + seen.set(seen.get() + 1); + if seen.get() > max_fds { + return Err(vm.new_value_error("too many file descriptors in select()")); + } + Ok(selectable) + })?; let mut fds = FdSet::new(); for fd in &v { @@ -170,7 +180,7 @@ mod decl { #[cfg(unix)] #[pyattr] - use libc::{POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI}; + use host_select::{POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI}; #[cfg(unix)] pub(super) mod poll { @@ -261,7 +271,8 @@ mod decl { } } - const DEFAULT_EVENTS: i16 = libc::POLLIN | libc::POLLPRI | libc::POLLOUT; + const DEFAULT_EVENTS: i16 = + host_select::POLLIN | host_select::POLLPRI | host_select::POLLOUT; #[pyclass] impl PyPoll { @@ -303,7 +314,10 @@ mod decl { timeout: OptionalArg>, vm: &VirtualMachine, ) -> PyResult> { - let mut fds = self.fds.lock(); + // Poll a copy: the wait releases the GIL-equivalent and runs + // signal handlers, which can register or unregister on the same + // object, and a held lock would deadlock them. + let mut fds = self.fds.lock().clone(); let TimeoutArg(timeout) = timeout.unwrap_or_default(); let timeout_ms = match timeout { Some(d) => i32::try_from(d.as_millis()) @@ -315,7 +329,7 @@ mod decl { loop { match vm.allow_threads(|| host_select::poll_fds(&mut fds, poll_timeout)) { Ok(_) => break, - Err(err) if err.raw_os_error() == Some(libc::EINTR) => { + Err(err) if err.raw_os_error() == Some(host_select::EINTR) => { vm.check_signals()? } Err(err) => return Err(err.into_pyexception(vm)), @@ -346,14 +360,14 @@ mod decl { #[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))] #[pyattr] - use libc::{ + use host_select::{ EPOLL_CLOEXEC, EPOLLERR, EPOLLEXCLUSIVE, EPOLLHUP, EPOLLIN, EPOLLMSG, EPOLLONESHOT, EPOLLOUT, EPOLLPRI, EPOLLRDBAND, EPOLLRDHUP, EPOLLRDNORM, EPOLLWAKEUP, EPOLLWRBAND, EPOLLWRNORM, }; #[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))] #[pyattr] - const EPOLLET: u32 = libc::EPOLLET as u32; + const EPOLLET: u32 = host_select::EPOLLET as u32; #[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))] pub(super) mod epoll { @@ -392,7 +406,7 @@ mod decl { if let ..=-2 | 0 = args.sizehint { return Err(vm.new_value_error("negative sizehint")); } - if !matches!(args.flags, 0 | libc::EPOLL_CLOEXEC) { + if !matches!(args.flags, 0 | host_select::EPOLL_CLOEXEC) { return Err(vm.new_os_error("invalid flags")); } Self::new().map_err(|e| e.into_pyexception(vm)) @@ -497,7 +511,7 @@ mod decl { "maxevents must be greater than 0, got {maxevents}" ))); } - -1 => libc::FD_SETSIZE - 1, + -1 => host_select::FD_SETSIZE - 1, _ => maxevents as usize, }; diff --git a/crates/stdlib/src/sha1.rs b/crates/stdlib/src/sha1.rs index 3e3d4928c79..71495435e56 100644 --- a/crates/stdlib/src/sha1.rs +++ b/crates/stdlib/src/sha1.rs @@ -3,10 +3,17 @@ pub(crate) use _sha1::module_def; #[pymodule] mod _sha1 { use crate::hashlib::_hashlib::{HashArgs, local_sha1}; - use crate::vm::{PyPayload, PyResult, VirtualMachine}; + use crate::vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule}; #[pyfunction] fn sha1(args: HashArgs, vm: &VirtualMachine) -> PyResult { Ok(local_sha1(args, vm)?.into_pyobject(vm)) } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + let _ = vm.import("_hashlib", 0); + __module_exec(vm, module); + Ok(()) + } } diff --git a/crates/stdlib/src/sha3.rs b/crates/stdlib/src/sha3.rs index 0eb2dfa84d5..642ed838a4d 100644 --- a/crates/stdlib/src/sha3.rs +++ b/crates/stdlib/src/sha3.rs @@ -6,7 +6,7 @@ mod _sha3 { HashArgs, local_sha3_224, local_sha3_256, local_sha3_384, local_sha3_512, local_shake_128, local_shake_256, }; - use crate::vm::{PyPayload, PyResult, VirtualMachine}; + use crate::vm::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule}; #[pyfunction] fn sha3_224(args: HashArgs, vm: &VirtualMachine) -> PyResult { @@ -37,4 +37,11 @@ mod _sha3 { fn shake_256(args: HashArgs, vm: &VirtualMachine) -> PyResult { Ok(local_shake_256(args, vm)?.into_pyobject(vm)) } + + #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + let _ = vm.import("_hashlib", 0); + __module_exec(vm, module); + Ok(()) + } } diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__bare_function_annotations_check_attribute_and_subscript_expressions.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__bare_function_annotations_check_attribute_and_subscript_expressions.snap index 3274352b920..4d78128b5e6 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__bare_function_annotations_check_attribute_and_subscript_expressions.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__bare_function_annotations_check_attribute_and_subscript_expressions.snap @@ -1,5 +1,6 @@ --- source: crates/stdlib/src/_opcode.rs +assertion_line: 318 expression: "dis(r#\"\ndef f(one: int):\n int.new_attr: int\n [list][0].new_attr: [int, str]\n my_lst = [1]\n my_lst[one]: int\n return my_lst\n\"#)" --- 0 RESUME 0 @@ -15,7 +16,7 @@ expression: "dis(r#\"\ndef f(one: int):\n int.new_attr: int\n [list][0].ne Disassembly of ", line 1>: 1 RESUME 0 - LOAD_FAST_BORROW 0 (format) + LOAD_FAST_CHECK 0 (format) LOAD_SMALL_INT 2 COMPARE_OP 132 (>) POP_JUMP_IF_FALSE 3 (to L1) diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__const_no_op.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__const_no_op.snap index 347e58767ae..dc97f6b79c1 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__const_no_op.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__const_no_op.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: x = not True +assertion_line: 281 +expression: "dis(r#\"\nx = not True\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__constant_true_if_pass_keeps_line_anchor_nop.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__constant_true_if_pass_keeps_line_anchor_nop.snap index 02e2473501d..3de37ce2009 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__constant_true_if_pass_keeps_line_anchor_nop.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__constant_true_if_pass_keeps_line_anchor_nop.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: "if 1:\n pass" +assertion_line: 290 +expression: "dis(r#\"\nif 1:\n pass\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ands.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ands.snap index b5957dda5e5..5c58a2b6b85 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ands.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ands.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: "if True and False and False:\n pass" +assertion_line: 252 +expression: "dis(r#\"\nif True and False and False:\n pass\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_mixed.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_mixed.snap index f8976b8c6e5..6bef04ee143 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_mixed.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_mixed.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: "if (True and False) or (False and True):\n pass" +assertion_line: 262 +expression: "dis(r#\"\nif (True and False) or (False and True):\n pass\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ors.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ors.snap index f8cc3a1f28f..065d893732e 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ors.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__if_ors.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: "if True or False or False:\n pass" +assertion_line: 242 +expression: "dis(r#\"\nif True or False or False:\n pass\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__nested_bool_op.snap b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__nested_bool_op.snap index c0e3659487b..00eeb277455 100644 --- a/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__nested_bool_op.snap +++ b/crates/stdlib/src/snapshots/rustpython_stdlib___opcode__tests__nested_bool_op.snap @@ -1,6 +1,7 @@ --- source: crates/stdlib/src/_opcode.rs -expression: x = Test() and False or False +assertion_line: 272 +expression: "dis(r#\"\nx = Test() and False or False\n\"#)" --- 0 RESUME 0 diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index e3ce52b943a..a1998ba7c3b 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -40,13 +40,12 @@ mod _socket { } use core::{ - mem::MaybeUninit, net::{Ipv4Addr, Ipv6Addr, SocketAddr}, time::Duration, }; use crossbeam_utils::atomic::AtomicCell; + use host_socket::raw::Socket; use num_traits::ToPrimitive; - use socket2::Socket; use std::{ ffi, io::{self, Read, Write}, @@ -885,9 +884,9 @@ mod _socket { fn get_raw_sock(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { #[cfg(unix)] - type CastFrom = libc::c_long; + type CastFrom = core::ffi::c_long; #[cfg(windows)] - type CastFrom = libc::c_longlong; + type CastFrom = core::ffi::c_longlong; // should really just be to_index() but test_socket tests the error messages explicitly if obj.fast_isinstance(vm.ctx.types.float_type) { @@ -1109,7 +1108,7 @@ mod _socket { addr: PyObjectRef, caller: &str, vm: &VirtualMachine, - ) -> Result { + ) -> Result { let family = self.family.load(); match family { #[cfg(unix)] @@ -1122,7 +1121,7 @@ mod _socket { ArgStrOrBytesLike::Buf(_) => ffi::OsStr::from_bytes(bytes).into(), ArgStrOrBytesLike::Str(s) => vm.fsencode(s)?, }; - socket2::SockAddr::unix(path) + host_socket::raw::SockAddr::unix(path) .map_err(|_| vm.new_os_error("AF_UNIX path too long").into()) } c::AF_INET => { @@ -1214,19 +1213,18 @@ mod _socket { }; // Create sockaddr_can - let mut storage: libc::sockaddr_storage = unsafe { core::mem::zeroed() }; - let can_addr = - &mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_can; + let mut storage: c::sockaddr_storage = unsafe { core::mem::zeroed() }; + let can_addr = &mut storage as *mut c::sockaddr_storage as *mut c::sockaddr_can; unsafe { - (*can_addr).can_family = libc::AF_CAN as libc::sa_family_t; + (*can_addr).can_family = c::AF_CAN as c::sa_family_t; (*can_addr).can_ifindex = ifindex; } - let storage: socket2::SockAddrStorage = + let storage: host_socket::raw::SockAddrStorage = unsafe { core::mem::transmute(storage) }; Ok(unsafe { - socket2::SockAddr::new( + host_socket::raw::SockAddr::new( storage, - core::mem::size_of::() as libc::socklen_t, + core::mem::size_of::() as c::socklen_t, ) }) } @@ -1273,11 +1271,10 @@ mod _socket { } // Create sockaddr_alg - let mut storage: libc::sockaddr_storage = unsafe { core::mem::zeroed() }; - let alg_addr = - &mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_alg; + let mut storage: c::sockaddr_storage = unsafe { core::mem::zeroed() }; + let alg_addr = &mut storage as *mut c::sockaddr_storage as *mut c::sockaddr_alg; unsafe { - (*alg_addr).salg_family = libc::AF_ALG as libc::sa_family_t; + (*alg_addr).salg_family = c::AF_ALG as c::sa_family_t; // Copy type string for (i, b) in type_str.bytes().enumerate() { (*alg_addr).salg_type[i] = b; @@ -1287,12 +1284,12 @@ mod _socket { (*alg_addr).salg_name[i] = b; } } - let storage: socket2::SockAddrStorage = + let storage: host_socket::raw::SockAddrStorage = unsafe { core::mem::transmute(storage) }; Ok(unsafe { - socket2::SockAddr::new( + host_socket::raw::SockAddr::new( storage, - core::mem::size_of::() as libc::socklen_t, + core::mem::size_of::() as c::socklen_t, ) }) } @@ -1380,23 +1377,11 @@ mod _socket { fn del(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { // Emit ResourceWarning if socket is still open if zelf.sock.read().is_some() { - let laddr = if let Ok(sock) = zelf.sock() - && let Ok(addr) = sock.local_addr() - && let Ok(repr) = get_addr_tuple(&addr, vm).repr(vm) - { - format!(", laddr={}", repr.as_wtf8()) - } else { - String::new() - }; - - let msg = format!( - "unclosed ", - zelf.fileno(), - zelf.family.load(), - zelf.kind.load(), - zelf.proto.load(), - laddr - ); + let repr = zelf + .as_object() + .repr(vm) + .unwrap_or_else(|_| vm.ctx.new_str("")); + let msg = format!("unclosed {}", repr.as_wtf8()); let _ = crate::vm::warn::warn( vm.ctx.new_str(msg).into(), Some(vm.ctx.exceptions.resource_warning.to_owned()), @@ -1603,7 +1588,10 @@ mod _socket { vm: &VirtualMachine, ) -> Result, IoOrPyException> { let flags = flags.unwrap_or(0); - let mut buffer = Vec::with_capacity(bufsize); + let mut buffer = Vec::new(); + buffer + .try_reserve_exact(bufsize) + .map_err(|_| vm.new_memory_error(""))?; let sock = self.sock()?; let n = self.sock_op(vm, SockWaitKind::Read, || { sock.recv_with_flags(buffer.spare_capacity_mut(), flags) @@ -1622,8 +1610,6 @@ mod _socket { ) -> Result { let flags = flags.unwrap_or(0); let sock = self.sock()?; - let mut buf = buf.borrow_buf_mut(); - let buf = &mut *buf; // Handle nbytes parameter let read_len = if let OptionalArg::Present(nbytes) = nbytes { @@ -1635,10 +1621,13 @@ mod _socket { buf.len() }; - let buf = &mut buf[..read_len]; - self.sock_op(vm, SockWaitKind::Read, || { - sock.recv_with_flags(unsafe { slice_as_uninit(buf) }, flags) - }) + let mut scratch = alloc_recv_scratch(read_len, vm)?; + let n = self.sock_op(vm, SockWaitKind::Read, || { + sock.recv_with_flags(&mut scratch.spare_capacity_mut()[..read_len], flags) + })?; + unsafe { scratch.set_len(n) }; + buf.borrow_buf_mut()[..n].copy_from_slice(&scratch); + Ok(n) } #[pymethod] @@ -1652,7 +1641,10 @@ mod _socket { let bufsize = bufsize .to_usize() .ok_or_else(|| vm.new_value_error("negative buffersize in recvfrom"))?; - let mut buffer = Vec::with_capacity(bufsize); + let mut buffer = Vec::new(); + buffer + .try_reserve_exact(bufsize) + .map_err(|_| vm.new_memory_error(""))?; let (n, addr) = self.sock_op(vm, SockWaitKind::Read, || { self.sock()? .recv_from_with_flags(buffer.spare_capacity_mut(), flags) @@ -1669,24 +1661,28 @@ mod _socket { flags: OptionalArg, vm: &VirtualMachine, ) -> Result<(usize, PyObjectRef), IoOrPyException> { - let mut buf = buf.borrow_buf_mut(); - let buf = &mut *buf; - let buf = match nbytes { + let read_len = match nbytes { OptionalArg::Present(i) => { let i = i.to_usize().ok_or_else(|| { vm.new_value_error("negative buffersize in recvfrom_into") })?; - buf.get_mut(..i).ok_or_else(|| { - vm.new_value_error("nbytes is greater than the length of the buffer") - })? + if i > buf.len() { + return Err(vm + .new_value_error("nbytes is greater than the length of the buffer") + .into()); + } + i } - OptionalArg::Missing => buf, + OptionalArg::Missing => buf.len(), }; let flags = flags.unwrap_or(0); let sock = self.sock()?; + let mut scratch = alloc_recv_scratch(read_len, vm)?; let (n, addr) = self.sock_op(vm, SockWaitKind::Read, || { - sock.recv_from_with_flags(unsafe { slice_as_uninit(buf) }, flags) + sock.recv_from_with_flags(&mut scratch.spare_capacity_mut()[..read_len], flags) })?; + unsafe { scratch.set_len(n) }; + buf.borrow_buf_mut()[..n].copy_from_slice(&scratch); Ok((n, get_addr_tuple(&addr, vm))) } @@ -1698,7 +1694,7 @@ mod _socket { vm: &VirtualMachine, ) -> Result { let flags = flags.unwrap_or(0); - let buf = bytes.borrow_buf(); + let buf = bytes.borrow_buf_unlocked(vm)?; let buf = &*buf; self.sock_op(vm, SockWaitKind::Write, || { self.sock()?.send_with_flags(buf, flags) @@ -1718,7 +1714,7 @@ mod _socket { let deadline = timeout.map(Deadline::new); - let buf = bytes.borrow_buf(); + let buf = bytes.borrow_buf_unlocked(vm)?; let buf = &*buf; let mut buf_offset = 0; // now we have like 3 layers of interrupt loop :) @@ -1755,7 +1751,7 @@ mod _socket { OptionalArg::Missing => (0, arg2), }; let addr = self.extract_address(address, "sendto", vm)?; - let buf = bytes.borrow_buf(); + let buf = bytes.borrow_buf_unlocked(vm)?; let buf = &*buf; self.sock_op(vm, SockWaitKind::Write, || { self.sock()?.send_to_with_flags(buf, &addr, flags) @@ -1773,7 +1769,7 @@ mod _socket { vm: &VirtualMachine, ) -> PyResult { let flags = flags.unwrap_or(0); - let mut msg = socket2::MsgHdr::new(); + let mut msg = host_socket::raw::MsgHdr::new(); let sockaddr; if let Some(addr) = addr.flatten() { @@ -1785,8 +1781,8 @@ mod _socket { let buffers = buffers .iter() - .map(|buf| buf.borrow_buf()) - .collect::>(); + .map(|buf| buf.borrow_buf_unlocked(vm)) + .collect::>>()?; let buffers = buffers .iter() .map(|buf| io::IoSlice::new(buf)) @@ -1896,9 +1892,9 @@ mod _socket { // Build address tuple let address = if let Some(address) = msg.address { - let storage: socket2::SockAddrStorage = + let storage: host_socket::raw::SockAddrStorage = unsafe { core::mem::transmute(address.storage) }; - let addr = unsafe { socket2::SockAddr::new(storage, address.len as _) }; + let addr = unsafe { host_socket::raw::SockAddr::new(storage, address.len as _) }; get_addr_tuple(&addr, vm) } else { vm.ctx.none() @@ -2232,7 +2228,7 @@ mod _socket { let addr = Self::from_tuple(tuple, vm)?; let flowinfo = tuple .get(2) - .map(|obj| u32::try_from_borrowed_object(vm, obj)) + .map(|obj| obj.clone().try_index(vm)?.try_to_primitive_raw(vm)) .transpose()? .unwrap_or(0); let scopeid = tuple @@ -2260,7 +2256,7 @@ mod _socket { } } - fn get_addr_tuple(addr: &socket2::SockAddr, vm: &VirtualMachine) -> PyObjectRef { + fn get_addr_tuple(addr: &host_socket::raw::SockAddr, vm: &VirtualMachine) -> PyObjectRef { if let Some(addr) = addr.as_socket() { return get_ip_addr_tuple(&addr, vm); } @@ -2280,9 +2276,9 @@ mod _socket { #[cfg(target_os = "linux")] { let family = addr.family(); - if family == libc::AF_CAN as libc::sa_family_t { + if family == c::AF_CAN as c::sa_family_t { // AF_CAN address: (interface_name,) or (interface_name, can_id) - let can_addr = unsafe { &*(addr.as_ptr() as *const libc::sockaddr_can) }; + let can_addr = unsafe { &*(addr.as_ptr() as *const c::sockaddr_can) }; let ifindex = can_addr.can_ifindex; let ifname = if ifindex == 0 { String::new() @@ -2291,9 +2287,9 @@ mod _socket { }; return vm.ctx.new_tuple(vec![vm.ctx.new_str(ifname).into()]).into(); } - if family == libc::AF_ALG as libc::sa_family_t { + if family == c::AF_ALG as c::sa_family_t { // AF_ALG address: (type, name) - let alg_addr = unsafe { &*(addr.as_ptr() as *const libc::sockaddr_alg) }; + let alg_addr = unsafe { &*(addr.as_ptr() as *const c::sockaddr_alg) }; let type_bytes = &alg_addr.salg_type; let name_bytes = &alg_addr.salg_name; let type_nul = memchr::memchr(b'\0', type_bytes).unwrap_or(type_bytes.len()); @@ -2319,7 +2315,7 @@ mod _socket { audit.call((vm.ctx.new_str("socket.gethostname"),), vm)?; } - gethostname::gethostname() + rustpython_host_env::socket::hostname() .into_string() .map(|hostname| vm.ctx.new_str(hostname)) .map_err(|err| vm.new_os_error(err.into_string().unwrap())) @@ -2327,8 +2323,8 @@ mod _socket { #[cfg(all(unix, not(any(target_os = "redox", target_os = "android"))))] #[pyfunction] - fn sethostname(hostname: PyUtf8StrRef) -> std::io::Result<()> { - host_socket::sethostname(hostname.as_str()) + fn sethostname(hostname: FsPath) -> std::io::Result<()> { + host_socket::sethostname(hostname.as_bytes()) } #[pyfunction] @@ -2348,7 +2344,7 @@ mod _socket { Ok(vm.ctx.new_str(Ipv4Addr::from(*packed_ip).to_string())) } - fn cstr_opt_as_ptr(x: &OptionalArg) -> *const libc::c_char { + fn cstr_opt_as_ptr(x: &OptionalArg) -> *const core::ffi::c_char { x.as_ref().map_or_else(core::ptr::null, |s| s.as_ptr()) } @@ -2394,8 +2390,21 @@ mod _socket { Ok(s.to_string_lossy().into_owned()) } - unsafe fn slice_as_uninit(v: &mut [T]) -> &mut [MaybeUninit] { - unsafe { &mut *(v as *mut [T] as *mut [MaybeUninit]) } + /// Room to receive into that belongs to no Python object. + /// + /// A peer may never send, so the wait for it is unbounded. The export of + /// the caller's buffer is held for the whole call, which is what keeps it + /// from being resized, but the borrow that reaches its bytes is a lock + /// every other thread touching that object waits on, and a thread waiting + /// on a lock never reaches a safepoint — holding it across the wait stops + /// the world from being stopped at all. The bytes are copied over once + /// they have arrived. + fn alloc_recv_scratch(len: usize, vm: &VirtualMachine) -> PyResult> { + let mut scratch = Vec::new(); + scratch + .try_reserve_exact(len) + .map_err(|_| vm.new_memory_error(""))?; + Ok(scratch) } enum IoOrPyException { @@ -2446,6 +2455,7 @@ mod _socket { } /// returns Ok(true) on timeout + #[cfg(feature = "ssl")] pub(crate) fn sock_wait( sock: &Socket, wait_kind: SockWaitKind, @@ -2593,7 +2603,7 @@ mod _socket { opts: GAIOptions, vm: &VirtualMachine, ) -> Result, IoOrPyException> { - let hints = dns_lookup::AddrInfoHints { + let hints = host_socket::dns::AddrInfoHints { socktype: opts.ty, protocol: opts.proto, address: opts.family, @@ -2613,8 +2623,15 @@ mod _socket { } Some(ArgStrOrBytesLike::Buf(b)) => { let bytes = b.borrow_buf(); - let host_str = core::str::from_utf8(&bytes) - .map_err(|_| vm.new_unicode_decode_error("host bytes is not utf8"))?; + let host_str = core::str::from_utf8(&bytes).map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(bytes.to_vec()), + e.valid_up_to(), + e.error_len().map_or(bytes.len(), |n| e.valid_up_to() + n), + vm.ctx.new_str("host bytes is not utf8"), + ) + })?; Some(host_str.to_owned()) } None => None, @@ -2628,14 +2645,35 @@ mod _socket { ArgStrOrBytesLike::Str(s) => { // For str, check for surrogates and raise UnicodeEncodeError if found s.to_str() - .ok_or_else(|| vm.new_unicode_encode_error("surrogates not allowed"))? + .ok_or_else(|| { + let start = s + .as_wtf8() + .code_points() + .position(|c| c.to_char().is_none()) + .unwrap(); + vm.new_unicode_encode_error_real( + vm.ctx.new_str("utf-8"), + (*s).clone(), + start, + start + 1, + vm.ctx.new_str("surrogates not allowed"), + ) + })? .to_owned() } ArgStrOrBytesLike::Buf(b) => { // For bytes, check if it's valid UTF-8 let bytes = b.borrow_buf(); core::str::from_utf8(&bytes) - .map_err(|_| vm.new_unicode_decode_error("port is not utf8"))? + .map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(bytes.to_vec()), + e.valid_up_to(), + e.error_len().map_or(bytes.len(), |n| e.valid_up_to() + n), + vm.ctx.new_str("port is not utf8"), + ) + })? .to_owned() } }; @@ -2646,7 +2684,7 @@ mod _socket { }; let port = port_encoded.as_deref(); - let addrs = dns_lookup::getaddrinfo(host, port, Some(hints)) + let addrs = host_socket::dns::getaddrinfo(host, port, Some(hints)) .map_err(|err| convert_socket_error(vm, err, SocketError::GaiError))?; let list = addrs @@ -2672,7 +2710,7 @@ mod _socket { vm: &VirtualMachine, ) -> Result<(String, PyListRef, PyListRef), IoOrPyException> { let addr = get_addr(vm, addr, c::AF_UNSPEC)?; - let (hostname, _) = dns_lookup::getnameinfo(&addr, 0) + let (hostname, _) = host_socket::dns::getnameinfo(&addr, 0) .map_err(|e| convert_socket_error(vm, e, SocketError::HError))?; Ok(( hostname, @@ -2697,7 +2735,7 @@ mod _socket { vm: &VirtualMachine, ) -> Result<(String, PyListRef, PyListRef), IoOrPyException> { let addr = get_addr(vm, name, c::AF_INET)?; - let (hostname, _) = dns_lookup::getnameinfo(&addr, 0) + let (hostname, _) = host_socket::dns::getnameinfo(&addr, 0) .map_err(|e| convert_socket_error(vm, e, SocketError::HError))?; Ok(( hostname, @@ -2772,7 +2810,7 @@ mod _socket { } } let (addr, flowinfo, scopeid) = Address::from_tuple_ipv6(&address, vm)?; - let hints = dns_lookup::AddrInfoHints { + let hints = host_socket::dns::AddrInfoHints { address: c::AF_UNSPEC, socktype: c::SOCK_DGRAM, flags: c::AI_NUMERICHOST, @@ -2780,7 +2818,7 @@ mod _socket { }; let service = addr.port.to_string(); let host_str = addr.host.as_str(); - let mut res = dns_lookup::getaddrinfo(Some(host_str), Some(&service), Some(hints)) + let mut res = host_socket::dns::getaddrinfo(Some(host_str), Some(&service), Some(hints)) .map_err(|e| convert_socket_error(vm, e, SocketError::GaiError))? .filter_map(Result::ok); let mut ainfo = res.next().unwrap(); @@ -2800,7 +2838,7 @@ mod _socket { addr.set_scope_id(scopeid); } } - dns_lookup::getnameinfo(&ainfo.sockaddr, flags) + host_socket::dns::getnameinfo(&ainfo.sockaddr, flags) .map_err(|e| convert_socket_error(vm, e, SocketError::GaiError)) } @@ -2811,8 +2849,8 @@ mod _socket { socket_kind: OptionalArg, proto: OptionalArg, ) -> Result<(PySocket, PySocket), IoOrPyException> { - let family = family.unwrap_or(libc::AF_UNIX); - let socket_kind = socket_kind.unwrap_or(libc::SOCK_STREAM); + let family = family.unwrap_or(c::AF_UNIX); + let socket_kind = socket_kind.unwrap_or(c::SOCK_STREAM); let proto = proto.unwrap_or(0); let (a, b) = Socket::pair(family.into(), socket_kind.into(), Some(proto.into()))?; let py_a = PySocket::default(); @@ -2839,7 +2877,7 @@ mod _socket { { let name = name.to_cstring(vm)?; // in case 'if_nametoindex' does not set errno - rustpython_host_env::os::set_errno(libc::ENODEV); + rustpython_host_env::os::set_errno(c::ENODEV); let ret = unsafe { c::if_nametoindex(name.as_ptr() as _) }; if ret == 0 { Err(vm.new_last_errno_error()) @@ -2860,7 +2898,7 @@ mod _socket { { let mut buf = [0; c::IF_NAMESIZE + 1]; // in case 'if_indextoname' does not set errno - rustpython_host_env::os::set_errno(libc::ENXIO); + rustpython_host_env::os::set_errno(c::ENXIO); let ret = unsafe { c::if_indextoname(index, buf.as_mut_ptr()) }; if ret.is_null() { Err(vm.new_last_errno_error()) @@ -2912,13 +2950,13 @@ mod _socket { ) -> Result { let name = pyname.as_str(); if name.is_empty() { - let hints = dns_lookup::AddrInfoHints { + let hints = host_socket::dns::AddrInfoHints { address: af, socktype: c::SOCK_DGRAM, flags: c::AI_PASSIVE, protocol: 0, }; - let mut res = dns_lookup::getaddrinfo(None, Some("0"), Some(hints)) + let mut res = host_socket::dns::getaddrinfo(None, Some("0"), Some(hints)) .map_err(|e| convert_socket_error(vm, e, SocketError::GaiError))?; let ainfo = res.next().unwrap()?; if res.next().is_some() { @@ -2951,7 +2989,7 @@ mod _socket { { return Ok(SocketAddr::V6(net::SocketAddrV6::new(addr, 0, 0, 0))); } - let hints = dns_lookup::AddrInfoHints { + let hints = host_socket::dns::AddrInfoHints { address: af, ..Default::default() }; @@ -2961,7 +2999,7 @@ mod _socket { .encode_text(pyname.into_wtf8(), "idna", None, vm)?; let name = core::str::from_utf8(name.as_bytes()) .map_err(|_| vm.new_runtime_error("idna output is not utf8"))?; - let mut res = dns_lookup::getaddrinfo(Some(name), None, Some(hints)) + let mut res = host_socket::dns::getaddrinfo(Some(name), None, Some(hints)) .map_err(|e| convert_socket_error(vm, e, SocketError::GaiError))?; Ok(res.next().unwrap().map(|ainfo| ainfo.sockaddr)?) } @@ -3027,10 +3065,10 @@ mod _socket { fn convert_socket_error( vm: &VirtualMachine, - err: dns_lookup::LookupError, + err: host_socket::dns::LookupError, err_kind: SocketError, ) -> IoOrPyException { - if let dns_lookup::LookupErrorKind::System = err.kind() { + if let host_socket::dns::LookupErrorKind::System = err.kind() { return io::Error::from(err).into(); } let strerr = { @@ -3143,14 +3181,16 @@ mod _socket { #[cfg(all(unix, not(target_os = "redox")))] #[pyfunction(name = "CMSG_LEN")] - fn cmsg_len(length: usize, vm: &VirtualMachine) -> PyResult { + fn cmsg_len(length: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let length = length.try_index(vm)?.try_to_primitive_raw(vm)?; host_socket::checked_cmsg_len(length) .ok_or_else(|| vm.new_overflow_error("CMSG_LEN() argument out of range")) } #[cfg(all(unix, not(target_os = "redox")))] #[pyfunction(name = "CMSG_SPACE")] - fn cmsg_space(length: usize, vm: &VirtualMachine) -> PyResult { + fn cmsg_space(length: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let length = length.try_index(vm)?.try_to_primitive_raw(vm)?; host_socket::checked_cmsg_space(length) .ok_or_else(|| vm.new_overflow_error("CMSG_SPACE() argument out of range")) } diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 35cb1794045..b942e27fc69 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -13,6 +13,9 @@ //! //! Warning: This library contains AI-generated code and comments. Do not trust any code or comment without verification. Please have a qualified expert review the code and remove this notice after review. +// false positive: core::io::{Cursor, ErrorKind} are unstable (core_io), unusable on stable +#![expect(clippy::std_instead_of_core)] + // OID (Object Identifier) management module mod oid; @@ -45,7 +48,7 @@ mod _ssl { VirtualMachine, builtins::{ PyBaseExceptionRef, PyByteArray, PyBytesRef, PyListRef, PyStrRef, PyType, - PyTypeRef, PyUtf8StrRef, + PyTypeRef, PyUtf8StrRef, PyWeak, }, convert::IntoPyException, function::{ @@ -64,9 +67,12 @@ mod _ssl { use alloc::sync::Arc; use core::{ hash::{Hash, Hasher}, + hint::cold_path, sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; + use memchr::memchr; + use rustpython_vm::exceptions; use std::{ collections::{HashMap, hash_map::DefaultHasher}, io::BufRead, @@ -131,6 +137,8 @@ mod _ssl { #[pyattr] const PROTOCOL_TLSv1_3: i32 = 6; + static NEXT_SSL_SESSION_NONCE: AtomicUsize = AtomicUsize::new(1); + // Protocol version constants for TLSVersion enum #[pyattr] const PROTO_SSLv3: i32 = 0x0300; @@ -315,15 +323,17 @@ mod _ssl { #[pyattr] const ALERT_DESCRIPTION_NO_APPLICATION_PROTOCOL: i32 = 120; - // Version info - reporting as OpenSSL 3.3.0 for compatibility + // `ssl.py` still requires OpenSSL-shaped numeric compatibility fields even + // for non-OpenSSL TLS providers. Keep them in the supported 3.x ABI range, + // but report the actual rustls/AWS-LC backend in the human-readable string. #[pyattr] - const OPENSSL_VERSION_NUMBER: i32 = 0x30300000; // OpenSSL 3.3.0 (808452096) + const OPENSSL_VERSION_NUMBER: i32 = 0x30300000; #[pyattr] - const OPENSSL_VERSION: &str = "OpenSSL 3.3.0 (rustls/0.23)"; + const OPENSSL_VERSION: &str = "OpenSSL 3.3.0-compatible (AWS-LC/rustls 0.23)"; #[pyattr] - const OPENSSL_VERSION_INFO: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release + const OPENSSL_VERSION_INFO: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); #[pyattr] - const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release + const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // Default cipher list for rustls - using modern secure ciphers #[pyattr] @@ -388,8 +398,9 @@ mod _ssl { // IP addresses are allowed as server_hostname // SNI will not be sent for IP addresses - if hostname.contains('\0') { - return Err(vm.new_type_error("embedded null character")); + if memchr(b'\0', hostname.as_bytes()).is_some() { + cold_path(); + return Err(exceptions::nul_char_type_error(vm)); } if hostname.len() > 253 { @@ -432,6 +443,31 @@ mod _ssl { lifetime: u64, } + impl SessionData { + // NOTE: This is NOT the actual TLS session ID, just a unique identifier. + fn new(server_name: &str, lifetime: u64) -> Self { + let creation_time = SystemTime::now(); + let nonce = NEXT_SSL_SESSION_NONCE.fetch_add(1, Ordering::Relaxed); + let mut hasher = Sha256::new(); + hasher.update(server_name.as_bytes()); + hasher.update( + creation_time + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + .to_le_bytes(), + ); + hasher.update(nonce.to_le_bytes()); + + Self { + _server_name: server_name.to_owned(), + session_id: hasher.finalize()[..16].to_vec(), + creation_time, + lifetime, + } + } + } + // Type alias to simplify complex session cache type type SessionCache = Arc, Arc>>>>; @@ -459,20 +495,6 @@ mod _ssl { // ✓ session_reused - tracked via handshake_kind() // ✗ Actual TLS session ID/ticket data - NOT ACCESSIBLE - // Generate a synthetic session ID from server name and timestamp - // NOTE: This is NOT the actual TLS session ID, just a unique identifier - fn generate_session_id_from_metadata(server_name: &str, time: &SystemTime) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(server_name.as_bytes()); - hasher.update( - time.duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() - .to_le_bytes(), - ); - hasher.finalize()[..16].to_vec() - } - // Custom ClientSessionStore that tracks session metadata for Python access // NOTE: This wraps ClientSessionMemoryCache and records metadata when sessions are stored #[derive(Debug)] @@ -481,6 +503,39 @@ mod _ssl { session_cache: SessionCache, } + impl PythonClientSessionStore { + fn new(session_cache: SessionCache) -> Self { + Self { + inner: Arc::new(ClientSessionMemoryCache::new(SSL_SESSION_CACHE_SIZE)), + session_cache, + } + } + + fn transfer_session( + &self, + target: &Self, + server_name: &ServerName<'static>, + kind: ClientSessionKind, + ) { + if let Some(group) = self.kx_hint(server_name) { + target.set_kx_hint(server_name.clone(), group); + } + + match kind { + ClientSessionKind::Tls12 => { + if let Some(session) = self.tls12_session(server_name) { + target.set_tls12_session(server_name.clone(), session); + } + } + ClientSessionKind::Tls13 => { + if let Some(ticket) = self.take_tls13_ticket(server_name) { + target.insert_tls13_ticket(server_name.clone(), ticket); + } + } + } + } + } + impl ClientSessionStore for PythonClientSessionStore { fn set_kx_hint(&self, server_name: ServerName<'static>, group: rustls::NamedGroup) { self.inner.set_kx_hint(server_name, group); @@ -501,17 +556,8 @@ mod _ssl { // Record metadata in Python-accessible cache // NOTE: We can't access value.session_id or value.ticket (private fields) // So we generate a synthetic ID from metadata - let creation_time = SystemTime::now(); let server_name_str = server_name.to_str(); - let session_data = SessionData { - _server_name: server_name_str.as_ref().to_string(), - session_id: generate_session_id_from_metadata( - server_name_str.as_ref(), - &creation_time, - ), - creation_time, - lifetime: 7200, // TLS 1.2 default session lifetime - }; + let session_data = SessionData::new(server_name_str.as_ref(), 7200); let key = server_name_str.as_bytes().to_vec(); self.session_cache @@ -545,17 +591,8 @@ mod _ssl { // Record metadata in Python-accessible cache // NOTE: We can't access value.ticket or value.lifetime_secs (private fields) // So we use default values - let creation_time = SystemTime::now(); let server_name_str = server_name.to_str(); - let session_data = SessionData { - _server_name: server_name_str.to_string(), - session_id: generate_session_id_from_metadata( - server_name_str.as_ref(), - &creation_time, - ), - creation_time, - lifetime: 7200, // Default TLS 1.3 ticket lifetime (Rustls uses this) - }; + let session_data = SessionData::new(server_name_str.as_ref(), 7200); let key = server_name_str.as_bytes().to_vec(); self.session_cache @@ -738,6 +775,8 @@ mod _ssl { #[pyclass(name = "_SSLContext", module = "ssl", traverse)] #[derive(Debug, PyPayload)] struct PySSLContext { + #[pytraverse(skip)] + context_identity: Arc<()>, #[pytraverse(skip)] protocol: i32, #[pytraverse(skip)] @@ -800,9 +839,6 @@ mod _ssl { // Session management #[pytraverse(skip)] client_session_cache: SessionCache, - // Rustls session store for actual TLS session resumption - #[pytraverse(skip)] - rustls_session_store: Arc, // Rustls server session store for server-side session resumption #[pytraverse(skip)] rustls_server_session_store: Arc, @@ -874,6 +910,24 @@ mod _ssl { #[pyclass(with(Constructor, Representable), flags(BASETYPE))] impl PySSLContext { + fn warn_deprecated_tls_version(version: i32, vm: &VirtualMachine) -> PyResult<()> { + let version_name = match version { + PROTO_SSLv3 => Some("SSLv3"), + PROTO_TLSv1 => Some("TLSv1"), + PROTO_TLSv1_1 => Some("TLSv1_1"), + _ => None, + }; + if let Some(version_name) = version_name { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!("ssl.TLSVersion.{version_name} is deprecated"), + 2, + vm, + )?; + } + Ok(()) + } + // Helper method to convert DER certificate bytes to Python dict fn cert_der_to_dict(&self, vm: &VirtualMachine, cert_der: &[u8]) -> PyResult { cert::cert_der_to_dict_helper(vm, cert_der) @@ -1017,6 +1071,8 @@ mod _ssl { { return Err(vm.new_value_error(format!("invalid protocol version: {value}"))); } + Self::warn_deprecated_tls_version(value, vm)?; + // Convert special values to rustls actual supported versions // MINIMUM_SUPPORTED (-2) -> 0 (auto-negotiate) // MAXIMUM_SUPPORTED (-1) -> MAXIMUM_VERSION (TLSv1.3) @@ -1048,6 +1104,8 @@ mod _ssl { { return Err(vm.new_value_error(format!("invalid protocol version: {value}"))); } + Self::warn_deprecated_tls_version(value, vm)?; + // Convert special values to rustls actual supported versions // MAXIMUM_SUPPORTED (-1) -> 0 (auto-negotiate) // MINIMUM_SUPPORTED (-2) -> MINIMUM_VERSION (TLSv1.2) @@ -1100,19 +1158,19 @@ mod _ssl { let pwd_result = callable.call((), vm)?; // Convert callable result to string - let password_from_callable = if let Ok(pwd_str) = - PyUtf8StrRef::try_from_object(vm, pwd_result.clone()) - { - pwd_str.as_str().to_owned() - } else if let Ok(pwd_bytes_like) = ArgBytesLike::try_from_object(vm, pwd_result) { - String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()).map_err(|_| { - vm.new_type_error("password callback returned invalid UTF-8 bytes") - })? - } else { - return Err( - vm.new_type_error("password callback must return a string or bytes") - ); - }; + let password_from_callable = + if let Ok(pwd_str) = PyUtf8StrRef::try_from_object(vm, pwd_result.clone()) { + pwd_str.as_str().to_owned() + } else if pwd_result.check_buffer() { + let pwd_bytes_like = ArgBytesLike::try_from_object(vm, pwd_result)?; + String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()).map_err(|_| { + vm.new_type_error("password callback returned invalid UTF-8 bytes") + })? + } else { + return Err( + vm.new_type_error("password callback must return a string or bytes") + ); + }; // Validate callable password length if password_from_callable.len() > PEM_BUFSIZE { @@ -1750,7 +1808,8 @@ mod _ssl { // Validate filepath is str or bytes let path_str = if let Ok(s) = PyUtf8StrRef::try_from_object(vm, filepath.clone()) { s.as_str().to_owned() - } else if let Ok(b) = ArgBytesLike::try_from_object(vm, filepath) { + } else if filepath.check_buffer() { + let b = ArgBytesLike::try_from_object(vm, filepath)?; String::from_utf8(b.borrow_buf().to_vec()) .map_err(|_| vm.new_value_error("Invalid path encoding"))? } else { @@ -1805,7 +1864,8 @@ mod _ssl { // Validate name is str or bytes let curve_name = if let Ok(s) = PyUtf8StrRef::try_from_object(vm, name.clone()) { s.as_str().to_owned() - } else if let Ok(b) = ArgBytesLike::try_from_object(vm, name) { + } else if name.check_buffer() { + let b = ArgBytesLike::try_from_object(vm, name)?; String::from_utf8(b.borrow_buf().to_vec()) .map_err(|_| vm.new_value_error("Invalid curve name encoding"))? } else { @@ -1850,25 +1910,7 @@ mod _ssl { let hostname = match args.server_hostname.into_option().flatten() { Some(hostname_str) => { let hostname = hostname_str.as_str(); - - // Validate hostname - if hostname.is_empty() { - return Err(vm.new_value_error("server_hostname cannot be an empty string")); - } - - // Check if it starts with a dot - if hostname.starts_with('.') { - return Err(vm.new_value_error("server_hostname cannot start with a dot")); - } - - // IP addresses are allowed - // SNI will not be sent for IP addresses - - // Check for NULL bytes - if hostname.contains('\0') { - return Err(vm.new_type_error("embedded null character")); - } - + validate_hostname(hostname, vm)?; Some(hostname.to_string()) } None => None, @@ -1909,9 +1951,15 @@ mod _ssl { connection: PyMutex::new(None), handshake_done: PyMutex::new(false), session_was_reused: PyMutex::new(false), - owner: PyRwLock::new(args.owner.into_option()), - // Filter out Python None objects - only store actual SSLSession objects - session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))), + owner: PyRwLock::new( + args.owner + .into_option() + .map(|o| o.downgrade(None, vm)) + .transpose()?, + ), + session: PyRwLock::new(None), + client_config: PyRwLock::new(None), + client_session_store: PyRwLock::new(None), incoming_bio: None, outgoing_bio: None, sni_state: PyRwLock::new(None), @@ -1929,6 +1977,12 @@ mod _ssl { .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) .map_err(|_| vm.new_type_error("Failed to create SSLSocket"))?; + if let Some(session) = args.session.into_option() + && !vm.is_none(&session) + { + ssl_socket_ref.set_session(session, vm)?; + } + Ok(ssl_socket_ref) } @@ -1986,9 +2040,15 @@ mod _ssl { connection: PyMutex::new(None), handshake_done: PyMutex::new(false), session_was_reused: PyMutex::new(false), - owner: PyRwLock::new(args.owner.into_option()), - // Filter out Python None objects - only store actual SSLSession objects - session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))), + owner: PyRwLock::new( + args.owner + .into_option() + .map(|o| o.downgrade(None, vm)) + .transpose()?, + ), + session: PyRwLock::new(None), + client_config: PyRwLock::new(None), + client_session_store: PyRwLock::new(None), incoming_bio: Some(args.incoming), outgoing_bio: Some(args.outgoing), sni_state: PyRwLock::new(None), @@ -2005,6 +2065,12 @@ mod _ssl { .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) .map_err(|_| vm.new_type_error("Failed to create SSLSocket"))?; + if let Some(session) = args.session.into_option() + && !vm.is_none(&session) + { + ssl_socket_ref.set_session(session, vm)?; + } + Ok(ssl_socket_ref) } @@ -2042,8 +2108,8 @@ mod _ssl { Ok((Some(pwd_str.as_str().to_owned()), None)) } // Try bytes-like - else if let Ok(pwd_bytes_like) = ArgBytesLike::try_from_object(vm, p.clone()) - { + else if p.check_buffer() { + let pwd_bytes_like = ArgBytesLike::try_from_object(vm, p.clone())?; let pwd = String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()) .map_err(|_| vm.new_type_error("password bytes must be valid UTF-8"))?; Ok((Some(pwd), None)) @@ -2225,12 +2291,10 @@ mod _ssl { ) -> PyResult { let crypto_ext = CryptoExt::get_ext(); - // Validate protocol - match protocol { - PROTOCOL_TLS | PROTOCOL_TLS_CLIENT | PROTOCOL_TLS_SERVER | PROTOCOL_TLSv1_2 - | PROTOCOL_TLSv1_3 => { - // Valid protocols - } + let deprecated_protocol = match protocol { + PROTOCOL_TLS => Some("PROTOCOL_TLS"), + PROTOCOL_TLSv1_2 => Some("PROTOCOL_TLSv1_2"), + PROTOCOL_TLS_CLIENT | PROTOCOL_TLS_SERVER | PROTOCOL_TLSv1_3 => None, PROTOCOL_TLSv1 | PROTOCOL_TLSv1_1 => { return Err(vm.new_value_error( "TLS 1.0 and 1.1 are not supported by rustls for security reasons", @@ -2239,6 +2303,14 @@ mod _ssl { _ => { return Err(vm.new_value_error(format!("invalid protocol version: {protocol}"))); } + }; + if let Some(protocol_name) = deprecated_protocol { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!("ssl.{protocol_name} is deprecated"), + 2, + vm, + )?; } // Set default options @@ -2277,19 +2349,11 @@ mod _ssl { _ => (PROTO_MINIMUM_SUPPORTED, PROTO_MAXIMUM_SUPPORTED), // Auto-negotiate }; - // IMPORTANT: Create shared session cache BEFORE PySSLContext - // Both client_session_cache and PythonClientSessionStore.session_cache - // MUST point to the same HashMap to ensure Python-level and Rustls-level - // sessions are synchronized + // Session metadata is scoped to the SSLContext. Each per-connection + // rustls session store records into this shared cache. let shared_session_cache = Arc::new(ParkingRwLock::new(HashMap::new())); - let rustls_client_store = Arc::new(PythonClientSessionStore { - inner: Arc::new(rustls::client::ClientSessionMemoryCache::new( - SSL_SESSION_CACHE_SIZE, - )), - session_cache: shared_session_cache.clone(), - }); - Ok(Self { + context_identity: Arc::new(()), protocol, check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT), verify_mode: PyRwLock::new(default_verify_mode), @@ -2313,7 +2377,6 @@ mod _ssl { x509_cert_count: PyRwLock::new(0), // Use the shared cache created above client_session_cache: shared_session_cache, - rustls_session_store: rustls_client_store, rustls_server_session_store: rustls::server::ServerSessionMemoryCache::new( SSL_SESSION_CACHE_SIZE, ), @@ -2360,9 +2423,16 @@ mod _ssl { #[pytraverse(skip)] session_was_reused: PyMutex, // Owner (SSLSocket instance that owns this _SSLSocket) - owner: PyRwLock>, + owner: PyRwLock>>, // Session for resumption session: PyRwLock>, + // Client configuration used by this connection. Retained so the resulting + // SSLSession can reuse the same verifier and client credentials. + #[pytraverse(skip)] + client_config: PyRwLock>>, + // Per-connection store containing the session selected by this connection. + #[pytraverse(skip)] + client_session_store: PyRwLock>>, // MemoryBIO mode (optional) incoming_bio: Option>, outgoing_bio: Option>, @@ -2460,31 +2530,27 @@ mod _ssl { } // Create and store a session object after successful handshake - fn create_session_after_handshake(&self, vm: &VirtualMachine) { + fn create_session_after_handshake(&self, was_resumed: bool, vm: &VirtualMachine) { // Only create session for client-side connections if self.server_side { return; } - // Check if session already exists - let session_opt = self.session.read().clone(); - if let Some(ref s) = session_opt { - if vm.is_none(s) { - } else { - return; - } - } - // Get server hostname let server_name = self.server_hostname.read().clone(); + let previous_session = self.session.read().clone(); // Try to get session data from context's session cache // IMPORTANT: Acquire and release locks quickly to avoid deadlock - let context = self.context.read(); - let session_cache_arc = context.client_session_cache.clone(); - drop(context); // Release context lock ASAP + let (context_identity, session_cache_arc) = { + let context = self.context.read(); + ( + context.context_identity.clone(), + context.client_session_cache.clone(), + ) + }; - let (session_id, creation_time, lifetime) = if let Some(ref name) = server_name { + let cached_session_data = if let Some(ref name) = server_name { let key = name.as_bytes().to_vec(); // Clone the data we need while holding the lock, then immediately release @@ -2494,29 +2560,57 @@ mod _ssl { }; // Lock released here if let Some(session_data_arc) = session_data_opt { - let data = session_data_arc.lock(); - let result = (data.session_id.clone(), data.creation_time, data.lifetime); - drop(data); // Explicit unlock - result + session_data_arc.lock().clone() } else { - // Create new session ID if not in cache - let time = std::time::SystemTime::now(); - (generate_session_id_from_metadata(name, &time), time, 7200) + SessionData::new(name, 7200) } } else { - // No server name, use defaults - let time = std::time::SystemTime::now(); - (vec![0; 16], time, 7200) + SessionData::new("", 7200) + }; + + let session_data = if was_resumed { + previous_session + .as_ref() + .and_then(|session| session.downcast_ref::()) + .map_or(cached_session_data, |session| SessionData { + _server_name: server_name.clone().unwrap_or_default(), + session_id: session.session_id.clone(), + creation_time: session.creation_time, + lifetime: session.lifetime, + }) + } else { + cached_session_data + }; + + let rustls_server_name = server_name.and_then(|name| ServerName::try_from(name).ok()); + let protocol_version = self + .connection + .lock() + .as_ref() + .and_then(|connection| connection.protocol_version()); + let session_kind = match protocol_version { + Some(rustls::ProtocolVersion::TLSv1_2) => ClientSessionKind::Tls12, + Some(rustls::ProtocolVersion::TLSv1_3) => ClientSessionKind::Tls13, + _ => return, + }; + + let Some(client_config) = self.client_config.write().take() else { + return; + }; + let Some(session_store) = self.client_session_store.write().take() else { + return; }; - // Create a new SSLSession object with real metadata let session = PySSLSession { - // Use dummy session data to indicate we have a ticket - // TLS 1.2+ always uses session tickets/resumption - session_data: vec![1], // Non-empty to indicate has_ticket=True - session_id, - creation_time, - lifetime, + context_identity, + client_config, + session_store, + server_name: rustls_server_name, + kind: session_kind, + session_id: session_data.session_id, + creation_time: session_data.creation_time, + lifetime: session_data.lifetime, + has_ticket: true, }; let py_session = session.into_pyobject(vm); @@ -2611,7 +2705,7 @@ mod _ssl { let _ = self.track_used_ca_from_capath(); } - self.create_session_after_handshake(vm); + self.create_session_after_handshake(was_resumed, vm); } // Internal implementation with timeout control @@ -2709,15 +2803,27 @@ mod _ssl { sni_name: Option<&str>, vm: &VirtualMachine, ) -> PyResult<()> { - let callback = self - .context - .read() - .sni_callback - .read() - .clone() - .ok_or_else(|| vm.new_value_error("SNI callback not set"))?; + // The callback may have been cleared (sni_callback = None) between the + // handshake deciding to invoke it and this point. A concurrent removal + // is not an error: there is simply nothing to run. + let callback = self.context.read().sni_callback.read().clone(); + let Some(callback) = callback else { + return Ok(()); + }; - let ssl_sock = self.owner.read().clone().unwrap_or_else(|| vm.ctx.none()); + let ssl_sock = self + .owner + .read() + .as_ref() + .and_then(|owner| owner.upgrade()) + .ok_or_else(|| { + super::compat::SslError::create_ssl_error_with_reason( + vm, + Some("SSL"), + "PARSE_TLSEXT", + "[SSL: PARSE_TLSEXT] SNI callback owner is no longer available", + ) + })?; let server_name_py: PyObjectRef = match sni_name { Some(name) => vm.ctx.new_str(name.to_string()).into(), None => vm.ctx.none(), @@ -3447,9 +3553,9 @@ mod _ssl { let check_hostname = *ctx.check_hostname.read(); let verify_flags = *ctx.verify_flags.read(); + let context_identity = ctx.context_identity.clone(); - // Get session store before dropping ctx - let session_store = ctx.rustls_session_store.clone(); + let session_cache = ctx.client_session_cache.clone(); // Get CRLs for revocation checking let crls_clone = ctx.crls.read().clone(); @@ -3457,31 +3563,6 @@ mod _ssl { // Drop ctx early to avoid borrow conflicts drop(ctx); - // Build client config using compat helper - let config_options = ClientConfigOptions { - protocol_settings, - root_store: if verify_mode != CERT_NONE { - Some(root_store_clone) - } else { - None - }, - ca_certs_der: ca_certs_der_clone, - cert_chain: if !cert_chain_clone.is_empty() { - Some(cert_chain_clone) - } else { - None - }, - private_key: private_key_opt, - verify_server_cert: verify_mode != CERT_NONE, - check_hostname, - verify_flags, - session_store: Some(session_store), - crls: crls_clone, - }; - - let config = - create_client_config(config_options).map_err(|e| vm.new_value_error(e))?; - // Parse server name for SNI // Convert to ServerName use rustls::pki_types::ServerName; @@ -3500,10 +3581,61 @@ mod _ssl { ) }; - let conn = ClientConnection::new(Arc::new(config), server_name.clone()) - .map_err(|e| { - vm.new_value_error(format!("Failed to create client connection: {e}")) - })?; + let explicit_session = self.session.read().clone(); + let session_store = Arc::new(PythonClientSessionStore::new(session_cache)); + let config = if let Some(session) = explicit_session { + let session = session + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; + if !Arc::ptr_eq(&session.context_identity, &context_identity) { + return Err( + vm.new_value_error("Session refers to a different SSLContext.") + ); + } + if session.server_name.as_ref() == Some(&server_name) { + session.session_store.transfer_session( + &session_store, + &server_name, + session.kind, + ); + } + let mut config = (*session.client_config).clone(); + config.resumption = + rustls::client::Resumption::store(session_store.clone()); + Arc::new(config) + } else { + let config_options = ClientConfigOptions { + protocol_settings, + root_store: if verify_mode != CERT_NONE { + Some(root_store_clone) + } else { + None + }, + ca_certs_der: ca_certs_der_clone, + cert_chain: if !cert_chain_clone.is_empty() { + Some(cert_chain_clone) + } else { + None + }, + private_key: private_key_opt, + verify_server_cert: verify_mode != CERT_NONE, + check_hostname, + verify_flags, + session_store: Some(session_store.clone()), + crls: crls_clone, + }; + Arc::new( + create_client_config(config_options) + .map_err(|e| vm.new_value_error(e))?, + ) + }; + + *self.client_config.write() = Some(config.clone()); + *self.client_session_store.write() = Some(session_store); + + let conn = ClientConnection::new(config, server_name).map_err(|e| { + vm.new_value_error(format!("Failed to create client connection: {e}")) + })?; *conn_guard = Some(Connection::Client(conn)); } @@ -3938,12 +4070,13 @@ mod _ssl { #[pygetset] fn owner(&self) -> Option { - self.owner.read().clone() + self.owner.read().as_ref().and_then(|owner| owner.upgrade()) } #[pygetset(setter)] - fn set_owner(&self, owner: PyObjectRef, _vm: &VirtualMachine) { - *self.owner.write() = Some(owner); + fn set_owner(&self, owner: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + *self.owner.write() = Some(owner.downgrade(None, vm)?); + Ok(()) } #[pygetset] @@ -4006,12 +4139,15 @@ mod _ssl { #[pygetset(setter)] fn set_session(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - // Validate that value is an SSLSession - if !value.is(vm.ctx.types.none_type) { - // Try to downcast to SSLSession to validate - let _ = value - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; + let session = value + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; + + if !Arc::ptr_eq( + &session.context_identity, + &self.context.read().context_identity, + ) { + return Err(vm.new_value_error("Session refers to a different SSLContext.")); } // Check if this is a client socket @@ -4025,11 +4161,7 @@ mod _ssl { } // Store the session for potential use during handshake - *self.session.write() = if value.is(vm.ctx.types.none_type) { - None - } else { - Some(value) - }; + *self.session.write() = Some(value); Ok(()) } @@ -4711,22 +4843,31 @@ mod _ssl { // SSLSession - represents a cached SSL session // NOTE: This is an EMULATION - actual session data is managed by Rustls internally + #[derive(Debug, Clone, Copy)] + enum ClientSessionKind { + Tls12, + Tls13, + } + #[pyattr] #[pyclass(name = "SSLSession", module = "ssl")] #[derive(Debug, PyPayload)] struct PySSLSession { - // Session data - serialized rustls session (EMULATED - kept empty) - session_data: Vec, + context_identity: Arc<()>, + client_config: Arc, + session_store: Arc, + server_name: Option>, + kind: ClientSessionKind, // Session ID - synthetic ID generated from metadata (NOT actual TLS session ID) - #[allow(dead_code)] session_id: Vec, // Session metadata creation_time: std::time::SystemTime, // Lifetime in seconds (default 7200 = 2 hours) lifetime: u64, + has_ticket: bool, } - #[pyclass(flags(BASETYPE))] + #[pyclass(flags(BASETYPE), with(Comparable))] impl PySSLSession { #[pygetset] fn time(&self) -> i64 { @@ -4751,20 +4892,29 @@ mod _ssl { #[pygetset] fn id(&self, vm: &VirtualMachine) -> PyBytesRef { - // Return session ID (hash of session data for uniqueness) - - let mut hasher = DefaultHasher::new(); - self.session_data.hash(&mut hasher); - let hash = hasher.finish(); - - // Convert hash to bytes - vm.ctx.new_bytes(hash.to_be_bytes().to_vec()) + vm.ctx.new_bytes(self.session_id.clone()) } #[pygetset] fn has_ticket(&self) -> bool { - // For rustls, if we have session data, we have a ticket - !self.session_data.is_empty() + self.has_ticket + } + } + + impl Comparable for PySSLSession { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + _vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + if let Some(other_session) = other.downcast_ref::() { + Ok((zelf.session_id == other_session.session_id).into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + }) } } @@ -5051,7 +5201,10 @@ mod _ssl { } } - #[pyclass(with(Comparable, Hashable, Representable))] + #[pyclass( + flags(IMMUTABLETYPE, DISALLOW_INSTANTIATION), + with(Comparable, Hashable, Representable) + )] impl PySSLCertificate { #[pymethod] fn public_bytes( diff --git a/crates/stdlib/src/ssl/cert.rs b/crates/stdlib/src/ssl/cert.rs index e304781b644..47d11f730b2 100644 --- a/crates/stdlib/src/ssl/cert.rs +++ b/crates/stdlib/src/ssl/cert.rs @@ -10,7 +10,7 @@ //! - Loading certificates from files, directories, and bytes use alloc::sync::Arc; -use chrono::{DateTime, Utc}; +use jiff::{Timestamp, Zoned, tz::TimeZone}; use parking_lot::RwLock as ParkingRwLock; use rustls::{ DigitallySignedStruct, RootCertStore, SignatureScheme, @@ -201,10 +201,10 @@ fn format_ip_address(ip: &[u8]) -> String { /// Formats certificate validity dates in the format: /// "Mon DD HH:MM:SS YYYY GMT" fn format_asn1_time(time: &x509_parser::time::ASN1Time) -> String { - let timestamp = time.timestamp(); - DateTime::::from_timestamp(timestamp, 0) - .expect("ASN1Time must be valid timestamp") - .format("%b %e %H:%M:%S %Y GMT") + let timestamp = + Timestamp::from_second(time.timestamp()).expect("ASN1Time must be valid timestamp"); + Zoned::new(timestamp, TimeZone::UTC) + .strftime("%b %e %H:%M:%S %Y GMT") .to_string() } @@ -287,9 +287,11 @@ pub(super) fn is_ca_certificate(cert_der: &[u8]) -> bool { return ext.value.ca; } - // No Basic Constraints extension -> NOT a CA certificate - // (matches OpenSSL X509_check_ca() behavior) - false + // X509_check_ca() also retains OpenSSL's legacy trust-anchor rule: a + // self-issued X.509v1 certificate has no extensions at all, but is still + // classified as a CA. CPython's test CA at capath/4e1295a3.0 exercises + // precisely this case. + cert.version().0 == 0 && cert.subject() == cert.issuer() } /// Convert an X509Name to Python nested tuple format for SSL certificate dicts @@ -341,7 +343,7 @@ pub(super) fn cert_to_dict( let serial = format_serial_number(&cert.serial); dict.set_item("serialNumber", vm.ctx.new_str(serial).into(), vm)?; - // Validity dates - format with GMT using chrono + // Validity dates - format with GMT using jiff dict.set_item( "notBefore", vm.ctx @@ -414,7 +416,7 @@ pub(super) fn cert_der_to_dict_helper( // CPython ordering: issuer, notAfter, notBefore, serialNumber, subject, version dict.set_item("issuer", name_to_tuple(cert.issuer())?, vm)?; - // Validity - format with GMT using chrono + // Validity - format with GMT using jiff dict.set_item( "notAfter", vm.ctx @@ -867,26 +869,36 @@ impl ServerCertVerifier for NoVerifier { fn verify_tls12_signature( &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, ) -> Result { - // Accept all signatures without verification - Ok(HandshakeSignatureValid::assertion()) + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &CryptoExt::get_provider().signature_verification_algorithms, + ) } fn verify_tls13_signature( &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, ) -> Result { - // Accept all signatures without verification - Ok(HandshakeSignatureValid::assertion()) + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &CryptoExt::get_provider().signature_verification_algorithms, + ) } fn supported_verify_schemes(&self) -> Vec { - ALL_SIGNATURE_SCHEMES.to_vec() + CryptoExt::get_provider() + .signature_verification_algorithms + .supported_schemes() } } diff --git a/crates/stdlib/src/suggestions.rs b/crates/stdlib/src/suggestions.rs index e0667dfb553..bfde00d2bb9 100644 --- a/crates/stdlib/src/suggestions.rs +++ b/crates/stdlib/src/suggestions.rs @@ -2,19 +2,25 @@ pub(crate) use _suggestions::module_def; #[pymodule] mod _suggestions { - use rustpython_vm::VirtualMachine; + use rustpython_vm::{PyResult, VirtualMachine, builtins::PyList}; use crate::vm::PyObjectRef; #[pyfunction] fn _generate_suggestions( - candidates: Vec, + candidates: PyObjectRef, name: PyObjectRef, vm: &VirtualMachine, - ) -> PyObjectRef { - match crate::vm::suggestion::calculate_suggestions(candidates.iter(), &name) { - Some(suggestion) => suggestion.into(), - None => vm.ctx.none(), - } + ) -> PyResult { + let candidates = candidates + .downcast::() + .map_err(|_| vm.new_type_error("candidates must be a list"))?; + let candidates = candidates.borrow_vec(); + Ok( + match crate::vm::suggestion::calculate_suggestions(candidates.iter(), &name) { + Some(suggestion) => suggestion.into(), + None => vm.ctx.none(), + }, + ) } } diff --git a/crates/stdlib/src/syslog.rs b/crates/stdlib/src/syslog.rs index 52424972a0a..910175ec6ec 100644 --- a/crates/stdlib/src/syslog.rs +++ b/crates/stdlib/src/syslog.rs @@ -13,7 +13,7 @@ mod syslog { use rustpython_host_env::syslog as host_syslog; #[pyattr] - use libc::{ + use host_syslog::{ LOG_ALERT, LOG_AUTH, LOG_CONS, LOG_CRIT, LOG_DAEMON, LOG_DEBUG, LOG_EMERG, LOG_ERR, LOG_INFO, LOG_KERN, LOG_LOCAL0, LOG_LOCAL1, LOG_LOCAL2, LOG_LOCAL3, LOG_LOCAL4, LOG_LOCAL5, LOG_LOCAL6, LOG_LOCAL7, LOG_LPR, LOG_MAIL, LOG_NDELAY, LOG_NEWS, LOG_NOTICE, LOG_NOWAIT, @@ -22,7 +22,7 @@ mod syslog { #[cfg(not(target_os = "redox"))] #[pyattr] - use libc::{LOG_AUTHPRIV, LOG_CRON, LOG_PERROR}; + use host_syslog::{LOG_AUTHPRIV, LOG_CRON, LOG_PERROR}; fn get_argv(vm: &VirtualMachine) -> Option { if let Some(argv) = vm.state.config.settings.argv.first() diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index 7a2c2472443..67d382aa521 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -99,25 +99,9 @@ mod termios { ))] #[pyattr] use host_termios::{CBAUD, CIBAUD, IUCLC, OLCUC, XCASE}; - #[cfg(any( - target_os = "android", - target_os = "freebsd", - target_os = "illumos", - target_os = "linux", - target_os = "macos", - target_os = "solaris" - ))] - #[pyattr] - use host_termios::{TAB0, TABDLY}; - #[cfg(any(target_os = "android", target_os = "linux"))] - #[pyattr] - use host_termios::{VSWTC, VSWTC as VSWTCH}; - #[cfg(any(target_os = "illumos", target_os = "solaris"))] - #[pyattr] - use host_termios::{VSWTCH, VSWTCH as VSWTC}; #[cfg(any(target_os = "illumos", target_os = "solaris"))] #[pyattr] - use libc::{CSTART, CSTOP, CSWTCH}; + use host_termios::{CSTART, CSTOP, CSWTCH}; #[cfg(any( target_os = "dragonfly", target_os = "freebsd", @@ -126,9 +110,9 @@ mod termios { target_os = "openbsd" ))] #[pyattr] - use libc::{FIOASYNC, TIOCGETD, TIOCSETD}; + use host_termios::{FIOASYNC, TIOCGETD, TIOCSETD}; #[pyattr] - use libc::{FIOCLEX, FIONBIO, TIOCGWINSZ, TIOCSWINSZ}; + use host_termios::{FIOCLEX, FIONBIO, TIOCGWINSZ, TIOCSWINSZ}; #[cfg(any( target_os = "android", target_os = "dragonfly", @@ -139,17 +123,27 @@ mod termios { target_os = "openbsd" ))] #[pyattr] - use libc::{ + use host_termios::{ FIONCLEX, FIONREAD, TIOCEXCL, TIOCM_CAR, TIOCM_CD, TIOCM_CTS, TIOCM_DSR, TIOCM_DTR, TIOCM_LE, TIOCM_RI, TIOCM_RNG, TIOCM_RTS, TIOCM_SR, TIOCM_ST, TIOCMBIC, TIOCMBIS, TIOCMGET, TIOCMSET, TIOCNXCL, TIOCSCTTY, }; #[cfg(any(target_os = "android", target_os = "linux"))] #[pyattr] - use libc::{ + use host_termios::{ IBSHIFT, TCFLSH, TCGETA, TCGETS, TCSBRK, TCSETA, TCSETAF, TCSETAW, TCSETS, TCSETSF, TCSETSW, TCXONC, TIOCGSERIAL, TIOCGSOFTCAR, TIOCINQ, TIOCLINUX, TIOCSSOFTCAR, XTABS, }; + #[cfg(any( + target_os = "android", + target_os = "freebsd", + target_os = "illumos", + target_os = "linux", + target_os = "macos", + target_os = "solaris" + ))] + #[pyattr] + use host_termios::{TAB0, TABDLY}; #[cfg(any( target_os = "android", target_os = "dragonfly", @@ -158,13 +152,19 @@ mod termios { target_os = "macos" ))] #[pyattr] - use libc::{TIOCCONS, TIOCGPGRP, TIOCOUTQ, TIOCSPGRP, TIOCSTI}; + use host_termios::{TIOCCONS, TIOCGPGRP, TIOCOUTQ, TIOCSPGRP, TIOCSTI}; #[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "macos"))] #[pyattr] - use libc::{ + use host_termios::{ TIOCNOTTY, TIOCPKT, TIOCPKT_DATA, TIOCPKT_DOSTOP, TIOCPKT_FLUSHREAD, TIOCPKT_FLUSHWRITE, TIOCPKT_NOSTOP, TIOCPKT_START, TIOCPKT_STOP, }; + #[cfg(any(target_os = "android", target_os = "linux"))] + #[pyattr] + use host_termios::{VSWTC, VSWTC as VSWTCH}; + #[cfg(any(target_os = "illumos", target_os = "solaris"))] + #[pyattr] + use host_termios::{VSWTCH, VSWTCH as VSWTC}; #[pyfunction] fn tcgetattr(fd: PyObjectRef, vm: &VirtualMachine) -> PyResult> { @@ -268,6 +268,28 @@ mod termios { Ok(()) } + #[pyfunction] + fn tcgetwinsize(Fildes(fd): Fildes, vm: &VirtualMachine) -> PyResult<(u16, u16)> { + let size = host_termios::tcgetwinsize(fd).map_err(|e| termios_error(e, vm))?; + Ok(size) + } + + #[pyfunction] + fn tcsetwinsize(Fildes(fd): Fildes, size: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let seq = size.try_sequence(vm)?; + if seq.length(vm)? != 2 { + return Err(vm.new_type_error("tcsetwinsize: size must be a 2 element sequence")); + } + let row = seq.get_item(0, vm)?; + let col = seq.get_item(1, vm)?; + + let row: u16 = row.try_index(vm)?.try_to_primitive(vm)?; + let col: u16 = col.try_index(vm)?.try_to_primitive(vm)?; + + host_termios::tcsetwinsize(fd, row, col).map_err(|e| termios_error(e, vm))?; + Ok(()) + } + fn termios_error(err: std::io::Error, vm: &VirtualMachine) -> PyBaseExceptionRef { vm.new_os_subtype_error( error_type(vm), diff --git a/crates/stdlib/src/tkinter.rs b/crates/stdlib/src/tkinter.rs index 0ccc0a97f9e..ca70561b3ab 100644 --- a/crates/stdlib/src/tkinter.rs +++ b/crates/stdlib/src/tkinter.rs @@ -160,10 +160,22 @@ mod _tkinter { return Ok(varname); } - if let Some(_tcl_obj) = obj.downcast_ref::() { - // Assume that the Tcl object has a method to retrieve a string. - // return tcl_obj. - todo!(); + if let Some(tcl_obj) = obj.downcast_ref::() { + let c_str = unsafe { tk_sys::Tcl_GetString(tcl_obj.value) }; + let bytes = unsafe { ffi::CStr::from_ptr(c_str as _) }.to_bytes(); + let varname = core::str::from_utf8(bytes) + .map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(bytes.to_vec()), + e.valid_up_to(), + e.error_len() + .map_or(bytes.len(), |len| e.valid_up_to() + len), + vm.ctx.new_str(e.to_string()), + ) + })? + .to_owned(); + return Ok(varname); } // Construct an error message using the type name (truncated to 50 characters). diff --git a/crates/stdlib/src/unicodedata.rs b/crates/stdlib/src/unicodedata.rs index 4152118f51d..134332bc9d5 100644 --- a/crates/stdlib/src/unicodedata.rs +++ b/crates/stdlib/src/unicodedata.rs @@ -2,153 +2,26 @@ See also: https://docs.python.org/3/library/unicodedata.html */ -// spell-checker:ignore codep decomp DECOMP nfkc unistr unidata - -use core::{ - cmp::Ordering, - fmt::{self, Display, Formatter}, - hint::cold_path, -}; +// spell-checker:ignore nfkc unistr unidata pub(crate) use unicodedata::module_def; -use icu_properties::props::{ - BidiClass, CanonicalCombiningClass, EastAsianWidth, GeneralCategory, NumericType, -}; +use rustpython_unicode::{self as unicode_core, NormalizeForm}; use crate::vm::{ PyObject, PyResult, VirtualMachine, builtins::PyStr, convert::TryFromBorrowedObject, }; -include!(concat!(env!("OUT_DIR"), "/generated/unicode_3_2.rs")); -include!(concat!(env!("OUT_DIR"), "/generated/unicode_latest.rs")); -include!(concat!(env!("OUT_DIR"), "/generated/unicode_num_type.rs")); -include!(concat!( - env!("OUT_DIR"), - "/generated/unicode_numeric_value.rs" -)); - -#[derive(Clone, Copy, Debug, PartialEq)] -struct UnicodeVersion { - pub major: u8, - pub minor: u8, - pub micro: u8, -} - -impl Display for UnicodeVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.micro) - } -} - -const UNICODE_VERSION: UnicodeVersion = UnicodeVersion { - major: char::UNICODE_VERSION.0, - minor: char::UNICODE_VERSION.1, - micro: char::UNICODE_VERSION.2, -}; - -#[derive(Clone, Copy)] -#[repr(u8)] -enum DecompositionType { - #[allow(unused)] - Canonical, - Compat, - Circle, - Final, - Font, - Fraction, - Initial, - Isolated, - Medial, - Narrow, - Nobreak, - Small, - Square, - Sub, - Super, - Vertical, - Wide, -} - -impl DecompositionType { - const fn type_tag(self) -> &'static str { - match self { - Self::Canonical => "canonical", - Self::Compat => "compat", - Self::Circle => "circle", - Self::Final => "final", - Self::Font => "font", - Self::Fraction => "fraction", - Self::Initial => "initial", - Self::Isolated => "isolated", - Self::Medial => "medial", - Self::Narrow => "narrow", - Self::Nobreak => "noBreak", - Self::Small => "small", - Self::Square => "square", - Self::Sub => "sub", - Self::Super => "super", - Self::Vertical => "vertical", - Self::Wide => "wide", - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq)] -enum NormalizeForm { - Nfc, - Nfkc, - Nfd, - Nfkd, -} - -fn lookup_property(table: &[(u32, u32, T)], ch: char) -> Option { - let ch = ch as u32; - table - .binary_search_by(|&(start, end, _)| { - if ch > end { - Ordering::Less - } else if ch < start { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .ok() - .map(|i| table[i].2) -} - -fn lookup_numeric_val(ch: char, version: UnicodeVersion) -> Option { - if version.major > 3 { - lookup_property(NUMERIC_VALUES, ch) - } else { - cold_path(); - lookup_property(NUMERIC_VALUES_DIFF, ch).or_else(|| { - NUMERIC_VAL_EXISTS_32 - .binary_search_by(|&(start, end)| { - let ch = ch as u32; - if ch > end { - Ordering::Less - } else if ch < start { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .ok() - .and_then(|_| lookup_property(NUMERIC_VALUES, ch)) - }) - } -} +struct NormalizeFormArg(NormalizeForm); -impl<'a> TryFromBorrowedObject<'a> for NormalizeForm { +impl<'a> TryFromBorrowedObject<'a> for NormalizeFormArg { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { obj.try_value_with( |form: &PyStr| match form.as_bytes() { - b"NFC" => Ok(Self::Nfc), - b"NFKC" => Ok(Self::Nfkc), - b"NFD" => Ok(Self::Nfd), - b"NFKD" => Ok(Self::Nfkd), + b"NFC" => Ok(Self(NormalizeForm::Nfc)), + b"NFKC" => Ok(Self(NormalizeForm::Nfkc)), + b"NFD" => Ok(Self(NormalizeForm::Nfd)), + b"NFKD" => Ok(Self(NormalizeForm::Nfkd)), _ => Err(vm.new_value_error("invalid normalization form")), }, vm, @@ -158,27 +31,12 @@ impl<'a> TryFromBorrowedObject<'a> for NormalizeForm { #[pymodule] mod unicodedata { - use core::{cmp::Ordering, fmt::Write, hint::cold_path}; - - use super::{ - BIDI_CLASS, BIDI_MIRRORED, COMBINING_CLASS, DECOMP_COMPAT, DECOMP_RANGE, DECOMP_UPDATES, - EAST_ASIAN_WIDTH, GENERAL_CATEGORY, NUMERIC_TYPE_DIFF, NormalizeForm, UNICODE_VERSION, - UnicodeVersion, lookup_numeric_val, lookup_property, - }; + use super::{NormalizeFormArg, unicode_core}; use crate::vm::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyModule, PyStrRef}, function::OptionalArg, }; - - use icu_normalizer::{ - ComposingNormalizerBorrowed, DecomposingNormalizerBorrowed, - properties::{CanonicalDecomposition, Decomposed}, - }; - use icu_properties::props::{ - BidiClass, BidiMirrored, BinaryProperty, CanonicalCombiningClass, EastAsianWidth, - EnumeratedProperty, GeneralCategory, NamedEnumeratedProperty, NumericType, - }; use itertools::Itertools; use rustpython_common::wtf8::{CodePoint, Wtf8Buf}; @@ -186,7 +44,7 @@ mod unicodedata { __module_exec(vm, module); // Add UCD methods as module-level functions - let ucd: PyObjectRef = Ucd::new(UNICODE_VERSION).into_ref(&vm.ctx).into(); + let ucd: PyObjectRef = Ucd::new(true).into_ref(&vm.ctx).into(); for attr in [ "category", @@ -213,12 +71,14 @@ mod unicodedata { #[pyclass(name = "UCD")] #[derive(Debug, PyPayload)] pub(super) struct Ucd { - unic_version: UnicodeVersion, + inner: unicode_core::Ucd, } impl Ucd { - pub(super) const fn new(unic_version: UnicodeVersion) -> Self { - Self { unic_version } + pub(super) const fn new(modern: bool) -> Self { + Self { + inner: unicode_core::Ucd::new(modern), + } } fn extract_char(&self, character: PyStrRef, vm: &VirtualMachine) -> PyResult { @@ -234,26 +94,14 @@ mod unicodedata { impl Ucd { #[pymethod] fn category(&self, character: PyStrRef, vm: &VirtualMachine) -> PyResult<&'static str> { - self.extract_char(character, vm).map(|c| { - let Some(c) = c.to_char() else { - return GeneralCategory::Surrogate.short_name(); - }; - if self.unic_version.major > 3 { - Some(GeneralCategory::for_char(c)) - } else { - cold_path(); - lookup_property(GENERAL_CATEGORY, c) - } - .unwrap_or(GeneralCategory::Unassigned) - .short_name() - }) + self.extract_char(character, vm) + .map(|c| self.inner.category(c)) } - // TODO: Names needs to account for Unicode 3.2.0 and 16.0.0 #[pymethod] fn lookup(&self, name: PyStrRef, vm: &VirtualMachine) -> PyResult { if let Some(name_str) = name.to_str() - && let Some(character) = unicode_names2::character(name_str) + && let Some(character) = unicode_core::lookup_character(name_str) { return Ok(character.to_string()); } @@ -264,7 +112,6 @@ mod unicodedata { )) } - // TODO: Names needs to account for Unicode 3.2.0 and 16.0.0 #[pymethod] fn name( &self, @@ -275,9 +122,9 @@ mod unicodedata { if let Some(name) = self .extract_char(character, vm)? .to_char() - .and_then(unicode_names2::name) + .and_then(unicode_core::character_name) { - return Ok(vm.ctx.new_str(name.to_string()).into()); + return Ok(vm.ctx.new_str(name).into()); } default.ok_or_else(|| vm.new_value_error("no such name")) } @@ -288,19 +135,8 @@ mod unicodedata { character: PyStrRef, vm: &VirtualMachine, ) -> PyResult<&'static str> { - self.extract_char(character, vm).map(|c| { - c.to_char() - .and_then(|c| { - if self.unic_version.major > 3 { - Some(BidiClass::for_char(c)) - } else { - cold_path(); - lookup_property(BIDI_CLASS, c) - } - }) - .unwrap_or(BidiClass::LeftToRight) - .short_name() - }) + self.extract_char(character, vm) + .map(|c| self.inner.bidirectional(c)) } #[pymethod] @@ -309,190 +145,36 @@ mod unicodedata { character: PyStrRef, vm: &VirtualMachine, ) -> PyResult<&'static str> { - self.extract_char(character, vm).map(|c| { - c.to_char() - .and_then(|c| { - if self.unic_version.major > 3 { - Some(EastAsianWidth::for_char(c)) - } else { - cold_path(); - // CPython overrides characters in the PUA for 3.2.0. - // Basic Multilingual Plane: - // https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane - // https://en.wikipedia.org/wiki/Private_Use_Areas - // https://www.unicode.org/reports/tr11/tr11-10.html - // https://www.unicode.org/reports/tr11/ - // - // Currently, this implementation is incomplete because I can't figure - // out what CPython is doing. - lookup_property(EAST_ASIAN_WIDTH, c) - } - }) - .unwrap_or(EastAsianWidth::Neutral) - .short_name() - }) + self.extract_char(character, vm) + .map(|c| self.inner.east_asian_width(c)) } #[pymethod] - fn normalize(&self, form: super::NormalizeForm, unistr: PyStrRef) -> Wtf8Buf { - let text = unistr.as_wtf8(); - match form { - NormalizeForm::Nfc => { - let normalizer = ComposingNormalizerBorrowed::new_nfc(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfkc => { - let normalizer = ComposingNormalizerBorrowed::new_nfkc(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfd => { - let normalizer = DecomposingNormalizerBorrowed::new_nfd(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfkd => { - let normalizer = DecomposingNormalizerBorrowed::new_nfkd(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - } + fn normalize(&self, form: NormalizeFormArg, unistr: PyStrRef) -> Wtf8Buf { + unicode_core::normalize(form.0, unistr.as_wtf8()) } #[pymethod] - fn is_normalized(&self, form: super::NormalizeForm, unistr: PyStrRef) -> bool { - let text = unistr.as_wtf8(); - let normalized: Wtf8Buf = match form { - NormalizeForm::Nfc => { - let normalizer = ComposingNormalizerBorrowed::new_nfc(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfkc => { - let normalizer = ComposingNormalizerBorrowed::new_nfkc(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfd => { - let normalizer = DecomposingNormalizerBorrowed::new_nfd(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - NormalizeForm::Nfkd => { - let normalizer = DecomposingNormalizerBorrowed::new_nfkd(); - text.map_utf8(|s| normalizer.normalize_iter(s.chars())) - .collect() - } - }; - text == &*normalized + fn is_normalized(&self, form: NormalizeFormArg, unistr: PyStrRef) -> bool { + unicode_core::is_normalized(form.0, unistr.as_wtf8()) } #[pymethod] fn mirrored(&self, character: PyStrRef, vm: &VirtualMachine) -> PyResult { - self.extract_char(character, vm).map(|c| { - c.to_char().map_or(0, |c| { - (if self.unic_version.major > 3 { - BidiMirrored::for_char(c) - } else { - cold_path(); - let c = c as u32; - BIDI_MIRRORED - .binary_search_by(|&(start, end)| { - if c > end { - Ordering::Less - } else if c < start { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .is_ok() - }) as i32 - }) - }) + self.extract_char(character, vm) + .map(|c| self.inner.mirrored(c)) } #[pymethod] fn combining(&self, character: PyStrRef, vm: &VirtualMachine) -> PyResult { - self.extract_char(character, vm).map(|c| { - c.to_char() - .and_then(|c| { - if self.unic_version.major > 3 { - Some(CanonicalCombiningClass::for_char(c)) - } else { - cold_path(); - lookup_property(COMBINING_CLASS, c) - } - }) - .unwrap_or(CanonicalCombiningClass::NotReordered) - .to_icu4c_value() - }) + self.extract_char(character, vm) + .map(|c| self.inner.combining(c)) } #[pymethod] fn decomposition(&self, character: PyStrRef, vm: &VirtualMachine) -> PyResult { - let Some(ch) = self.extract_char(character, vm).map(CodePoint::to_char)? else { - return Ok(String::new()); - }; - - // Decomposition is remarkable stable according to the normalization file, - // so the updates slice is very small - only about four char pairs. Linearly searching - // it is very fast. The file lists the original, incorrect decomp and the fixed char. - // For 3.2.0, we use the original decomp for compatibility while ignoring the update. - // - // Finally, we don't have to do anything for the latest UCD as it's already updated. - if self.unic_version.major == 3 - && let Some((_, original)) = DECOMP_UPDATES - .iter() - .find(|&&(codep, _original)| codep == ch as u32) - { - Ok(format!("{original:04X}")) - } else if let Ok(i) = - DECOMP_COMPAT.binary_search_by_key(&(ch as u32), |&(codep, _, _)| codep) - { - // Compatibility decomposition - // `icu4x` doesn't expose a non-recursive, compatibility decomposer so we - // have to do it manually for now. - let tag = DECOMP_COMPAT[i].1.type_tag(); - let end = DECOMP_COMPAT[i].2; - let start = i - .checked_sub(1) - .map(|i| DECOMP_COMPAT[i].2) - .unwrap_or_default(); - - let decomp = &DECOMP_RANGE[start..end]; - let cap = decomp.len() * 10 + decomp.len() + tag.len() + 1; - let mut out = String::with_capacity(cap); - - write!(out, "<{tag}>").unwrap(); - for ch in decomp { - write!(out, " {ch:04X}").unwrap(); - } - - Ok(out) - } else { - // Canonical decomposition - let decomposed = CanonicalDecomposition::new().decompose(ch); - match decomposed { - Decomposed::Default => Ok(String::new()), - Decomposed::Singleton(ch) => Ok(format!("{:04X}", ch as u32)), - Decomposed::Expansion(l, r) => Ok(format!("{:04X} {:04X}", l as u32, r as u32)), - } - } - } - - fn numeric_type_matches(&self, ch: CodePoint, expected: &[NumericType]) -> Option { - let ch = ch.to_char()?; - - let actual = if self.unic_version.major > 3 { - NumericType::for_char(ch) - } else { - cold_path(); - lookup_property(NUMERIC_TYPE_DIFF, ch).unwrap_or_else(|| NumericType::for_char(ch)) - }; - - expected.contains(&actual).then_some(ch) + self.extract_char(character, vm) + .map(|c| self.inner.decomposition(c)) } #[pymethod] @@ -503,12 +185,9 @@ mod unicodedata { vm: &VirtualMachine, ) -> PyResult> { let ch = self.extract_char(character, vm)?; - let expected = [NumericType::Decimal, NumericType::Digit]; - self.numeric_type_matches(ch, &expected) - .and_then(|ch| { - let value = lookup_numeric_val(ch, UNICODE_VERSION)?; - (value.trunc() == value).then(|| vm.ctx.new_int(value as u64).into()) - }) + self.inner + .digit(ch) + .map(|value| vm.ctx.new_int(value).into()) .or_else(|| default.present()) .map(Option::Some) .ok_or_else(|| vm.new_value_error("not a digit")) @@ -522,12 +201,9 @@ mod unicodedata { vm: &VirtualMachine, ) -> PyResult> { let ch = self.extract_char(character, vm)?; - let expected = [NumericType::Decimal]; - self.numeric_type_matches(ch, &expected) - .and_then(|ch| { - let value = lookup_numeric_val(ch, self.unic_version)?; - (value.trunc() == value).then(|| vm.ctx.new_int(value as u64).into()) - }) + self.inner + .decimal(ch) + .map(|value| vm.ctx.new_int(value).into()) .or_else(|| default.present()) .map(Option::Some) .ok_or_else(|| vm.new_value_error("not a decimal")) @@ -541,12 +217,9 @@ mod unicodedata { vm: &VirtualMachine, ) -> PyResult> { let ch = self.extract_char(character, vm)?; - let expected = &NumericType::ALL_VALUES[1..]; - self.numeric_type_matches(ch, expected) - .and_then(|ch| { - lookup_numeric_val(ch, self.unic_version) - .map(|value| vm.ctx.new_float(value).into()) - }) + self.inner + .numeric(ch) + .map(|value| vm.ctx.new_float(value).into()) .or_else(|| default.present()) .map(Option::Some) .ok_or_else(|| vm.new_value_error("not a numeric character")) @@ -554,24 +227,17 @@ mod unicodedata { #[pygetset] fn unidata_version(&self) -> String { - self.unic_version.to_string() + self.inner.unidata_version() } } #[pyattr] fn ucd_3_2_0(vm: &VirtualMachine) -> PyRef { - Ucd { - unic_version: UnicodeVersion { - major: 3, - minor: 2, - micro: 0, - }, - } - .into_ref(&vm.ctx) + Ucd::new(false).into_ref(&vm.ctx) } #[pyattr] fn unidata_version(_vm: &VirtualMachine) -> String { - UNICODE_VERSION.to_string() + unicode_core::unicode_version() } } diff --git a/crates/stdlib/src/uuid.rs b/crates/stdlib/src/uuid.rs index 44121683628..10dc9541755 100644 --- a/crates/stdlib/src/uuid.rs +++ b/crates/stdlib/src/uuid.rs @@ -3,16 +3,13 @@ pub(crate) use _uuid::module_def; #[pymodule] mod _uuid { use crate::{builtins::PyNone, vm::VirtualMachine}; - use mac_address::get_mac_address; use std::sync::OnceLock; use uuid::{ContextV1, Uuid, timestamp::Timestamp}; fn get_node_id() -> [u8; 6] { - match get_mac_address() { - Ok(Some(_ma)) => get_mac_address().unwrap().unwrap().bytes(), - // os_random is expensive, but this is only ever called once - _ => rustpython_common::rand::os_random::<6>(), - } + // os_random is expensive, but this is only ever called once + rustpython_host_env::socket::mac_address() + .unwrap_or_else(rustpython_common::rand::os_random::<6>) } #[pyfunction] diff --git a/crates/unicode/Cargo.toml b/crates/unicode/Cargo.toml new file mode 100644 index 00000000000..52ab05f4d1f --- /dev/null +++ b/crates/unicode/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rustpython-unicode" +description = "Runtime-independent CPython-compatible Unicode semantics and data for RustPython and related Python tooling." +edition = { workspace = true } +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +rustpython-wtf8 = { workspace = true } + +icu_casemap = { workspace = true } +icu_locale = { workspace = true } +icu_properties = { workspace = true } +icu_normalizer = { workspace = true } +unicode_names2 = { workspace = true } +writeable = { workspace = true } + +[build-dependencies] +icu_properties = { workspace = true } + +[lints] +workspace = true diff --git a/crates/unicode/build.rs b/crates/unicode/build.rs new file mode 100644 index 00000000000..3a82df85eb8 --- /dev/null +++ b/crates/unicode/build.rs @@ -0,0 +1,612 @@ +// spell-checker:ignore decomp DECOMP + +extern crate alloc; + +use core::num::NonZeroUsize; + +use alloc::collections::{BTreeMap, BTreeSet}; + +use std::{ + env, + fs::{self, File}, + io::{self, BufRead, BufReader, BufWriter, Write}, + path::{Path, PathBuf}, + thread, +}; + +use icu_properties::props::{EnumeratedProperty, GeneralCategory, NumericType}; + +fn generate_unicode_3_2() { + let path = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("generated") + .join("unicode_3_2.rs"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut writer = BufWriter::new(File::create(&path).unwrap()); + + let base = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("unicode") + .join("ucd32"); + + write_derived( + &base, + "DerivedGeneralCategory-3.2.0.txt", + "GENERAL_CATEGORY", + "(u32, u32, GeneralCategory)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + let id = parse_general(id); + if id != GeneralCategory::Unassigned { + Some((start, end, id)) + } else { + None + } + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _, _)| *start); + write!(writer, "[").unwrap(); + for (start, end, id) in values { + write!(writer, "({start}, {end}, GeneralCategory::{id:?}),").unwrap(); + } + write!(writer, "];").unwrap(); + }, + ); + + write_derived( + &base, + "DerivedEastAsianWidth-3.2.0.txt", + "EAST_ASIAN_WIDTH", + "(u32, u32, EastAsianWidth)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + let id = parse_eaw(id); + if id != "EastAsianWidth::Neutral" { + Some((start, end, id)) + } else { + None + } + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _, _)| *start); + write!(writer, "[").unwrap(); + for (start, end, id) in values { + write!(writer, "({start}, {end}, {id}),").unwrap(); + } + write!(writer, "];").unwrap(); + }, + ); + + write_derived( + &base, + "DerivedBidiClass-3.2.0.txt", + "BIDI_CLASS", + "(u32, u32, BidiClass)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + let id = parse_bidi(id); + if id != "BidiClass::LeftToRight" { + Some((start, end, id)) + } else { + None + } + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _, _)| *start); + write!(writer, "[").unwrap(); + for (start, end, id) in values { + write!(writer, "({start}, {end}, {id}),").unwrap(); + } + write!(writer, "];").unwrap(); + }, + ); + + write_derived( + &base, + "DerivedBinaryProperties-3.2.0.txt", + "BIDI_MIRRORED", + "(u32, u32)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + assert_eq!( + "Bidi_Mirrored", + id.trim(), + "DerivedBinaryProperties-3.2.0 only has Bidi_Mirrored" + ); + Some((start, end)) + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _)| *start); + writeln!(writer, "{values:?};").unwrap(); + }, + ); + + write_derived( + &base, + "DerivedCombiningClass-3.2.0.txt", + "COMBINING_CLASS", + "(u32, u32, CanonicalCombiningClass)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + let id: u8 = id.parse().unwrap(); + if id == 0 { + return None; + } + Some((start, end, id)) + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _, _)| *start); + write!(writer, "[").unwrap(); + for (start, end, id) in values { + write!( + writer, + "({start}, {end}, CanonicalCombiningClass::from_icu4c_value({id}))," + ) + .unwrap(); + } + writeln!(writer, "];").unwrap(); + }, + ); +} + +fn generate_numeric_type() { + let path = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("generated") + .join("unicode_num_type.rs"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut writer = BufWriter::new(File::create(&path).unwrap()); + + let base = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("unicode") + .join("ucd32"); + + write_derived( + &base, + "DerivedNumericType-3.2.0.txt", + "NUMERIC_TYPE_DIFF", + "(u32, u32, NumericType)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, id, _| { + let id = parse_numeric_type_str(id); + let differs = (start..=end).any(|c| match char::from_u32(c) { + Some(c) => { + let modern = parse_numeric_type_val(NumericType::for_char(c)); + modern != id + } + None => true, + }); + + if differs { + Some((start, end, id)) + } else { + None + } + }, + |writer, mut values| { + values.sort_unstable_by_key(|(start, _, _)| *start); + write!(writer, "[").unwrap(); + for (start, end, id) in values { + write!(writer, "({start}, {end}, {id}),").unwrap(); + } + writeln!(writer, "];").unwrap(); + }, + ); +} + +fn generate_numeric_value() { + let path = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("generated") + .join("unicode_numeric_value.rs"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut writer = BufWriter::new(File::create(&path).unwrap()); + + // Ideally, this would store the diffs between the two tables. However, we need 3.2.0 + // membership as well as different chars. The final tables are both smaller than storing the + // full 3.2.0 value table. + let ucd32 = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("unicode") + .join("ucd32"); + let mut ucd32_diffs = BTreeMap::new(); + let mut ucd32_member = BTreeSet::new(); + let numeric_32 = + BufReader::new(File::open(ucd32.join("DerivedNumericValues-3.2.0.txt")).unwrap()); + parse_unicode_3_2( + numeric_32, + NonZeroUsize::new(1).unwrap(), + &mut io::empty(), + |start, end, value, _| { + let value: f64 = value + .parse() + .expect("Unicode data contains valid properties"); + ucd32_diffs.insert((start, end), value); + ucd32_member.insert((start, end)); + Option::<()>::None + }, + |_writer, _values| {}, + ); + + let ucd_latest = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("unicode") + .join("latest"); + + write_derived( + &ucd_latest, + "DerivedNumericValues.txt", + "NUMERIC_VALUES", + "(u32, u32, f64)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, end, value, _| { + let value: f64 = value + .parse() + .expect("Unicode data contains valid properties"); + + if ucd32_diffs + .get(&(start, end)) + .is_some_and(|old_v| *old_v == value) + { + ucd32_diffs.remove(&(start, end)); + } + + Some((start, end, value)) + }, + |writer, mut values| { + values.sort_unstable_by_key(|(ch, _, _)| *ch); + writeln!(writer, "{values:?};").unwrap(); + }, + ); + + // TODO: More flexible parser + writeln!( + writer, + "static NUMERIC_VALUES_DIFF: &[(u32, u32, f64)] = &[" + ) + .unwrap(); + for ((start, end), value) in ucd32_diffs { + write!(writer, "({start}, {end}, {value:?}),").unwrap(); + } + writeln!(writer, "];").unwrap(); + + // Compress membership table + let mut iter = ucd32_member.iter(); + let &(mut start_prev, mut end_prev) = iter.next().unwrap(); + let mut membership = Vec::new(); + + for &(start, end) in iter { + if start <= end_prev + 1 { + end_prev = end_prev.max(end); + } else { + membership.push((start_prev, end_prev)); + start_prev = start; + end_prev = end; + } + } + membership.push((start_prev, end_prev)); + membership.sort_unstable_by_key(|&(start, _)| start); + + writeln!(writer, "static NUMERIC_VAL_EXISTS_32: &[(u32, u32)] = &").unwrap(); + write!(writer, "{membership:?};").unwrap(); +} + +fn generate_unicode_latest() { + let path = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("generated") + .join("unicode_latest.rs"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut writer = BufWriter::new(File::create(&path).unwrap()); + + let base = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("unicode") + .join("latest"); + + // NOTE: + // This ONLY parses compatibility decomposition because Python exposes the tags. The tags are + // the "", "", et cetera bits before the decomposition. Thus, we can save space + // by using icu4x's CanonicalDecomposer for non-compatibility decomposition. + let mut decomp_ranges = Vec::new(); + write_derived( + &base, + "UnicodeData.txt", + "DECOMP_COMPAT", + "(u32, DecompositionType, usize)", + NonZeroUsize::new(5).unwrap(), + &mut writer, + |start, _end, value, _| { + // We're building a sparse array. Most characters don't decompose, so we don't + // need to literally store a row for each char. + if value.is_empty() { + return None; + } + + let (dtype, decomp) = value.split_once('>').map(|(dtype, decomp)| { + let dtype = dtype.strip_prefix('<').unwrap_or_else(|| { + panic!("Compatibility decomp; expected \n\tgot: {value}") + }); + ( + parse_decomp_type(dtype), + decomp + .split_whitespace() + .map(|s| u32::from_str_radix(s, 16).unwrap()), + ) + })?; + + decomp_ranges.extend(decomp); + let end = decomp_ranges.len(); + + Some((start, dtype, end)) + }, + |writer, values| { + // UnicodeData.txt should already be sorted + write!(writer, "[").unwrap(); + for (start, dtype, end) in values { + write!(writer, "({start}, DecompositionType::{dtype:?}, {end}),").unwrap(); + } + writeln!(writer, "];").unwrap(); + }, + ); + + writeln!(writer, "static DECOMP_RANGE: &[u32] = &{decomp_ranges:?};").unwrap(); + + // Normalization corrections is super small - only a handful chars at the time of writing. + write_derived( + &base, + "NormalizationCorrections.txt", + "DECOMP_UPDATES", + "(u32, u32)", + NonZeroUsize::new(1).unwrap(), + &mut writer, + |start, _end, value, line| { + let original = u32::from_str_radix(value.trim(), 16).unwrap_or_else(|e| { + panic!("field 2 of decomp corrections should be a char in hex: {value} {e}") + }); + let version = line + .rsplit(';') + .next() + .unwrap_or_else(|| { + panic!("field 4 of decomp corrections should be a UCD version: {line}") + }) + .split_once('#') + .unwrap() + .0 + .trim(); + + // `version` = when the char was updated. Therefore, we use the incorrect chars past + // 3.2.0 but skip the chars fixed in 3.2.0 because they'll already be right. + if version != "3.2.0" { + Some((start, original)) + } else { + None + } + }, + |writer, mut values| { + values.sort_unstable_by_key(|(c, _)| *c); + write!(writer, "{values:?};").unwrap(); + }, + ); +} + +#[expect(clippy::too_many_arguments)] +fn write_derived( + base: &Path, + file_name: &str, + static_name: &str, + array_type: &str, + field: NonZeroUsize, + writer: &mut W, + parse: P, + write_vec: FW, +) where + W: Write, + P: FnMut(u32, u32, &str, &str) -> Option, + FW: FnMut(&mut W, Vec), +{ + let path = base.join(file_name); + let reader = BufReader::new(File::open(path).unwrap()); + writeln!(writer, "static {static_name}: &[{array_type}] = &").unwrap(); + parse_unicode_3_2(reader, field, writer, parse, write_vec); +} + +/// Parse Unicode 3.2.0 property files. +fn parse_unicode_3_2( + reader: impl BufRead, + field: NonZeroUsize, + writer: &mut W, + mut parse: P, + mut write_vec: FW, +) where + W: Write, + P: FnMut(u32, u32, &str, &str) -> Option, + FW: FnMut(&mut W, Vec), +{ + let mut parsed = Vec::new(); + + for line in reader.lines().map(Result::unwrap) { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + let mut fields = line.split(';'); + let range = fields.next().expect("Unicode data is missing a char range"); + let id = fields + .nth(field.get().saturating_sub(1)) + .expect("Unicode data is missing a property"); + let (start, end) = match range.split_once("..") { + Some((left, right)) => { + let start = u32::from_str_radix(left.trim(), 16).unwrap(); + let end = u32::from_str_radix(right.trim(), 16).unwrap(); + (start, end) + } + None => { + let start = u32::from_str_radix(range.trim(), 16).unwrap(); + (start, start) + } + }; + + let id = id.split_once('#').map_or(id, |(left, _)| left).trim(); + if let Some(val) = parse(start, end, id, line) { + parsed.push(val); + } + } + write_vec(writer, parsed); +} + +fn parse_general(id: &str) -> GeneralCategory { + match id.trim() { + "Cn" => GeneralCategory::Unassigned, + "Lu" => GeneralCategory::UppercaseLetter, + "Ll" => GeneralCategory::LowercaseLetter, + "Lt" => GeneralCategory::TitlecaseLetter, + "Lm" => GeneralCategory::ModifierLetter, + "Lo" => GeneralCategory::OtherLetter, + "Mn" => GeneralCategory::NonspacingMark, + "Mc" => GeneralCategory::SpacingMark, + "Me" => GeneralCategory::EnclosingMark, + "Nd" => GeneralCategory::DecimalNumber, + "Nl" => GeneralCategory::LetterNumber, + "No" => GeneralCategory::OtherNumber, + "Zs" => GeneralCategory::SpaceSeparator, + "Zl" => GeneralCategory::LineSeparator, + "Zp" => GeneralCategory::ParagraphSeparator, + "Cc" => GeneralCategory::Control, + "Cf" => GeneralCategory::Format, + "Co" => GeneralCategory::PrivateUse, + "Cs" => GeneralCategory::Surrogate, + "Pd" => GeneralCategory::DashPunctuation, + "Ps" => GeneralCategory::OpenPunctuation, + "Pe" => GeneralCategory::ClosePunctuation, + "Pc" => GeneralCategory::ConnectorPunctuation, + "Pi" => GeneralCategory::InitialPunctuation, + "Pf" => GeneralCategory::FinalPunctuation, + "Po" => GeneralCategory::OtherPunctuation, + "Sm" => GeneralCategory::MathSymbol, + "Sc" => GeneralCategory::CurrencySymbol, + "Sk" => GeneralCategory::ModifierSymbol, + "So" => GeneralCategory::OtherSymbol, + invalid => unreachable!("Unicode data contains valid properties: {invalid}"), + } +} + +fn parse_eaw(id: &str) -> &'static str { + match id.trim() { + "N" => "EastAsianWidth::Neutral", + "A" => "EastAsianWidth::Ambiguous", + "H" => "EastAsianWidth::Halfwidth", + "F" => "EastAsianWidth::Fullwidth", + "Na" => "EastAsianWidth::Narrow", + "W" => "EastAsianWidth::Wide", + invalid => unreachable!("Unicode data contains valid properties: {invalid}"), + } +} + +fn parse_bidi(id: &str) -> &'static str { + match id.trim() { + "L" => "BidiClass::LeftToRight", + "R" => "BidiClass::RightToLeft", + "EN" => "BidiClass::EuropeanNumber", + "ES" => "BidiClass::EuropeanSeparator", + "ET" => "BidiClass::EuropeanTerminator", + "AN" => "BidiClass::ArabicNumber", + "CS" => "BidiClass::CommonSeparator", + "B" => "BidiClass::ParagraphSeparator", + "S" => "BidiClass::SegmentSeparator", + "WS" => "BidiClass::WhiteSpace", + "ON" => "BidiClass::OtherNeutral", + "LRE" => "BidiClass::LeftToRightEmbedding", + "LRO" => "BidiClass::LeftToRightOverride", + "AL" => "BidiClass::ArabicLetter", + "RLE" => "BidiClass::RightToLeftEmbedding", + "RLO" => "BidiClass::RightToLeftOverride", + "PDF" => "BidiClass::PopDirectionalFormat", + "NSM" => "BidiClass::NonspacingMark", + "BN" => "BidiClass::BoundaryNeutral", + "FSI" => "BidiClass::FirstStrongIsolate", + "LRI" => "BidiClass::LeftToRightIsolate", + "RLI" => "BidiClass::RightToLeftIsolate", + "PDI" => "BidiClass::PopDirectionalIsolate", + invalid => unreachable!("Unicode data contains valid properties: {invalid}"), + } +} + +fn parse_numeric_type_val(val: NumericType) -> &'static str { + match val { + NumericType::None => "none", + NumericType::Decimal => "decimal", + NumericType::Digit => "digit", + NumericType::Numeric => "numeric", + _ => unreachable!("Unicode data contains valid properties"), + } +} + +fn parse_numeric_type_str(id: &str) -> &'static str { + match id { + "none" => "NumericType::None", + "decimal" => "NumericType::Decimal", + "digit" => "NumericType::Digit", + "numeric" => "NumericType::Numeric", + invalid => unreachable!("Unicode data contains valid properties: {invalid}"), + } +} + +#[derive(Debug, Default)] +enum DecompositionType { + #[default] + Canonical, + Compat, + Circle, + Final, + Font, + Fraction, + Initial, + Isolated, + Medial, + Narrow, + Nobreak, + Small, + Square, + Sub, + Super, + Vertical, + Wide, +} + +fn parse_decomp_type(id: &str) -> DecompositionType { + match id { + "canonical" => DecompositionType::Canonical, + "compat" => DecompositionType::Compat, + "circle" => DecompositionType::Circle, + "final" => DecompositionType::Final, + "font" => DecompositionType::Font, + "fraction" => DecompositionType::Fraction, + "initial" => DecompositionType::Initial, + "isolated" => DecompositionType::Isolated, + "medial" => DecompositionType::Medial, + "narrow" => DecompositionType::Narrow, + "noBreak" => DecompositionType::Nobreak, + "small" => DecompositionType::Small, + "square" => DecompositionType::Square, + "sub" => DecompositionType::Sub, + "super" => DecompositionType::Super, + "vertical" => DecompositionType::Vertical, + "wide" => DecompositionType::Wide, + invalid => unreachable!("Unicode data contains valid properties: {invalid}"), + } +} + +fn main() { + println!("cargo:rerun-if-changed=unicode/ucd32"); + println!("cargo:rerun-if-changed=unicode/latest"); + + let t_32 = thread::spawn(generate_unicode_3_2); + let t_numeric_type = thread::spawn(generate_numeric_type); + let t_numeric_value = thread::spawn(generate_numeric_value); + let t_latest = thread::spawn(generate_unicode_latest); + t_32.join().unwrap(); + t_numeric_type.join().unwrap(); + t_numeric_value.join().unwrap(); + t_latest.join().unwrap(); +} diff --git a/crates/unicode/src/case.rs b/crates/unicode/src/case.rs new file mode 100644 index 00000000000..c563db09339 --- /dev/null +++ b/crates/unicode/src/case.rs @@ -0,0 +1,393 @@ +//! Case mapping, case folding, and casing predicates for Python string casing. +//! +//! Code-point mappings (`simple_*`) return a single `char` and back the SRE +//! engine's `IGNORECASE` handling. String-level helpers (`capitalize`, `title`, +//! `swapcase`, `casefold`) implement the full, context-sensitive mappings used +//! by `str` methods and pass lone surrogates through unchanged. The casing +//! predicates expose the derived properties that `str.islower`/`isupper`/ +//! `istitle` need. +//! +//! Plain `str.lower`/`str.upper` have no such context beyond the final-sigma +//! rule that `str::to_lowercase` already applies, so they stay on +//! `rustpython_wtf8::Wtf8::to_lowercase`/`to_uppercase` rather than being +//! duplicated here. + +// spell-checker:ignore ΟΔΟΣ Οδος + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use icu_casemap::options::{LeadingAdjustment, TitlecaseOptions}; +use icu_casemap::{CaseMapper, TitlecaseMapper}; +use icu_locale::LanguageIdentifier; +use icu_properties::props::{ + BinaryProperty, CaseIgnorable, Cased, EnumeratedProperty, GeneralCategory, Lowercase, Uppercase, +}; +use rustpython_wtf8::{CodePoint, Wtf8, Wtf8Buf, Wtf8Chunk}; +use writeable::Writeable; + +// Code-point mappings + +/// Simple (one-to-one) lowercase mapping of `c` (`Py_UNICODE_TOLOWER`). +#[must_use] +pub fn simple_lowercase(c: char) -> char { + CaseMapper::new().simple_lowercase(c) +} + +/// Simple (one-to-one) uppercase mapping of `c` (`Py_UNICODE_TOUPPER`). +#[must_use] +pub fn simple_uppercase(c: char) -> char { + CaseMapper::new().simple_uppercase(c) +} + +/// Simple (one-to-one) titlecase mapping of `c` (`Py_UNICODE_TOTITLE`). +#[must_use] +pub fn simple_titlecase(c: char) -> char { + CaseMapper::new().simple_titlecase(c) +} + +/// Simple (one-to-one) case fold of `c`. +#[must_use] +pub fn simple_fold(c: char) -> char { + CaseMapper::new().simple_fold(c) +} + +// Casing predicates + +/// Whether `c` has the `Lowercase` property. +#[must_use] +pub fn is_lowercase(c: char) -> bool { + Lowercase::for_char(c) +} + +/// Whether `c` has the `Uppercase` property. +#[must_use] +pub fn is_uppercase(c: char) -> bool { + Uppercase::for_char(c) +} + +/// Whether `c` is a titlecase letter (general category `Lt`). +#[must_use] +pub fn is_titlecase(c: char) -> bool { + GeneralCategory::for_char(c) == GeneralCategory::TitlecaseLetter +} + +/// Whether `c` has the `Cased` property. +#[must_use] +pub fn is_cased(c: char) -> bool { + Cased::for_char(c) +} + +/// Whether `c` has the `Case_Ignorable` property. +#[must_use] +pub fn is_case_ignorable(c: char) -> bool { + CaseIgnorable::for_char(c) +} + +// String-level mappings + +/// Full Unicode case fold of `text` (`str.casefold`). +#[must_use] +pub fn casefold_str(text: &str) -> String { + CaseMapper::new().fold_string(text).to_string() +} + +/// Full Unicode case fold of `text`, passing lone surrogates through unchanged. +#[must_use] +pub fn casefold_wtf8(text: &Wtf8) -> Wtf8Buf { + map_wtf8(text, |s, out| { + CaseMapper::new() + .fold(s) + .write_to(out) + .expect("writing to an in-memory buffer cannot fail"); + }) +} + +/// Capitalize `text` (`str.capitalize`): titlecase the first cased character, +/// lowercase the rest, with final-sigma context. +#[must_use] +pub fn capitalize_str(text: &str) -> String { + let mut out = Vec::with_capacity(text.len()); + capitalize_utf8(text, &mut FmtWriter(&mut out)); + // SAFETY: capitalize_utf8 only appends valid UTF-8. + unsafe { String::from_utf8_unchecked(out) } +} + +/// Capitalize `text`, passing lone surrogates through unchanged. +/// +/// Only the first character of the whole string is titlecased; every later +/// character (including the first of a run that follows a lone surrogate) is +/// lowercased. +#[must_use] +pub fn capitalize_wtf8(text: &Wtf8) -> Wtf8Buf { + let mut out = Vec::with_capacity(text.len()); + let mut first = true; + for chunk in text.chunks() { + match chunk { + Wtf8Chunk::Utf8(s) => { + let mut writer = FmtWriter(&mut out); + if first { + capitalize_utf8(s, &mut writer); + first = false; + } else { + for (i, ch) in s.char_indices() { + lowercase_or_sigma(ch, s, i, &mut writer); + } + } + } + Wtf8Chunk::Surrogate(c) => { + first = false; + push_surrogate(&mut out, c); + } + } + } + // SAFETY: + // * capitalize_utf8 / lowercase_or_sigma only append valid UTF-8. + // * Surrogates are appended as valid WTF-8 (encoded via Wtf8Buf::push). + unsafe { Wtf8Buf::from_bytes_unchecked(out) } +} + +/// Title case `text` (`str.title`). +#[must_use] +pub fn title_str(text: &str) -> String { + let mut out = Vec::with_capacity(text.len()); + titlecase_string(text, &mut FmtWriter(&mut out)); + // SAFETY: titlecase_string only appends valid UTF-8. + unsafe { String::from_utf8_unchecked(out) } +} + +/// Title case `text`, passing lone surrogates through unchanged. +#[must_use] +pub fn title_wtf8(text: &Wtf8) -> Wtf8Buf { + map_wtf8(text, titlecase_string) +} + +/// Swap the case of every character in `text` (`str.swapcase`). +#[must_use] +pub fn swapcase_str(text: &str) -> String { + let mut out = Vec::with_capacity(text.len()); + swapcase_utf8(text, &mut FmtWriter(&mut out)); + // SAFETY: swapcase_utf8 only appends valid UTF-8. + unsafe { String::from_utf8_unchecked(out) } +} + +/// Swap the case of every character in `text`, passing lone surrogates through. +#[must_use] +pub fn swapcase_wtf8(text: &Wtf8) -> Wtf8Buf { + map_wtf8(text, swapcase_utf8) +} + +// Internal helpers + +/// Run `f` over each valid UTF-8 run of `text`, appending the mapped output and +/// carrying lone surrogates through unchanged. +fn map_wtf8(text: &Wtf8, f: impl Fn(&str, &mut FmtWriter<'_>)) -> Wtf8Buf { + let mut out = Vec::with_capacity(text.len()); + for chunk in text.chunks() { + match chunk { + Wtf8Chunk::Utf8(s) => f(s, &mut FmtWriter(&mut out)), + Wtf8Chunk::Surrogate(c) => push_surrogate(&mut out, c), + } + } + // SAFETY: + // * `f` only appends valid UTF-8. + // * Surrogates are appended as valid WTF-8 (encoded via Wtf8Buf::push). + unsafe { Wtf8Buf::from_bytes_unchecked(out) } +} + +/// Append a lone surrogate to `out` as valid WTF-8 bytes. +fn push_surrogate(out: &mut Vec, c: CodePoint) { + let mut buf = Wtf8Buf::new(); + buf.push(c); + out.extend_from_slice(buf.as_bytes()); +} + +fn capitalize_utf8(s: &str, out: &mut FmtWriter<'_>) { + let mut chars = s.char_indices(); + if let Some((first_pos, first_ch)) = chars.next() { + let first = &s[..first_pos + first_ch.len_utf8()]; + titlecase_segment(first, out); + } + for (i, ch) in chars { + lowercase_or_sigma(ch, s, i, out); + } +} + +/// Title case a string following CPython conventions. +/// +/// The first character of each run of cased characters is title cased and the +/// rest are lowercased; a new run starts after any non-cased character (digits, +/// whitespace, punctuation, etc.). +/// "123abc" -> "123Abc" +/// "123abc456def" -> "123Abc456Def" +/// "123 abc" -> "123 Abc" +fn titlecase_string(s: &str, out: &mut FmtWriter<'_>) { + let mut previous_is_cased = false; + for (i, ch) in s.char_indices() { + if previous_is_cased { + lowercase_or_sigma(ch, s, i, out); + } else { + titlecase_segment(&s[i..i + ch.len_utf8()], out); + } + + previous_is_cased = is_cased(ch); + } +} + +fn titlecase_segment(s: &str, out: &mut FmtWriter<'_>) { + // Callers pass a single first-of-word code point, which Python titlecases + // unconditionally (applying its titlecase mapping). The default `Auto` + // leading adjustment looks for a head in Letter/Number/Symbol/Private_Use + // and skips anything else, dropping the titlecase mapping of cased marks + // such as U+0345 (`ͅ`, general category Mn) -> U+0399 (`Ι`). `None` + // titlecases the code point as given. + let mut options = TitlecaseOptions::default(); + options.leading_adjustment = Some(LeadingAdjustment::None); + TitlecaseMapper::new() + .titlecase_segment(s, &LanguageIdentifier::UNKNOWN, options) + .write_to(out) + .expect("writing to an in-memory buffer cannot fail"); +} + +fn lowercase_or_sigma(ch: char, s: &str, i: usize, out: &mut FmtWriter<'_>) { + let sigma = 'Σ'; + if ch == sigma { + push_char(handle_capital_sigma(s, i), out); + } else { + for ch in ch.to_lowercase() { + push_char(ch, out); + } + } +} + +// Handle context-sensitive sigma. +// +// Sigma is handled as a special case. This is more efficient than using icu4x +// to scan the entire string with CaseMapper because CaseMapper would allocate +// to produce a new string. +fn handle_capital_sigma(s: &str, i: usize) -> char { + let (left, rest) = s.split_at(i); + let right = &rest['Σ'.len_utf8()..]; + + // Check if any chars before or after sigma are cased. + let before = left + .chars() + .rev() + .find(|&ch| !is_case_ignorable(ch)) + .is_some_and(is_cased); + let after = right + .chars() + .find(|&ch| !is_case_ignorable(ch)) + .is_some_and(is_cased); + if before && !after { 'ς' } else { 'σ' } +} + +fn swapcase_utf8(s: &str, out: &mut FmtWriter<'_>) { + for (i, ch) in s.char_indices() { + if ch.is_uppercase() { + lowercase_or_sigma(ch, s, i, out); + } else if ch.is_lowercase() { + for ch in ch.to_uppercase() { + push_char(ch, out); + } + } else { + push_char(ch, out); + } + } +} + +fn push_char(ch: char, out: &mut FmtWriter<'_>) { + let mut buf = [0u8; 4]; + out.0.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes()); +} + +/// Adapter so `icu`'s `Writeable` output can be appended to a byte buffer. +struct FmtWriter<'a>(&'a mut Vec); + +impl core::fmt::Write for FmtWriter<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.0.extend_from_slice(s.as_bytes()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rustpython_wtf8::{CodePoint, Wtf8Buf}; + + use super::{ + capitalize_str, casefold_str, is_case_ignorable, is_cased, is_lowercase, is_titlecase, + is_uppercase, simple_lowercase, simple_uppercase, swapcase_str, title_str, title_wtf8, + }; + + #[test] + fn casefold_full_mappings() { + // ß case-folds to "ss" + assert_eq!(casefold_str("ß"), "ss"); + assert_eq!(casefold_str("Σ"), "σ"); + } + + #[test] + fn simple_mappings_are_one_to_one() { + // ß has no simple uppercase mapping, so it stays unchanged (unlike the + // full mapping "SS"). + assert_eq!(simple_uppercase('ß'), 'ß'); + assert_eq!(simple_uppercase('a'), 'A'); + assert_eq!(simple_lowercase('A'), 'a'); + // Dž (U+01C5) simple-titlecases to itself but upper/lowercases away. + assert_eq!(simple_uppercase('Dž'), 'DŽ'); + assert_eq!(simple_lowercase('Dž'), 'dž'); + } + + #[test] + fn casing_predicates() { + assert!(is_lowercase('a')); + assert!(!is_lowercase('A')); + assert!(is_uppercase('A')); + assert!(is_titlecase('Dž')); + assert!(!is_titlecase('D')); + assert!(is_cased('a') && is_cased('A')); + assert!(!is_cased('1')); + assert!(is_case_ignorable('\'')); + assert!(!is_case_ignorable('a')); + } + + #[test] + fn capitalize_final_sigma() { + // Final sigma at end of a cased run becomes ς. + assert_eq!(capitalize_str("ΟΔΟΣ"), "Οδος"); + assert_eq!(title_str("hello world"), "Hello World"); + assert_eq!(swapcase_str("Hello"), "hELLO"); + } + + #[test] + fn titlecase_first_of_word_takes_titlecase_mapping() { + // A leading cased combining mark still takes its titlecase mapping: + // U+0345 (ͅ, general category Mn) titlecases to U+0399 (Ι), even though + // it is not a Letter/Number/Symbol head. + assert_eq!(title_str("\u{0345}"), "\u{0399}"); + assert_eq!(capitalize_str("\u{0345}"), "\u{0399}"); + assert_eq!(title_str("\u{0345}a"), "\u{0399}a"); + // Full (one-to-many) titlecase mappings still apply to the first + // character of each word. + // cspell:ignore finnish NNISH dzungla Dzungla ßhello Sshello + assert_eq!(capitalize_str("finnish"), "Finnish"); + assert_eq!(title_str("fiNNISH"), "Finnish"); + assert_eq!(capitalize_str("dzungla"), "Dzungla"); + assert_eq!(capitalize_str("ßhello"), "Sshello"); + } + + #[test] + fn wtf8_passes_surrogates_through() { + let mut buf = Wtf8Buf::from("ab cd"); + buf.push(CodePoint::from_u32(0xD800).unwrap()); + let titled = title_wtf8(&buf); + assert!(titled.code_points().any(|c| c.to_u32() == 0xD800)); + assert_eq!( + titled.code_points().next().and_then(|c| c.to_char()), + Some('A') + ); + } +} diff --git a/crates/unicode/src/classify.rs b/crates/unicode/src/classify.rs new file mode 100644 index 00000000000..7a333763a57 --- /dev/null +++ b/crates/unicode/src/classify.rs @@ -0,0 +1,116 @@ +//! Character classification predicates for Python `str` methods. +//! +//! Each predicate operates on a single Unicode scalar. Callers iterating over +//! WTF-8 text apply these per code point, treating lone surrogates as failing +//! every predicate. + +use icu_properties::props::{ + BidiClass, EnumeratedProperty, GeneralCategory, GeneralCategoryGroup, NumericType, +}; + +/// `str.isalpha` for a single character: any `Letter` general category. +#[must_use] +pub fn is_alpha(c: char) -> bool { + GeneralCategoryGroup::Letter.contains(GeneralCategory::for_char(c)) +} + +/// `str.isalnum` for a single character: any `Letter` or `Number` category. +#[must_use] +pub fn is_alnum(c: char) -> bool { + GeneralCategoryGroup::Letter + .union(GeneralCategoryGroup::Number) + .contains(GeneralCategory::for_char(c)) +} + +/// `str.isdecimal` for a single character: `Decimal_Number` general category. +#[must_use] +pub fn is_decimal(c: char) -> bool { + matches!(GeneralCategory::for_char(c), GeneralCategory::DecimalNumber) +} + +/// `str.isdigit` for a single character: `Numeric_Type` of `Digit` or `Decimal`. +#[must_use] +pub fn is_digit(c: char) -> bool { + matches!( + NumericType::for_char(c), + NumericType::Digit | NumericType::Decimal + ) +} + +/// `str.isnumeric` for a single character: any numeric `Numeric_Type`. +#[must_use] +pub fn is_numeric(c: char) -> bool { + matches!( + NumericType::for_char(c), + NumericType::Decimal | NumericType::Digit | NumericType::Numeric + ) +} + +/// `str.isspace` for a single character: `Space_Separator`, or a bidi +/// whitespace / paragraph / segment separator. +#[must_use] +pub fn is_space(c: char) -> bool { + matches!( + GeneralCategory::for_char(c), + GeneralCategory::SpaceSeparator + ) || matches!( + BidiClass::for_char(c), + BidiClass::WhiteSpace | BidiClass::ParagraphSeparator | BidiClass::SegmentSeparator + ) +} + +/// `str.isprintable` for a single character: ASCII space is printable, as are +/// all characters that survive [`is_repr_printable`]. +#[must_use] +pub fn is_printable(c: char) -> bool { + c == '\u{0020}' || is_repr_printable(c) +} + +/// Repr/escape printable semantics. +/// +/// The following categories are not printable: +/// * Cc (Other, Control) +/// * Cf (Other, Format) +/// * Cs (Other, Surrogate) +/// * Co (Other, Private Use) +/// * Cn (Other, Not Assigned) +/// * Zl (Separator, Line) +/// * Zp (Separator, Paragraph) +/// * Zs (Separator, Space), including ASCII space +#[must_use] +pub fn is_repr_printable(c: char) -> bool { + !matches!( + GeneralCategory::for_char(c), + GeneralCategory::SpaceSeparator + | GeneralCategory::LineSeparator + | GeneralCategory::ParagraphSeparator + | GeneralCategory::Control + | GeneralCategory::Format + | GeneralCategory::Surrogate + | GeneralCategory::PrivateUse + | GeneralCategory::Unassigned + ) +} + +#[cfg(test)] +mod tests { + use super::{is_decimal, is_digit, is_numeric}; + + #[test] + fn numeric_type_chain_holds() { + // isdecimal ⊂ isdigit ⊂ isnumeric + for c in ('\0'..='\u{2FFFF}').filter_map(|c| char::from_u32(c as u32)) { + if is_decimal(c) { + assert!(is_digit(c), "{c:?} decimal but not digit"); + } + if is_digit(c) { + assert!(is_numeric(c), "{c:?} digit but not numeric"); + } + } + assert!(is_decimal('5')); + assert!(!is_decimal('²')); + assert!(is_digit('²')); + assert!(!is_digit('⅓')); + assert!(is_numeric('⅓')); + } +} diff --git a/crates/unicode/src/data.rs b/crates/unicode/src/data.rs new file mode 100644 index 00000000000..83d81612b6a --- /dev/null +++ b/crates/unicode/src/data.rs @@ -0,0 +1,373 @@ +//! Access to the Unicode character database (`unicodedata`). +//! +//! Owns the generated Unicode 3.2.0 / latest tables and the +//! `icu4x`/`unicode_names2` lookups behind them. + +// spell-checker:ignore codep decomp DECOMP unidata + +use core::{cmp::Ordering, fmt::Write, hint::cold_path}; + +use alloc::{ + format, + string::{String, ToString}, +}; + +use icu_normalizer::properties::{CanonicalDecomposition, Decomposed}; +use icu_properties::props::{ + BidiClass, BidiMirrored, BinaryProperty, CanonicalCombiningClass, EastAsianWidth, + EnumeratedProperty, GeneralCategory, NamedEnumeratedProperty, NumericType, +}; +use rustpython_wtf8::CodePoint; + +include!(concat!(env!("OUT_DIR"), "/generated/unicode_3_2.rs")); +include!(concat!(env!("OUT_DIR"), "/generated/unicode_latest.rs")); +include!(concat!(env!("OUT_DIR"), "/generated/unicode_num_type.rs")); +include!(concat!( + env!("OUT_DIR"), + "/generated/unicode_numeric_value.rs" +)); + +#[derive(Clone, Copy)] +enum DecompositionType { + Compat, + Circle, + Final, + Font, + Fraction, + Initial, + Isolated, + Medial, + Narrow, + Nobreak, + Small, + Square, + Sub, + Super, + Vertical, + Wide, +} + +impl DecompositionType { + const fn type_tag(self) -> &'static str { + match self { + Self::Compat => "compat", + Self::Circle => "circle", + Self::Final => "final", + Self::Font => "font", + Self::Fraction => "fraction", + Self::Initial => "initial", + Self::Isolated => "isolated", + Self::Medial => "medial", + Self::Narrow => "narrow", + Self::Nobreak => "noBreak", + Self::Small => "small", + Self::Square => "square", + Self::Sub => "sub", + Self::Super => "super", + Self::Vertical => "vertical", + Self::Wide => "wide", + } + } +} + +fn lookup_property(table: &[(u32, u32, T)], ch: char) -> Option { + let ch = ch as u32; + table + .binary_search_by(|&(start, end, _)| { + if ch > end { + Ordering::Less + } else if ch < start { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .ok() + .map(|i| table[i].2) +} + +fn lookup_numeric_val(ch: char, modern: bool) -> Option { + if modern { + lookup_property(NUMERIC_VALUES, ch) + } else { + cold_path(); + lookup_property(NUMERIC_VALUES_DIFF, ch).or_else(|| { + NUMERIC_VAL_EXISTS_32 + .binary_search_by(|&(start, end)| { + let ch = ch as u32; + if ch > end { + Ordering::Less + } else if ch < start { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .ok() + .and_then(|_| lookup_property(NUMERIC_VALUES, ch)) + }) + } +} + +/// The version string of the latest Unicode database bundled with the standard +/// library (`unicodedata.unidata_version`). +#[must_use] +pub fn unicode_version() -> String { + format!( + "{}.{}.{}", + char::UNICODE_VERSION.0, + char::UNICODE_VERSION.1, + char::UNICODE_VERSION.2 + ) +} + +/// Look up a character by its Unicode name (`unicodedata.lookup`). +pub use unicode_names2::character as lookup_character; + +/// The Unicode name of `ch` (`unicodedata.name`), if any. +#[must_use] +pub fn character_name(ch: char) -> Option { + unicode_names2::name(ch).map(|name| name.to_string()) +} + +/// A view over the Unicode character database at a fixed version. +/// +/// `modern` selects the latest bundled UCD; otherwise the Unicode 3.2.0 tables +/// used by `unicodedata.ucd_3_2_0` are consulted. +#[derive(Debug, Clone, Copy)] +pub struct Ucd { + modern: bool, +} + +impl Ucd { + #[must_use] + pub const fn new(modern: bool) -> Self { + Self { modern } + } + + #[must_use] + pub fn category(&self, c: CodePoint) -> &'static str { + let Some(c) = c.to_char() else { + return GeneralCategory::Surrogate.short_name(); + }; + if self.modern { + Some(GeneralCategory::for_char(c)) + } else { + cold_path(); + lookup_property(GENERAL_CATEGORY, c) + } + .unwrap_or(GeneralCategory::Unassigned) + .short_name() + } + + #[must_use] + pub fn bidirectional(&self, c: CodePoint) -> &'static str { + c.to_char() + .and_then(|c| { + if self.modern { + Some(BidiClass::for_char(c)) + } else { + cold_path(); + lookup_property(BIDI_CLASS, c) + } + }) + .unwrap_or(BidiClass::LeftToRight) + .short_name() + } + + #[must_use] + pub fn east_asian_width(&self, c: CodePoint) -> &'static str { + c.to_char() + .and_then(|c| { + if self.modern { + Some(EastAsianWidth::for_char(c)) + } else { + cold_path(); + // CPython overrides characters in the PUA for 3.2.0. + // Basic Multilingual Plane: + // https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane + // https://en.wikipedia.org/wiki/Private_Use_Areas + // https://www.unicode.org/reports/tr11/tr11-10.html + // https://www.unicode.org/reports/tr11/ + // + // Currently, this implementation is incomplete because I can't figure + // out what CPython is doing. + lookup_property(EAST_ASIAN_WIDTH, c) + } + }) + .unwrap_or(EastAsianWidth::Neutral) + .short_name() + } + + #[must_use] + pub fn mirrored(&self, c: CodePoint) -> i32 { + c.to_char().map_or(0, |c| { + (if self.modern { + BidiMirrored::for_char(c) + } else { + cold_path(); + let c = c as u32; + BIDI_MIRRORED + .binary_search_by(|&(start, end)| { + if c > end { + Ordering::Less + } else if c < start { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .is_ok() + }) as i32 + }) + } + + #[must_use] + pub fn combining(&self, c: CodePoint) -> u8 { + c.to_char() + .and_then(|c| { + if self.modern { + Some(CanonicalCombiningClass::for_char(c)) + } else { + cold_path(); + lookup_property(COMBINING_CLASS, c) + } + }) + .unwrap_or(CanonicalCombiningClass::NotReordered) + .to_icu4c_value() + } + + #[must_use] + pub fn decomposition(&self, c: CodePoint) -> String { + let Some(ch) = c.to_char() else { + return String::new(); + }; + + // Decomposition is remarkable stable according to the normalization file, + // so the updates slice is very small - only about four char pairs. Linearly searching + // it is very fast. The file lists the original, incorrect decomp and the fixed char. + // For 3.2.0, we use the original decomp for compatibility while ignoring the update. + // + // Finally, we don't have to do anything for the latest UCD as it's already updated. + if self.modern + && let Some((_, original)) = DECOMP_UPDATES + .iter() + .find(|&&(codep, _original)| codep == ch as u32) + { + format!("{original:04X}") + } else if let Ok(i) = + DECOMP_COMPAT.binary_search_by_key(&(ch as u32), |&(codep, _, _)| codep) + { + // Compatibility decomposition + // `icu4x` doesn't expose a non-recursive, compatibility decomposer so we + // have to do it manually for now. + let tag = DECOMP_COMPAT[i].1.type_tag(); + let end = DECOMP_COMPAT[i].2; + let start = i + .checked_sub(1) + .map(|i| DECOMP_COMPAT[i].2) + .unwrap_or_default(); + + let decomp = &DECOMP_RANGE[start..end]; + let cap = decomp.len() * 10 + decomp.len() + tag.len() + 1; + let mut out = String::with_capacity(cap); + + write!(out, "<{tag}>").unwrap(); + for ch in decomp { + write!(out, " {ch:04X}").unwrap(); + } + + out + } else { + // Canonical decomposition + let decomposed = CanonicalDecomposition::new().decompose(ch); + match decomposed { + Decomposed::Default => String::new(), + Decomposed::Singleton(ch) => format!("{:04X}", ch as u32), + Decomposed::Expansion(l, r) => format!("{:04X} {:04X}", l as u32, r as u32), + } + } + } + + fn numeric_type_matches(self, ch: CodePoint, expected: &[NumericType]) -> Option { + let ch = ch.to_char()?; + + let actual = if self.modern { + NumericType::for_char(ch) + } else { + cold_path(); + lookup_property(NUMERIC_TYPE_DIFF, ch).unwrap_or_else(|| NumericType::for_char(ch)) + }; + + expected.contains(&actual).then_some(ch) + } + + /// The integer digit value of `c` (`unicodedata.digit`), if it has one. + #[must_use] + pub fn digit(&self, c: CodePoint) -> Option { + let expected = [NumericType::Decimal, NumericType::Digit]; + self.numeric_type_matches(c, &expected).and_then(|ch| { + let value = lookup_numeric_val(ch, true)?; + let int = value as u64; + (int as f64 == value).then_some(int) + }) + } + + /// The integer decimal value of `c` (`unicodedata.decimal`), if it has one. + #[must_use] + pub fn decimal(&self, c: CodePoint) -> Option { + let expected = [NumericType::Decimal]; + self.numeric_type_matches(c, &expected).and_then(|ch| { + let value = lookup_numeric_val(ch, self.modern)?; + let int = value as u64; + (int as f64 == value).then_some(int) + }) + } + + /// The numeric value of `c` (`unicodedata.numeric`), if it has one. + #[must_use] + pub fn numeric(&self, c: CodePoint) -> Option { + let expected = &NumericType::ALL_VALUES[1..]; + self.numeric_type_matches(c, expected) + .and_then(|ch| lookup_numeric_val(ch, self.modern)) + } + + #[must_use] + pub fn unidata_version(&self) -> String { + if self.modern { + unicode_version() + } else { + "3.2.0".into() + } + } +} + +#[cfg(test)] +mod tests { + use rustpython_wtf8::CodePoint; + + use super::{Ucd, character_name, lookup_character}; + + fn cp(ch: char) -> CodePoint { + CodePoint::from(ch) + } + + #[test] + fn data_queries_match_unicodedata_behavior() { + let ucd = Ucd::new(true); + assert_eq!(ucd.category(cp('A')), "Lu"); + assert_eq!(ucd.category(CodePoint::from_u32(0xD800).unwrap()), "Cs"); + assert_eq!(lookup_character("SNOWMAN"), Some('☃')); + assert_eq!(character_name('☃').as_deref(), Some("SNOWMAN")); + assert_eq!(ucd.decimal(cp('५')), Some(5)); + assert_eq!(ucd.digit(cp('²')), Some(2)); + let third = ucd.numeric(cp('⅓')).unwrap(); + assert!((third - 1.0 / 3.0).abs() < 1e-6, "got {third}"); + } + + #[test] + fn ucd_3_2_0_view_differs_from_modern() { + let legacy = Ucd::new(false); + assert_eq!(legacy.unidata_version(), "3.2.0"); + } +} diff --git a/crates/unicode/src/identifier.rs b/crates/unicode/src/identifier.rs new file mode 100644 index 00000000000..413c722feb0 --- /dev/null +++ b/crates/unicode/src/identifier.rs @@ -0,0 +1,37 @@ +//! Python identifier predicates (`str.isidentifier`). + +use icu_properties::props::{BinaryProperty, XidContinue, XidStart}; + +/// Whether `c` has the `XID_Start` property. +#[must_use] +pub fn is_xid_start(c: char) -> bool { + XidStart::for_char(c) +} + +/// Whether `c` has the `XID_Continue` property. +#[must_use] +pub fn is_xid_continue(c: char) -> bool { + XidContinue::for_char(c) +} + +/// Whether `c` may start a Python identifier: `_` or `XID_Start`. +#[must_use] +pub fn is_start(c: char) -> bool { + c == '_' || is_xid_start(c) +} + +/// Whether `c` may continue a Python identifier: `XID_Continue`. +pub use is_xid_continue as is_continue; + +#[cfg(test)] +mod tests { + use super::{is_continue, is_start}; + + #[test] + fn identifier_predicates() { + assert!(is_start('_')); + assert!(is_start('가')); + assert!(!is_start('1')); + assert!(is_continue('1')); + } +} diff --git a/crates/unicode/src/lib.rs b/crates/unicode/src/lib.rs new file mode 100644 index 00000000000..a3f3eceb1c7 --- /dev/null +++ b/crates/unicode/src/lib.rs @@ -0,0 +1,19 @@ +//! Runtime-independent CPython-compatible Unicode semantics and data. +//! +//! Every entry point operates on plain `char`/`u32`/`CodePoint`/`&Wtf8` values +//! so it can be shared by any Python runtime; argument extraction and Python +//! exception mapping stay with the caller. There is no global mutable state and +//! results depend only on inputs. + +#![no_std] + +extern crate alloc; + +pub mod case; +pub mod classify; +pub mod data; +pub mod identifier; +pub mod normalize; + +pub use data::{Ucd, character_name, lookup_character, unicode_version}; +pub use normalize::{NormalizeForm, is_normalized, normalize}; diff --git a/crates/unicode/src/normalize.rs b/crates/unicode/src/normalize.rs new file mode 100644 index 00000000000..e2f80d02439 --- /dev/null +++ b/crates/unicode/src/normalize.rs @@ -0,0 +1,111 @@ +//! Unicode normalization (`unicodedata.normalize` / `is_normalized`). + +// spell-checker:ignore nfkc + +use core::str::FromStr; + +use icu_normalizer::{ComposingNormalizerBorrowed, DecomposingNormalizerBorrowed}; +use rustpython_wtf8::{Wtf8, Wtf8Buf, Wtf8Chunk}; + +/// One of the four Unicode normalization forms. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum NormalizeForm { + Nfc, + Nfkc, + Nfd, + Nfkd, +} + +impl FromStr for NormalizeForm { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "NFC" => Ok(Self::Nfc), + "NFKC" => Ok(Self::Nfkc), + "NFD" => Ok(Self::Nfd), + "NFKD" => Ok(Self::Nfkd), + _ => Err(()), + } + } +} + +/// Normalize `text` to `form` (`unicodedata.normalize`). +/// +/// Lone surrogates are passed through unchanged; only the valid UTF-8 runs are +/// normalized. +#[must_use] +pub fn normalize(form: NormalizeForm, text: &Wtf8) -> Wtf8Buf { + match form { + NormalizeForm::Nfc => { + let normalizer = ComposingNormalizerBorrowed::new_nfc(); + text.map_utf8(|s| normalizer.normalize_iter(s.chars())) + .collect() + } + NormalizeForm::Nfkc => { + let normalizer = ComposingNormalizerBorrowed::new_nfkc(); + text.map_utf8(|s| normalizer.normalize_iter(s.chars())) + .collect() + } + NormalizeForm::Nfd => { + let normalizer = DecomposingNormalizerBorrowed::new_nfd(); + text.map_utf8(|s| normalizer.normalize_iter(s.chars())) + .collect() + } + NormalizeForm::Nfkd => { + let normalizer = DecomposingNormalizerBorrowed::new_nfkd(); + text.map_utf8(|s| normalizer.normalize_iter(s.chars())) + .collect() + } + } +} + +/// Whether `text` is already in `form` (`unicodedata.is_normalized`). +/// +/// Lone surrogates split the text into valid UTF-8 runs; each run is checked +/// independently, matching the run-wise normalization performed by [`normalize`]. +#[must_use] +pub fn is_normalized(form: NormalizeForm, text: &Wtf8) -> bool { + let check: fn(&str) -> bool = match form { + NormalizeForm::Nfc => |s| ComposingNormalizerBorrowed::new_nfc().is_normalized(s), + NormalizeForm::Nfkc => |s| ComposingNormalizerBorrowed::new_nfkc().is_normalized(s), + NormalizeForm::Nfd => |s| DecomposingNormalizerBorrowed::new_nfd().is_normalized(s), + NormalizeForm::Nfkd => |s| DecomposingNormalizerBorrowed::new_nfkd().is_normalized(s), + }; + text.chunks().all(|chunk| match chunk { + Wtf8Chunk::Utf8(s) => check(s), + Wtf8Chunk::Surrogate(_) => true, + }) +} + +#[cfg(test)] +mod tests { + use rustpython_wtf8::{CodePoint, Wtf8Buf}; + + use super::{NormalizeForm, is_normalized, normalize}; + + #[test] + fn normalization_round_trips() { + let composed = Wtf8Buf::from("é"); + let decomposed = normalize(NormalizeForm::Nfd, &composed); + assert_eq!(normalize(NormalizeForm::Nfc, &decomposed), composed); + assert!(is_normalized( + NormalizeForm::Nfc, + Wtf8Buf::from("é").as_ref() + )); + assert!(!is_normalized( + NormalizeForm::Nfd, + Wtf8Buf::from("é").as_ref() + )); + } + + #[test] + fn is_normalized_skips_lone_surrogates() { + // A lone surrogate splits the text into UTF-8 runs; each run is checked + // independently, so a surrogate next to normalized text stays normalized. + let mut buf = Wtf8Buf::from("é"); + buf.push(CodePoint::from_u32(0xD800).unwrap()); + assert!(is_normalized(NormalizeForm::Nfc, &buf)); + assert!(!is_normalized(NormalizeForm::Nfd, &buf)); + } +} diff --git a/crates/unicode/tests/data/cpython3.14_mappings.txt b/crates/unicode/tests/data/cpython3.14_mappings.txt new file mode 100644 index 00000000000..f686c5fd15a --- /dev/null +++ b/crates/unicode/tests/data/cpython3.14_mappings.txt @@ -0,0 +1,2 @@ +# unidata_version 16.0.0 +tolower 41:61,42:62,43:63,44:64,45:65,46:66,47:67,48:68,49:69,4A:6A,4B:6B,4C:6C,4D:6D,4E:6E,4F:6F,50:70,51:71,52:72,53:73,54:74,55:75,56:76,57:77,58:78,59:79,5A:7A,C0:E0,C1:E1,C2:E2,C3:E3,C4:E4,C5:E5,C6:E6,C7:E7,C8:E8,C9:E9,CA:EA,CB:EB,CC:EC,CD:ED,CE:EE,CF:EF,D0:F0,D1:F1,D2:F2,D3:F3,D4:F4,D5:F5,D6:F6,D8:F8,D9:F9,DA:FA,DB:FB,DC:FC,DD:FD,DE:FE,100:101,102:103,104:105,106:107,108:109,10A:10B,10C:10D,10E:10F,110:111,112:113,114:115,116:117,118:119,11A:11B,11C:11D,11E:11F,120:121,122:123,124:125,126:127,128:129,12A:12B,12C:12D,12E:12F,130:69,132:133,134:135,136:137,139:13A,13B:13C,13D:13E,13F:140,141:142,143:144,145:146,147:148,14A:14B,14C:14D,14E:14F,150:151,152:153,154:155,156:157,158:159,15A:15B,15C:15D,15E:15F,160:161,162:163,164:165,166:167,168:169,16A:16B,16C:16D,16E:16F,170:171,172:173,174:175,176:177,178:FF,179:17A,17B:17C,17D:17E,181:253,182:183,184:185,186:254,187:188,189:256,18A:257,18B:18C,18E:1DD,18F:259,190:25B,191:192,193:260,194:263,196:269,197:268,198:199,19C:26F,19D:272,19F:275,1A0:1A1,1A2:1A3,1A4:1A5,1A6:280,1A7:1A8,1A9:283,1AC:1AD,1AE:288,1AF:1B0,1B1:28A,1B2:28B,1B3:1B4,1B5:1B6,1B7:292,1B8:1B9,1BC:1BD,1C4:1C6,1C5:1C6,1C7:1C9,1C8:1C9,1CA:1CC,1CB:1CC,1CD:1CE,1CF:1D0,1D1:1D2,1D3:1D4,1D5:1D6,1D7:1D8,1D9:1DA,1DB:1DC,1DE:1DF,1E0:1E1,1E2:1E3,1E4:1E5,1E6:1E7,1E8:1E9,1EA:1EB,1EC:1ED,1EE:1EF,1F1:1F3,1F2:1F3,1F4:1F5,1F6:195,1F7:1BF,1F8:1F9,1FA:1FB,1FC:1FD,1FE:1FF,200:201,202:203,204:205,206:207,208:209,20A:20B,20C:20D,20E:20F,210:211,212:213,214:215,216:217,218:219,21A:21B,21C:21D,21E:21F,220:19E,222:223,224:225,226:227,228:229,22A:22B,22C:22D,22E:22F,230:231,232:233,23A:2C65,23B:23C,23D:19A,23E:2C66,241:242,243:180,244:289,245:28C,246:247,248:249,24A:24B,24C:24D,24E:24F,370:371,372:373,376:377,37F:3F3,386:3AC,388:3AD,389:3AE,38A:3AF,38C:3CC,38E:3CD,38F:3CE,391:3B1,392:3B2,393:3B3,394:3B4,395:3B5,396:3B6,397:3B7,398:3B8,399:3B9,39A:3BA,39B:3BB,39C:3BC,39D:3BD,39E:3BE,39F:3BF,3A0:3C0,3A1:3C1,3A3:3C3,3A4:3C4,3A5:3C5,3A6:3C6,3A7:3C7,3A8:3C8,3A9:3C9,3AA:3CA,3AB:3CB,3CF:3D7,3D8:3D9,3DA:3DB,3DC:3DD,3DE:3DF,3E0:3E1,3E2:3E3,3E4:3E5,3E6:3E7,3E8:3E9,3EA:3EB,3EC:3ED,3EE:3EF,3F4:3B8,3F7:3F8,3F9:3F2,3FA:3FB,3FD:37B,3FE:37C,3FF:37D,400:450,401:451,402:452,403:453,404:454,405:455,406:456,407:457,408:458,409:459,40A:45A,40B:45B,40C:45C,40D:45D,40E:45E,40F:45F,410:430,411:431,412:432,413:433,414:434,415:435,416:436,417:437,418:438,419:439,41A:43A,41B:43B,41C:43C,41D:43D,41E:43E,41F:43F,420:440,421:441,422:442,423:443,424:444,425:445,426:446,427:447,428:448,429:449,42A:44A,42B:44B,42C:44C,42D:44D,42E:44E,42F:44F,460:461,462:463,464:465,466:467,468:469,46A:46B,46C:46D,46E:46F,470:471,472:473,474:475,476:477,478:479,47A:47B,47C:47D,47E:47F,480:481,48A:48B,48C:48D,48E:48F,490:491,492:493,494:495,496:497,498:499,49A:49B,49C:49D,49E:49F,4A0:4A1,4A2:4A3,4A4:4A5,4A6:4A7,4A8:4A9,4AA:4AB,4AC:4AD,4AE:4AF,4B0:4B1,4B2:4B3,4B4:4B5,4B6:4B7,4B8:4B9,4BA:4BB,4BC:4BD,4BE:4BF,4C0:4CF,4C1:4C2,4C3:4C4,4C5:4C6,4C7:4C8,4C9:4CA,4CB:4CC,4CD:4CE,4D0:4D1,4D2:4D3,4D4:4D5,4D6:4D7,4D8:4D9,4DA:4DB,4DC:4DD,4DE:4DF,4E0:4E1,4E2:4E3,4E4:4E5,4E6:4E7,4E8:4E9,4EA:4EB,4EC:4ED,4EE:4EF,4F0:4F1,4F2:4F3,4F4:4F5,4F6:4F7,4F8:4F9,4FA:4FB,4FC:4FD,4FE:4FF,500:501,502:503,504:505,506:507,508:509,50A:50B,50C:50D,50E:50F,510:511,512:513,514:515,516:517,518:519,51A:51B,51C:51D,51E:51F,520:521,522:523,524:525,526:527,528:529,52A:52B,52C:52D,52E:52F,531:561,532:562,533:563,534:564,535:565,536:566,537:567,538:568,539:569,53A:56A,53B:56B,53C:56C,53D:56D,53E:56E,53F:56F,540:570,541:571,542:572,543:573,544:574,545:575,546:576,547:577,548:578,549:579,54A:57A,54B:57B,54C:57C,54D:57D,54E:57E,54F:57F,550:580,551:581,552:582,553:583,554:584,555:585,556:586,10A0:2D00,10A1:2D01,10A2:2D02,10A3:2D03,10A4:2D04,10A5:2D05,10A6:2D06,10A7:2D07,10A8:2D08,10A9:2D09,10AA:2D0A,10AB:2D0B,10AC:2D0C,10AD:2D0D,10AE:2D0E,10AF:2D0F,10B0:2D10,10B1:2D11,10B2:2D12,10B3:2D13,10B4:2D14,10B5:2D15,10B6:2D16,10B7:2D17,10B8:2D18,10B9:2D19,10BA:2D1A,10BB:2D1B,10BC:2D1C,10BD:2D1D,10BE:2D1E,10BF:2D1F,10C0:2D20,10C1:2D21,10C2:2D22,10C3:2D23,10C4:2D24,10C5:2D25,10C7:2D27,10CD:2D2D,13A0:AB70,13A1:AB71,13A2:AB72,13A3:AB73,13A4:AB74,13A5:AB75,13A6:AB76,13A7:AB77,13A8:AB78,13A9:AB79,13AA:AB7A,13AB:AB7B,13AC:AB7C,13AD:AB7D,13AE:AB7E,13AF:AB7F,13B0:AB80,13B1:AB81,13B2:AB82,13B3:AB83,13B4:AB84,13B5:AB85,13B6:AB86,13B7:AB87,13B8:AB88,13B9:AB89,13BA:AB8A,13BB:AB8B,13BC:AB8C,13BD:AB8D,13BE:AB8E,13BF:AB8F,13C0:AB90,13C1:AB91,13C2:AB92,13C3:AB93,13C4:AB94,13C5:AB95,13C6:AB96,13C7:AB97,13C8:AB98,13C9:AB99,13CA:AB9A,13CB:AB9B,13CC:AB9C,13CD:AB9D,13CE:AB9E,13CF:AB9F,13D0:ABA0,13D1:ABA1,13D2:ABA2,13D3:ABA3,13D4:ABA4,13D5:ABA5,13D6:ABA6,13D7:ABA7,13D8:ABA8,13D9:ABA9,13DA:ABAA,13DB:ABAB,13DC:ABAC,13DD:ABAD,13DE:ABAE,13DF:ABAF,13E0:ABB0,13E1:ABB1,13E2:ABB2,13E3:ABB3,13E4:ABB4,13E5:ABB5,13E6:ABB6,13E7:ABB7,13E8:ABB8,13E9:ABB9,13EA:ABBA,13EB:ABBB,13EC:ABBC,13ED:ABBD,13EE:ABBE,13EF:ABBF,13F0:13F8,13F1:13F9,13F2:13FA,13F3:13FB,13F4:13FC,13F5:13FD,1C89:1C8A,1C90:10D0,1C91:10D1,1C92:10D2,1C93:10D3,1C94:10D4,1C95:10D5,1C96:10D6,1C97:10D7,1C98:10D8,1C99:10D9,1C9A:10DA,1C9B:10DB,1C9C:10DC,1C9D:10DD,1C9E:10DE,1C9F:10DF,1CA0:10E0,1CA1:10E1,1CA2:10E2,1CA3:10E3,1CA4:10E4,1CA5:10E5,1CA6:10E6,1CA7:10E7,1CA8:10E8,1CA9:10E9,1CAA:10EA,1CAB:10EB,1CAC:10EC,1CAD:10ED,1CAE:10EE,1CAF:10EF,1CB0:10F0,1CB1:10F1,1CB2:10F2,1CB3:10F3,1CB4:10F4,1CB5:10F5,1CB6:10F6,1CB7:10F7,1CB8:10F8,1CB9:10F9,1CBA:10FA,1CBD:10FD,1CBE:10FE,1CBF:10FF,1E00:1E01,1E02:1E03,1E04:1E05,1E06:1E07,1E08:1E09,1E0A:1E0B,1E0C:1E0D,1E0E:1E0F,1E10:1E11,1E12:1E13,1E14:1E15,1E16:1E17,1E18:1E19,1E1A:1E1B,1E1C:1E1D,1E1E:1E1F,1E20:1E21,1E22:1E23,1E24:1E25,1E26:1E27,1E28:1E29,1E2A:1E2B,1E2C:1E2D,1E2E:1E2F,1E30:1E31,1E32:1E33,1E34:1E35,1E36:1E37,1E38:1E39,1E3A:1E3B,1E3C:1E3D,1E3E:1E3F,1E40:1E41,1E42:1E43,1E44:1E45,1E46:1E47,1E48:1E49,1E4A:1E4B,1E4C:1E4D,1E4E:1E4F,1E50:1E51,1E52:1E53,1E54:1E55,1E56:1E57,1E58:1E59,1E5A:1E5B,1E5C:1E5D,1E5E:1E5F,1E60:1E61,1E62:1E63,1E64:1E65,1E66:1E67,1E68:1E69,1E6A:1E6B,1E6C:1E6D,1E6E:1E6F,1E70:1E71,1E72:1E73,1E74:1E75,1E76:1E77,1E78:1E79,1E7A:1E7B,1E7C:1E7D,1E7E:1E7F,1E80:1E81,1E82:1E83,1E84:1E85,1E86:1E87,1E88:1E89,1E8A:1E8B,1E8C:1E8D,1E8E:1E8F,1E90:1E91,1E92:1E93,1E94:1E95,1E9E:DF,1EA0:1EA1,1EA2:1EA3,1EA4:1EA5,1EA6:1EA7,1EA8:1EA9,1EAA:1EAB,1EAC:1EAD,1EAE:1EAF,1EB0:1EB1,1EB2:1EB3,1EB4:1EB5,1EB6:1EB7,1EB8:1EB9,1EBA:1EBB,1EBC:1EBD,1EBE:1EBF,1EC0:1EC1,1EC2:1EC3,1EC4:1EC5,1EC6:1EC7,1EC8:1EC9,1ECA:1ECB,1ECC:1ECD,1ECE:1ECF,1ED0:1ED1,1ED2:1ED3,1ED4:1ED5,1ED6:1ED7,1ED8:1ED9,1EDA:1EDB,1EDC:1EDD,1EDE:1EDF,1EE0:1EE1,1EE2:1EE3,1EE4:1EE5,1EE6:1EE7,1EE8:1EE9,1EEA:1EEB,1EEC:1EED,1EEE:1EEF,1EF0:1EF1,1EF2:1EF3,1EF4:1EF5,1EF6:1EF7,1EF8:1EF9,1EFA:1EFB,1EFC:1EFD,1EFE:1EFF,1F08:1F00,1F09:1F01,1F0A:1F02,1F0B:1F03,1F0C:1F04,1F0D:1F05,1F0E:1F06,1F0F:1F07,1F18:1F10,1F19:1F11,1F1A:1F12,1F1B:1F13,1F1C:1F14,1F1D:1F15,1F28:1F20,1F29:1F21,1F2A:1F22,1F2B:1F23,1F2C:1F24,1F2D:1F25,1F2E:1F26,1F2F:1F27,1F38:1F30,1F39:1F31,1F3A:1F32,1F3B:1F33,1F3C:1F34,1F3D:1F35,1F3E:1F36,1F3F:1F37,1F48:1F40,1F49:1F41,1F4A:1F42,1F4B:1F43,1F4C:1F44,1F4D:1F45,1F59:1F51,1F5B:1F53,1F5D:1F55,1F5F:1F57,1F68:1F60,1F69:1F61,1F6A:1F62,1F6B:1F63,1F6C:1F64,1F6D:1F65,1F6E:1F66,1F6F:1F67,1F88:1F80,1F89:1F81,1F8A:1F82,1F8B:1F83,1F8C:1F84,1F8D:1F85,1F8E:1F86,1F8F:1F87,1F98:1F90,1F99:1F91,1F9A:1F92,1F9B:1F93,1F9C:1F94,1F9D:1F95,1F9E:1F96,1F9F:1F97,1FA8:1FA0,1FA9:1FA1,1FAA:1FA2,1FAB:1FA3,1FAC:1FA4,1FAD:1FA5,1FAE:1FA6,1FAF:1FA7,1FB8:1FB0,1FB9:1FB1,1FBA:1F70,1FBB:1F71,1FBC:1FB3,1FC8:1F72,1FC9:1F73,1FCA:1F74,1FCB:1F75,1FCC:1FC3,1FD8:1FD0,1FD9:1FD1,1FDA:1F76,1FDB:1F77,1FE8:1FE0,1FE9:1FE1,1FEA:1F7A,1FEB:1F7B,1FEC:1FE5,1FF8:1F78,1FF9:1F79,1FFA:1F7C,1FFB:1F7D,1FFC:1FF3,2126:3C9,212A:6B,212B:E5,2132:214E,2160:2170,2161:2171,2162:2172,2163:2173,2164:2174,2165:2175,2166:2176,2167:2177,2168:2178,2169:2179,216A:217A,216B:217B,216C:217C,216D:217D,216E:217E,216F:217F,2183:2184,24B6:24D0,24B7:24D1,24B8:24D2,24B9:24D3,24BA:24D4,24BB:24D5,24BC:24D6,24BD:24D7,24BE:24D8,24BF:24D9,24C0:24DA,24C1:24DB,24C2:24DC,24C3:24DD,24C4:24DE,24C5:24DF,24C6:24E0,24C7:24E1,24C8:24E2,24C9:24E3,24CA:24E4,24CB:24E5,24CC:24E6,24CD:24E7,24CE:24E8,24CF:24E9,2C00:2C30,2C01:2C31,2C02:2C32,2C03:2C33,2C04:2C34,2C05:2C35,2C06:2C36,2C07:2C37,2C08:2C38,2C09:2C39,2C0A:2C3A,2C0B:2C3B,2C0C:2C3C,2C0D:2C3D,2C0E:2C3E,2C0F:2C3F,2C10:2C40,2C11:2C41,2C12:2C42,2C13:2C43,2C14:2C44,2C15:2C45,2C16:2C46,2C17:2C47,2C18:2C48,2C19:2C49,2C1A:2C4A,2C1B:2C4B,2C1C:2C4C,2C1D:2C4D,2C1E:2C4E,2C1F:2C4F,2C20:2C50,2C21:2C51,2C22:2C52,2C23:2C53,2C24:2C54,2C25:2C55,2C26:2C56,2C27:2C57,2C28:2C58,2C29:2C59,2C2A:2C5A,2C2B:2C5B,2C2C:2C5C,2C2D:2C5D,2C2E:2C5E,2C2F:2C5F,2C60:2C61,2C62:26B,2C63:1D7D,2C64:27D,2C67:2C68,2C69:2C6A,2C6B:2C6C,2C6D:251,2C6E:271,2C6F:250,2C70:252,2C72:2C73,2C75:2C76,2C7E:23F,2C7F:240,2C80:2C81,2C82:2C83,2C84:2C85,2C86:2C87,2C88:2C89,2C8A:2C8B,2C8C:2C8D,2C8E:2C8F,2C90:2C91,2C92:2C93,2C94:2C95,2C96:2C97,2C98:2C99,2C9A:2C9B,2C9C:2C9D,2C9E:2C9F,2CA0:2CA1,2CA2:2CA3,2CA4:2CA5,2CA6:2CA7,2CA8:2CA9,2CAA:2CAB,2CAC:2CAD,2CAE:2CAF,2CB0:2CB1,2CB2:2CB3,2CB4:2CB5,2CB6:2CB7,2CB8:2CB9,2CBA:2CBB,2CBC:2CBD,2CBE:2CBF,2CC0:2CC1,2CC2:2CC3,2CC4:2CC5,2CC6:2CC7,2CC8:2CC9,2CCA:2CCB,2CCC:2CCD,2CCE:2CCF,2CD0:2CD1,2CD2:2CD3,2CD4:2CD5,2CD6:2CD7,2CD8:2CD9,2CDA:2CDB,2CDC:2CDD,2CDE:2CDF,2CE0:2CE1,2CE2:2CE3,2CEB:2CEC,2CED:2CEE,2CF2:2CF3,A640:A641,A642:A643,A644:A645,A646:A647,A648:A649,A64A:A64B,A64C:A64D,A64E:A64F,A650:A651,A652:A653,A654:A655,A656:A657,A658:A659,A65A:A65B,A65C:A65D,A65E:A65F,A660:A661,A662:A663,A664:A665,A666:A667,A668:A669,A66A:A66B,A66C:A66D,A680:A681,A682:A683,A684:A685,A686:A687,A688:A689,A68A:A68B,A68C:A68D,A68E:A68F,A690:A691,A692:A693,A694:A695,A696:A697,A698:A699,A69A:A69B,A722:A723,A724:A725,A726:A727,A728:A729,A72A:A72B,A72C:A72D,A72E:A72F,A732:A733,A734:A735,A736:A737,A738:A739,A73A:A73B,A73C:A73D,A73E:A73F,A740:A741,A742:A743,A744:A745,A746:A747,A748:A749,A74A:A74B,A74C:A74D,A74E:A74F,A750:A751,A752:A753,A754:A755,A756:A757,A758:A759,A75A:A75B,A75C:A75D,A75E:A75F,A760:A761,A762:A763,A764:A765,A766:A767,A768:A769,A76A:A76B,A76C:A76D,A76E:A76F,A779:A77A,A77B:A77C,A77D:1D79,A77E:A77F,A780:A781,A782:A783,A784:A785,A786:A787,A78B:A78C,A78D:265,A790:A791,A792:A793,A796:A797,A798:A799,A79A:A79B,A79C:A79D,A79E:A79F,A7A0:A7A1,A7A2:A7A3,A7A4:A7A5,A7A6:A7A7,A7A8:A7A9,A7AA:266,A7AB:25C,A7AC:261,A7AD:26C,A7AE:26A,A7B0:29E,A7B1:287,A7B2:29D,A7B3:AB53,A7B4:A7B5,A7B6:A7B7,A7B8:A7B9,A7BA:A7BB,A7BC:A7BD,A7BE:A7BF,A7C0:A7C1,A7C2:A7C3,A7C4:A794,A7C5:282,A7C6:1D8E,A7C7:A7C8,A7C9:A7CA,A7CB:264,A7CC:A7CD,A7D0:A7D1,A7D6:A7D7,A7D8:A7D9,A7DA:A7DB,A7DC:19B,A7F5:A7F6,FF21:FF41,FF22:FF42,FF23:FF43,FF24:FF44,FF25:FF45,FF26:FF46,FF27:FF47,FF28:FF48,FF29:FF49,FF2A:FF4A,FF2B:FF4B,FF2C:FF4C,FF2D:FF4D,FF2E:FF4E,FF2F:FF4F,FF30:FF50,FF31:FF51,FF32:FF52,FF33:FF53,FF34:FF54,FF35:FF55,FF36:FF56,FF37:FF57,FF38:FF58,FF39:FF59,FF3A:FF5A,10400:10428,10401:10429,10402:1042A,10403:1042B,10404:1042C,10405:1042D,10406:1042E,10407:1042F,10408:10430,10409:10431,1040A:10432,1040B:10433,1040C:10434,1040D:10435,1040E:10436,1040F:10437,10410:10438,10411:10439,10412:1043A,10413:1043B,10414:1043C,10415:1043D,10416:1043E,10417:1043F,10418:10440,10419:10441,1041A:10442,1041B:10443,1041C:10444,1041D:10445,1041E:10446,1041F:10447,10420:10448,10421:10449,10422:1044A,10423:1044B,10424:1044C,10425:1044D,10426:1044E,10427:1044F,104B0:104D8,104B1:104D9,104B2:104DA,104B3:104DB,104B4:104DC,104B5:104DD,104B6:104DE,104B7:104DF,104B8:104E0,104B9:104E1,104BA:104E2,104BB:104E3,104BC:104E4,104BD:104E5,104BE:104E6,104BF:104E7,104C0:104E8,104C1:104E9,104C2:104EA,104C3:104EB,104C4:104EC,104C5:104ED,104C6:104EE,104C7:104EF,104C8:104F0,104C9:104F1,104CA:104F2,104CB:104F3,104CC:104F4,104CD:104F5,104CE:104F6,104CF:104F7,104D0:104F8,104D1:104F9,104D2:104FA,104D3:104FB,10570:10597,10571:10598,10572:10599,10573:1059A,10574:1059B,10575:1059C,10576:1059D,10577:1059E,10578:1059F,10579:105A0,1057A:105A1,1057C:105A3,1057D:105A4,1057E:105A5,1057F:105A6,10580:105A7,10581:105A8,10582:105A9,10583:105AA,10584:105AB,10585:105AC,10586:105AD,10587:105AE,10588:105AF,10589:105B0,1058A:105B1,1058C:105B3,1058D:105B4,1058E:105B5,1058F:105B6,10590:105B7,10591:105B8,10592:105B9,10594:105BB,10595:105BC,10C80:10CC0,10C81:10CC1,10C82:10CC2,10C83:10CC3,10C84:10CC4,10C85:10CC5,10C86:10CC6,10C87:10CC7,10C88:10CC8,10C89:10CC9,10C8A:10CCA,10C8B:10CCB,10C8C:10CCC,10C8D:10CCD,10C8E:10CCE,10C8F:10CCF,10C90:10CD0,10C91:10CD1,10C92:10CD2,10C93:10CD3,10C94:10CD4,10C95:10CD5,10C96:10CD6,10C97:10CD7,10C98:10CD8,10C99:10CD9,10C9A:10CDA,10C9B:10CDB,10C9C:10CDC,10C9D:10CDD,10C9E:10CDE,10C9F:10CDF,10CA0:10CE0,10CA1:10CE1,10CA2:10CE2,10CA3:10CE3,10CA4:10CE4,10CA5:10CE5,10CA6:10CE6,10CA7:10CE7,10CA8:10CE8,10CA9:10CE9,10CAA:10CEA,10CAB:10CEB,10CAC:10CEC,10CAD:10CED,10CAE:10CEE,10CAF:10CEF,10CB0:10CF0,10CB1:10CF1,10CB2:10CF2,10D50:10D70,10D51:10D71,10D52:10D72,10D53:10D73,10D54:10D74,10D55:10D75,10D56:10D76,10D57:10D77,10D58:10D78,10D59:10D79,10D5A:10D7A,10D5B:10D7B,10D5C:10D7C,10D5D:10D7D,10D5E:10D7E,10D5F:10D7F,10D60:10D80,10D61:10D81,10D62:10D82,10D63:10D83,10D64:10D84,10D65:10D85,118A0:118C0,118A1:118C1,118A2:118C2,118A3:118C3,118A4:118C4,118A5:118C5,118A6:118C6,118A7:118C7,118A8:118C8,118A9:118C9,118AA:118CA,118AB:118CB,118AC:118CC,118AD:118CD,118AE:118CE,118AF:118CF,118B0:118D0,118B1:118D1,118B2:118D2,118B3:118D3,118B4:118D4,118B5:118D5,118B6:118D6,118B7:118D7,118B8:118D8,118B9:118D9,118BA:118DA,118BB:118DB,118BC:118DC,118BD:118DD,118BE:118DE,118BF:118DF,16E40:16E60,16E41:16E61,16E42:16E62,16E43:16E63,16E44:16E64,16E45:16E65,16E46:16E66,16E47:16E67,16E48:16E68,16E49:16E69,16E4A:16E6A,16E4B:16E6B,16E4C:16E6C,16E4D:16E6D,16E4E:16E6E,16E4F:16E6F,16E50:16E70,16E51:16E71,16E52:16E72,16E53:16E73,16E54:16E74,16E55:16E75,16E56:16E76,16E57:16E77,16E58:16E78,16E59:16E79,16E5A:16E7A,16E5B:16E7B,16E5C:16E7C,16E5D:16E7D,16E5E:16E7E,16E5F:16E7F,1E900:1E922,1E901:1E923,1E902:1E924,1E903:1E925,1E904:1E926,1E905:1E927,1E906:1E928,1E907:1E929,1E908:1E92A,1E909:1E92B,1E90A:1E92C,1E90B:1E92D,1E90C:1E92E,1E90D:1E92F,1E90E:1E930,1E90F:1E931,1E910:1E932,1E911:1E933,1E912:1E934,1E913:1E935,1E914:1E936,1E915:1E937,1E916:1E938,1E917:1E939,1E918:1E93A,1E919:1E93B,1E91A:1E93C,1E91B:1E93D,1E91C:1E93E,1E91D:1E93F,1E91E:1E940,1E91F:1E941,1E920:1E942,1E921:1E943 diff --git a/crates/unicode/tests/data/cpython3.14_predicates.txt b/crates/unicode/tests/data/cpython3.14_predicates.txt new file mode 100644 index 00000000000..22f9d8d6105 --- /dev/null +++ b/crates/unicode/tests/data/cpython3.14_predicates.txt @@ -0,0 +1,13 @@ +# unidata_version 16.0.0 +isalpha 41:5A,61:7A,AA:AA,B5:B5,BA:BA,C0:D6,D8:F6,F8:2C1,2C6:2D1,2E0:2E4,2EC:2EC,2EE:2EE,370:374,376:377,37A:37D,37F:37F,386:386,388:38A,38C:38C,38E:3A1,3A3:3F5,3F7:481,48A:52F,531:556,559:559,560:588,5D0:5EA,5EF:5F2,620:64A,66E:66F,671:6D3,6D5:6D5,6E5:6E6,6EE:6EF,6FA:6FC,6FF:6FF,710:710,712:72F,74D:7A5,7B1:7B1,7CA:7EA,7F4:7F5,7FA:7FA,800:815,81A:81A,824:824,828:828,840:858,860:86A,870:887,889:88E,8A0:8C9,904:939,93D:93D,950:950,958:961,971:980,985:98C,98F:990,993:9A8,9AA:9B0,9B2:9B2,9B6:9B9,9BD:9BD,9CE:9CE,9DC:9DD,9DF:9E1,9F0:9F1,9FC:9FC,A05:A0A,A0F:A10,A13:A28,A2A:A30,A32:A33,A35:A36,A38:A39,A59:A5C,A5E:A5E,A72:A74,A85:A8D,A8F:A91,A93:AA8,AAA:AB0,AB2:AB3,AB5:AB9,ABD:ABD,AD0:AD0,AE0:AE1,AF9:AF9,B05:B0C,B0F:B10,B13:B28,B2A:B30,B32:B33,B35:B39,B3D:B3D,B5C:B5D,B5F:B61,B71:B71,B83:B83,B85:B8A,B8E:B90,B92:B95,B99:B9A,B9C:B9C,B9E:B9F,BA3:BA4,BA8:BAA,BAE:BB9,BD0:BD0,C05:C0C,C0E:C10,C12:C28,C2A:C39,C3D:C3D,C58:C5A,C5D:C5D,C60:C61,C80:C80,C85:C8C,C8E:C90,C92:CA8,CAA:CB3,CB5:CB9,CBD:CBD,CDD:CDE,CE0:CE1,CF1:CF2,D04:D0C,D0E:D10,D12:D3A,D3D:D3D,D4E:D4E,D54:D56,D5F:D61,D7A:D7F,D85:D96,D9A:DB1,DB3:DBB,DBD:DBD,DC0:DC6,E01:E30,E32:E33,E40:E46,E81:E82,E84:E84,E86:E8A,E8C:EA3,EA5:EA5,EA7:EB0,EB2:EB3,EBD:EBD,EC0:EC4,EC6:EC6,EDC:EDF,F00:F00,F40:F47,F49:F6C,F88:F8C,1000:102A,103F:103F,1050:1055,105A:105D,1061:1061,1065:1066,106E:1070,1075:1081,108E:108E,10A0:10C5,10C7:10C7,10CD:10CD,10D0:10FA,10FC:1248,124A:124D,1250:1256,1258:1258,125A:125D,1260:1288,128A:128D,1290:12B0,12B2:12B5,12B8:12BE,12C0:12C0,12C2:12C5,12C8:12D6,12D8:1310,1312:1315,1318:135A,1380:138F,13A0:13F5,13F8:13FD,1401:166C,166F:167F,1681:169A,16A0:16EA,16F1:16F8,1700:1711,171F:1731,1740:1751,1760:176C,176E:1770,1780:17B3,17D7:17D7,17DC:17DC,1820:1878,1880:1884,1887:18A8,18AA:18AA,18B0:18F5,1900:191E,1950:196D,1970:1974,1980:19AB,19B0:19C9,1A00:1A16,1A20:1A54,1AA7:1AA7,1B05:1B33,1B45:1B4C,1B83:1BA0,1BAE:1BAF,1BBA:1BE5,1C00:1C23,1C4D:1C4F,1C5A:1C7D,1C80:1C8A,1C90:1CBA,1CBD:1CBF,1CE9:1CEC,1CEE:1CF3,1CF5:1CF6,1CFA:1CFA,1D00:1DBF,1E00:1F15,1F18:1F1D,1F20:1F45,1F48:1F4D,1F50:1F57,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F7D,1F80:1FB4,1FB6:1FBC,1FBE:1FBE,1FC2:1FC4,1FC6:1FCC,1FD0:1FD3,1FD6:1FDB,1FE0:1FEC,1FF2:1FF4,1FF6:1FFC,2071:2071,207F:207F,2090:209C,2102:2102,2107:2107,210A:2113,2115:2115,2119:211D,2124:2124,2126:2126,2128:2128,212A:212D,212F:2139,213C:213F,2145:2149,214E:214E,2183:2184,2C00:2CE4,2CEB:2CEE,2CF2:2CF3,2D00:2D25,2D27:2D27,2D2D:2D2D,2D30:2D67,2D6F:2D6F,2D80:2D96,2DA0:2DA6,2DA8:2DAE,2DB0:2DB6,2DB8:2DBE,2DC0:2DC6,2DC8:2DCE,2DD0:2DD6,2DD8:2DDE,2E2F:2E2F,3005:3006,3031:3035,303B:303C,3041:3096,309D:309F,30A1:30FA,30FC:30FF,3105:312F,3131:318E,31A0:31BF,31F0:31FF,3400:4DBF,4E00:A48C,A4D0:A4FD,A500:A60C,A610:A61F,A62A:A62B,A640:A66E,A67F:A69D,A6A0:A6E5,A717:A71F,A722:A788,A78B:A7CD,A7D0:A7D1,A7D3:A7D3,A7D5:A7DC,A7F2:A801,A803:A805,A807:A80A,A80C:A822,A840:A873,A882:A8B3,A8F2:A8F7,A8FB:A8FB,A8FD:A8FE,A90A:A925,A930:A946,A960:A97C,A984:A9B2,A9CF:A9CF,A9E0:A9E4,A9E6:A9EF,A9FA:A9FE,AA00:AA28,AA40:AA42,AA44:AA4B,AA60:AA76,AA7A:AA7A,AA7E:AAAF,AAB1:AAB1,AAB5:AAB6,AAB9:AABD,AAC0:AAC0,AAC2:AAC2,AADB:AADD,AAE0:AAEA,AAF2:AAF4,AB01:AB06,AB09:AB0E,AB11:AB16,AB20:AB26,AB28:AB2E,AB30:AB5A,AB5C:AB69,AB70:ABE2,AC00:D7A3,D7B0:D7C6,D7CB:D7FB,F900:FA6D,FA70:FAD9,FB00:FB06,FB13:FB17,FB1D:FB1D,FB1F:FB28,FB2A:FB36,FB38:FB3C,FB3E:FB3E,FB40:FB41,FB43:FB44,FB46:FBB1,FBD3:FD3D,FD50:FD8F,FD92:FDC7,FDF0:FDFB,FE70:FE74,FE76:FEFC,FF21:FF3A,FF41:FF5A,FF66:FFBE,FFC2:FFC7,FFCA:FFCF,FFD2:FFD7,FFDA:FFDC,10000:1000B,1000D:10026,10028:1003A,1003C:1003D,1003F:1004D,10050:1005D,10080:100FA,10280:1029C,102A0:102D0,10300:1031F,1032D:10340,10342:10349,10350:10375,10380:1039D,103A0:103C3,103C8:103CF,10400:1049D,104B0:104D3,104D8:104FB,10500:10527,10530:10563,10570:1057A,1057C:1058A,1058C:10592,10594:10595,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,105C0:105F3,10600:10736,10740:10755,10760:10767,10780:10785,10787:107B0,107B2:107BA,10800:10805,10808:10808,1080A:10835,10837:10838,1083C:1083C,1083F:10855,10860:10876,10880:1089E,108E0:108F2,108F4:108F5,10900:10915,10920:10939,10980:109B7,109BE:109BF,10A00:10A00,10A10:10A13,10A15:10A17,10A19:10A35,10A60:10A7C,10A80:10A9C,10AC0:10AC7,10AC9:10AE4,10B00:10B35,10B40:10B55,10B60:10B72,10B80:10B91,10C00:10C48,10C80:10CB2,10CC0:10CF2,10D00:10D23,10D4A:10D65,10D6F:10D85,10E80:10EA9,10EB0:10EB1,10EC2:10EC4,10F00:10F1C,10F27:10F27,10F30:10F45,10F70:10F81,10FB0:10FC4,10FE0:10FF6,11003:11037,11071:11072,11075:11075,11083:110AF,110D0:110E8,11103:11126,11144:11144,11147:11147,11150:11172,11176:11176,11183:111B2,111C1:111C4,111DA:111DA,111DC:111DC,11200:11211,11213:1122B,1123F:11240,11280:11286,11288:11288,1128A:1128D,1128F:1129D,1129F:112A8,112B0:112DE,11305:1130C,1130F:11310,11313:11328,1132A:11330,11332:11333,11335:11339,1133D:1133D,11350:11350,1135D:11361,11380:11389,1138B:1138B,1138E:1138E,11390:113B5,113B7:113B7,113D1:113D1,113D3:113D3,11400:11434,11447:1144A,1145F:11461,11480:114AF,114C4:114C5,114C7:114C7,11580:115AE,115D8:115DB,11600:1162F,11644:11644,11680:116AA,116B8:116B8,11700:1171A,11740:11746,11800:1182B,118A0:118DF,118FF:11906,11909:11909,1190C:11913,11915:11916,11918:1192F,1193F:1193F,11941:11941,119A0:119A7,119AA:119D0,119E1:119E1,119E3:119E3,11A00:11A00,11A0B:11A32,11A3A:11A3A,11A50:11A50,11A5C:11A89,11A9D:11A9D,11AB0:11AF8,11BC0:11BE0,11C00:11C08,11C0A:11C2E,11C40:11C40,11C72:11C8F,11D00:11D06,11D08:11D09,11D0B:11D30,11D46:11D46,11D60:11D65,11D67:11D68,11D6A:11D89,11D98:11D98,11EE0:11EF2,11F02:11F02,11F04:11F10,11F12:11F33,11FB0:11FB0,12000:12399,12480:12543,12F90:12FF0,13000:1342F,13441:13446,13460:143FA,14400:14646,16100:1611D,16800:16A38,16A40:16A5E,16A70:16ABE,16AD0:16AED,16B00:16B2F,16B40:16B43,16B63:16B77,16B7D:16B8F,16D40:16D6C,16E40:16E7F,16F00:16F4A,16F50:16F50,16F93:16F9F,16FE0:16FE1,16FE3:16FE3,17000:187F7,18800:18CD5,18CFF:18D08,1AFF0:1AFF3,1AFF5:1AFFB,1AFFD:1AFFE,1B000:1B122,1B132:1B132,1B150:1B152,1B155:1B155,1B164:1B167,1B170:1B2FB,1BC00:1BC6A,1BC70:1BC7C,1BC80:1BC88,1BC90:1BC99,1D400:1D454,1D456:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D51E:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D552:1D6A5,1D6A8:1D6C0,1D6C2:1D6DA,1D6DC:1D6FA,1D6FC:1D714,1D716:1D734,1D736:1D74E,1D750:1D76E,1D770:1D788,1D78A:1D7A8,1D7AA:1D7C2,1D7C4:1D7CB,1DF00:1DF1E,1DF25:1DF2A,1E030:1E06D,1E100:1E12C,1E137:1E13D,1E14E:1E14E,1E290:1E2AD,1E2C0:1E2EB,1E4D0:1E4EB,1E5D0:1E5ED,1E5F0:1E5F0,1E7E0:1E7E6,1E7E8:1E7EB,1E7ED:1E7EE,1E7F0:1E7FE,1E800:1E8C4,1E900:1E943,1E94B:1E94B,1EE00:1EE03,1EE05:1EE1F,1EE21:1EE22,1EE24:1EE24,1EE27:1EE27,1EE29:1EE32,1EE34:1EE37,1EE39:1EE39,1EE3B:1EE3B,1EE42:1EE42,1EE47:1EE47,1EE49:1EE49,1EE4B:1EE4B,1EE4D:1EE4F,1EE51:1EE52,1EE54:1EE54,1EE57:1EE57,1EE59:1EE59,1EE5B:1EE5B,1EE5D:1EE5D,1EE5F:1EE5F,1EE61:1EE62,1EE64:1EE64,1EE67:1EE6A,1EE6C:1EE72,1EE74:1EE77,1EE79:1EE7C,1EE7E:1EE7E,1EE80:1EE89,1EE8B:1EE9B,1EEA1:1EEA3,1EEA5:1EEA9,1EEAB:1EEBB,20000:2A6DF,2A700:2B739,2B740:2B81D,2B820:2CEA1,2CEB0:2EBE0,2EBF0:2EE5D,2F800:2FA1D,30000:3134A,31350:323AF +isalnum 30:39,41:5A,61:7A,AA:AA,B2:B3,B5:B5,B9:BA,BC:BE,C0:D6,D8:F6,F8:2C1,2C6:2D1,2E0:2E4,2EC:2EC,2EE:2EE,370:374,376:377,37A:37D,37F:37F,386:386,388:38A,38C:38C,38E:3A1,3A3:3F5,3F7:481,48A:52F,531:556,559:559,560:588,5D0:5EA,5EF:5F2,620:64A,660:669,66E:66F,671:6D3,6D5:6D5,6E5:6E6,6EE:6FC,6FF:6FF,710:710,712:72F,74D:7A5,7B1:7B1,7C0:7EA,7F4:7F5,7FA:7FA,800:815,81A:81A,824:824,828:828,840:858,860:86A,870:887,889:88E,8A0:8C9,904:939,93D:93D,950:950,958:961,966:96F,971:980,985:98C,98F:990,993:9A8,9AA:9B0,9B2:9B2,9B6:9B9,9BD:9BD,9CE:9CE,9DC:9DD,9DF:9E1,9E6:9F1,9F4:9F9,9FC:9FC,A05:A0A,A0F:A10,A13:A28,A2A:A30,A32:A33,A35:A36,A38:A39,A59:A5C,A5E:A5E,A66:A6F,A72:A74,A85:A8D,A8F:A91,A93:AA8,AAA:AB0,AB2:AB3,AB5:AB9,ABD:ABD,AD0:AD0,AE0:AE1,AE6:AEF,AF9:AF9,B05:B0C,B0F:B10,B13:B28,B2A:B30,B32:B33,B35:B39,B3D:B3D,B5C:B5D,B5F:B61,B66:B6F,B71:B77,B83:B83,B85:B8A,B8E:B90,B92:B95,B99:B9A,B9C:B9C,B9E:B9F,BA3:BA4,BA8:BAA,BAE:BB9,BD0:BD0,BE6:BF2,C05:C0C,C0E:C10,C12:C28,C2A:C39,C3D:C3D,C58:C5A,C5D:C5D,C60:C61,C66:C6F,C78:C7E,C80:C80,C85:C8C,C8E:C90,C92:CA8,CAA:CB3,CB5:CB9,CBD:CBD,CDD:CDE,CE0:CE1,CE6:CEF,CF1:CF2,D04:D0C,D0E:D10,D12:D3A,D3D:D3D,D4E:D4E,D54:D56,D58:D61,D66:D78,D7A:D7F,D85:D96,D9A:DB1,DB3:DBB,DBD:DBD,DC0:DC6,DE6:DEF,E01:E30,E32:E33,E40:E46,E50:E59,E81:E82,E84:E84,E86:E8A,E8C:EA3,EA5:EA5,EA7:EB0,EB2:EB3,EBD:EBD,EC0:EC4,EC6:EC6,ED0:ED9,EDC:EDF,F00:F00,F20:F33,F40:F47,F49:F6C,F88:F8C,1000:102A,103F:1049,1050:1055,105A:105D,1061:1061,1065:1066,106E:1070,1075:1081,108E:108E,1090:1099,10A0:10C5,10C7:10C7,10CD:10CD,10D0:10FA,10FC:1248,124A:124D,1250:1256,1258:1258,125A:125D,1260:1288,128A:128D,1290:12B0,12B2:12B5,12B8:12BE,12C0:12C0,12C2:12C5,12C8:12D6,12D8:1310,1312:1315,1318:135A,1369:137C,1380:138F,13A0:13F5,13F8:13FD,1401:166C,166F:167F,1681:169A,16A0:16EA,16EE:16F8,1700:1711,171F:1731,1740:1751,1760:176C,176E:1770,1780:17B3,17D7:17D7,17DC:17DC,17E0:17E9,17F0:17F9,1810:1819,1820:1878,1880:1884,1887:18A8,18AA:18AA,18B0:18F5,1900:191E,1946:196D,1970:1974,1980:19AB,19B0:19C9,19D0:19DA,1A00:1A16,1A20:1A54,1A80:1A89,1A90:1A99,1AA7:1AA7,1B05:1B33,1B45:1B4C,1B50:1B59,1B83:1BA0,1BAE:1BE5,1C00:1C23,1C40:1C49,1C4D:1C7D,1C80:1C8A,1C90:1CBA,1CBD:1CBF,1CE9:1CEC,1CEE:1CF3,1CF5:1CF6,1CFA:1CFA,1D00:1DBF,1E00:1F15,1F18:1F1D,1F20:1F45,1F48:1F4D,1F50:1F57,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F7D,1F80:1FB4,1FB6:1FBC,1FBE:1FBE,1FC2:1FC4,1FC6:1FCC,1FD0:1FD3,1FD6:1FDB,1FE0:1FEC,1FF2:1FF4,1FF6:1FFC,2070:2071,2074:2079,207F:2089,2090:209C,2102:2102,2107:2107,210A:2113,2115:2115,2119:211D,2124:2124,2126:2126,2128:2128,212A:212D,212F:2139,213C:213F,2145:2149,214E:214E,2150:2189,2460:249B,24EA:24FF,2776:2793,2C00:2CE4,2CEB:2CEE,2CF2:2CF3,2CFD:2CFD,2D00:2D25,2D27:2D27,2D2D:2D2D,2D30:2D67,2D6F:2D6F,2D80:2D96,2DA0:2DA6,2DA8:2DAE,2DB0:2DB6,2DB8:2DBE,2DC0:2DC6,2DC8:2DCE,2DD0:2DD6,2DD8:2DDE,2E2F:2E2F,3005:3007,3021:3029,3031:3035,3038:303C,3041:3096,309D:309F,30A1:30FA,30FC:30FF,3105:312F,3131:318E,3192:3195,31A0:31BF,31F0:31FF,3220:3229,3248:324F,3251:325F,3280:3289,32B1:32BF,3400:4DBF,4E00:A48C,A4D0:A4FD,A500:A60C,A610:A62B,A640:A66E,A67F:A69D,A6A0:A6EF,A717:A71F,A722:A788,A78B:A7CD,A7D0:A7D1,A7D3:A7D3,A7D5:A7DC,A7F2:A801,A803:A805,A807:A80A,A80C:A822,A830:A835,A840:A873,A882:A8B3,A8D0:A8D9,A8F2:A8F7,A8FB:A8FB,A8FD:A8FE,A900:A925,A930:A946,A960:A97C,A984:A9B2,A9CF:A9D9,A9E0:A9E4,A9E6:A9FE,AA00:AA28,AA40:AA42,AA44:AA4B,AA50:AA59,AA60:AA76,AA7A:AA7A,AA7E:AAAF,AAB1:AAB1,AAB5:AAB6,AAB9:AABD,AAC0:AAC0,AAC2:AAC2,AADB:AADD,AAE0:AAEA,AAF2:AAF4,AB01:AB06,AB09:AB0E,AB11:AB16,AB20:AB26,AB28:AB2E,AB30:AB5A,AB5C:AB69,AB70:ABE2,ABF0:ABF9,AC00:D7A3,D7B0:D7C6,D7CB:D7FB,F900:FA6D,FA70:FAD9,FB00:FB06,FB13:FB17,FB1D:FB1D,FB1F:FB28,FB2A:FB36,FB38:FB3C,FB3E:FB3E,FB40:FB41,FB43:FB44,FB46:FBB1,FBD3:FD3D,FD50:FD8F,FD92:FDC7,FDF0:FDFB,FE70:FE74,FE76:FEFC,FF10:FF19,FF21:FF3A,FF41:FF5A,FF66:FFBE,FFC2:FFC7,FFCA:FFCF,FFD2:FFD7,FFDA:FFDC,10000:1000B,1000D:10026,10028:1003A,1003C:1003D,1003F:1004D,10050:1005D,10080:100FA,10107:10133,10140:10178,1018A:1018B,10280:1029C,102A0:102D0,102E1:102FB,10300:10323,1032D:1034A,10350:10375,10380:1039D,103A0:103C3,103C8:103CF,103D1:103D5,10400:1049D,104A0:104A9,104B0:104D3,104D8:104FB,10500:10527,10530:10563,10570:1057A,1057C:1058A,1058C:10592,10594:10595,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,105C0:105F3,10600:10736,10740:10755,10760:10767,10780:10785,10787:107B0,107B2:107BA,10800:10805,10808:10808,1080A:10835,10837:10838,1083C:1083C,1083F:10855,10858:10876,10879:1089E,108A7:108AF,108E0:108F2,108F4:108F5,108FB:1091B,10920:10939,10980:109B7,109BC:109CF,109D2:10A00,10A10:10A13,10A15:10A17,10A19:10A35,10A40:10A48,10A60:10A7E,10A80:10A9F,10AC0:10AC7,10AC9:10AE4,10AEB:10AEF,10B00:10B35,10B40:10B55,10B58:10B72,10B78:10B91,10BA9:10BAF,10C00:10C48,10C80:10CB2,10CC0:10CF2,10CFA:10D23,10D30:10D39,10D40:10D65,10D6F:10D85,10E60:10E7E,10E80:10EA9,10EB0:10EB1,10EC2:10EC4,10F00:10F27,10F30:10F45,10F51:10F54,10F70:10F81,10FB0:10FCB,10FE0:10FF6,11003:11037,11052:1106F,11071:11072,11075:11075,11083:110AF,110D0:110E8,110F0:110F9,11103:11126,11136:1113F,11144:11144,11147:11147,11150:11172,11176:11176,11183:111B2,111C1:111C4,111D0:111DA,111DC:111DC,111E1:111F4,11200:11211,11213:1122B,1123F:11240,11280:11286,11288:11288,1128A:1128D,1128F:1129D,1129F:112A8,112B0:112DE,112F0:112F9,11305:1130C,1130F:11310,11313:11328,1132A:11330,11332:11333,11335:11339,1133D:1133D,11350:11350,1135D:11361,11380:11389,1138B:1138B,1138E:1138E,11390:113B5,113B7:113B7,113D1:113D1,113D3:113D3,11400:11434,11447:1144A,11450:11459,1145F:11461,11480:114AF,114C4:114C5,114C7:114C7,114D0:114D9,11580:115AE,115D8:115DB,11600:1162F,11644:11644,11650:11659,11680:116AA,116B8:116B8,116C0:116C9,116D0:116E3,11700:1171A,11730:1173B,11740:11746,11800:1182B,118A0:118F2,118FF:11906,11909:11909,1190C:11913,11915:11916,11918:1192F,1193F:1193F,11941:11941,11950:11959,119A0:119A7,119AA:119D0,119E1:119E1,119E3:119E3,11A00:11A00,11A0B:11A32,11A3A:11A3A,11A50:11A50,11A5C:11A89,11A9D:11A9D,11AB0:11AF8,11BC0:11BE0,11BF0:11BF9,11C00:11C08,11C0A:11C2E,11C40:11C40,11C50:11C6C,11C72:11C8F,11D00:11D06,11D08:11D09,11D0B:11D30,11D46:11D46,11D50:11D59,11D60:11D65,11D67:11D68,11D6A:11D89,11D98:11D98,11DA0:11DA9,11EE0:11EF2,11F02:11F02,11F04:11F10,11F12:11F33,11F50:11F59,11FB0:11FB0,11FC0:11FD4,12000:12399,12400:1246E,12480:12543,12F90:12FF0,13000:1342F,13441:13446,13460:143FA,14400:14646,16100:1611D,16130:16139,16800:16A38,16A40:16A5E,16A60:16A69,16A70:16ABE,16AC0:16AC9,16AD0:16AED,16B00:16B2F,16B40:16B43,16B50:16B59,16B5B:16B61,16B63:16B77,16B7D:16B8F,16D40:16D6C,16D70:16D79,16E40:16E96,16F00:16F4A,16F50:16F50,16F93:16F9F,16FE0:16FE1,16FE3:16FE3,17000:187F7,18800:18CD5,18CFF:18D08,1AFF0:1AFF3,1AFF5:1AFFB,1AFFD:1AFFE,1B000:1B122,1B132:1B132,1B150:1B152,1B155:1B155,1B164:1B167,1B170:1B2FB,1BC00:1BC6A,1BC70:1BC7C,1BC80:1BC88,1BC90:1BC99,1CCF0:1CCF9,1D2C0:1D2D3,1D2E0:1D2F3,1D360:1D378,1D400:1D454,1D456:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D51E:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D552:1D6A5,1D6A8:1D6C0,1D6C2:1D6DA,1D6DC:1D6FA,1D6FC:1D714,1D716:1D734,1D736:1D74E,1D750:1D76E,1D770:1D788,1D78A:1D7A8,1D7AA:1D7C2,1D7C4:1D7CB,1D7CE:1D7FF,1DF00:1DF1E,1DF25:1DF2A,1E030:1E06D,1E100:1E12C,1E137:1E13D,1E140:1E149,1E14E:1E14E,1E290:1E2AD,1E2C0:1E2EB,1E2F0:1E2F9,1E4D0:1E4EB,1E4F0:1E4F9,1E5D0:1E5ED,1E5F0:1E5FA,1E7E0:1E7E6,1E7E8:1E7EB,1E7ED:1E7EE,1E7F0:1E7FE,1E800:1E8C4,1E8C7:1E8CF,1E900:1E943,1E94B:1E94B,1E950:1E959,1EC71:1ECAB,1ECAD:1ECAF,1ECB1:1ECB4,1ED01:1ED2D,1ED2F:1ED3D,1EE00:1EE03,1EE05:1EE1F,1EE21:1EE22,1EE24:1EE24,1EE27:1EE27,1EE29:1EE32,1EE34:1EE37,1EE39:1EE39,1EE3B:1EE3B,1EE42:1EE42,1EE47:1EE47,1EE49:1EE49,1EE4B:1EE4B,1EE4D:1EE4F,1EE51:1EE52,1EE54:1EE54,1EE57:1EE57,1EE59:1EE59,1EE5B:1EE5B,1EE5D:1EE5D,1EE5F:1EE5F,1EE61:1EE62,1EE64:1EE64,1EE67:1EE6A,1EE6C:1EE72,1EE74:1EE77,1EE79:1EE7C,1EE7E:1EE7E,1EE80:1EE89,1EE8B:1EE9B,1EEA1:1EEA3,1EEA5:1EEA9,1EEAB:1EEBB,1F100:1F10C,1FBF0:1FBF9,20000:2A6DF,2A700:2B739,2B740:2B81D,2B820:2CEA1,2CEB0:2EBE0,2EBF0:2EE5D,2F800:2FA1D,30000:3134A,31350:323AF +isdecimal 30:39,660:669,6F0:6F9,7C0:7C9,966:96F,9E6:9EF,A66:A6F,AE6:AEF,B66:B6F,BE6:BEF,C66:C6F,CE6:CEF,D66:D6F,DE6:DEF,E50:E59,ED0:ED9,F20:F29,1040:1049,1090:1099,17E0:17E9,1810:1819,1946:194F,19D0:19D9,1A80:1A89,1A90:1A99,1B50:1B59,1BB0:1BB9,1C40:1C49,1C50:1C59,A620:A629,A8D0:A8D9,A900:A909,A9D0:A9D9,A9F0:A9F9,AA50:AA59,ABF0:ABF9,FF10:FF19,104A0:104A9,10D30:10D39,10D40:10D49,11066:1106F,110F0:110F9,11136:1113F,111D0:111D9,112F0:112F9,11450:11459,114D0:114D9,11650:11659,116C0:116C9,116D0:116E3,11730:11739,118E0:118E9,11950:11959,11BF0:11BF9,11C50:11C59,11D50:11D59,11DA0:11DA9,11F50:11F59,16130:16139,16A60:16A69,16AC0:16AC9,16B50:16B59,16D70:16D79,1CCF0:1CCF9,1D7CE:1D7FF,1E140:1E149,1E2F0:1E2F9,1E4F0:1E4F9,1E5F1:1E5FA,1E950:1E959,1FBF0:1FBF9 +isdigit 30:39,B2:B3,B9:B9,660:669,6F0:6F9,7C0:7C9,966:96F,9E6:9EF,A66:A6F,AE6:AEF,B66:B6F,BE6:BEF,C66:C6F,CE6:CEF,D66:D6F,DE6:DEF,E50:E59,ED0:ED9,F20:F29,1040:1049,1090:1099,1369:1371,17E0:17E9,1810:1819,1946:194F,19D0:19DA,1A80:1A89,1A90:1A99,1B50:1B59,1BB0:1BB9,1C40:1C49,1C50:1C59,2070:2070,2074:2079,2080:2089,2460:2468,2474:247C,2488:2490,24EA:24EA,24F5:24FD,24FF:24FF,2776:277E,2780:2788,278A:2792,A620:A629,A8D0:A8D9,A900:A909,A9D0:A9D9,A9F0:A9F9,AA50:AA59,ABF0:ABF9,FF10:FF19,104A0:104A9,10A40:10A43,10D30:10D39,10D40:10D49,10E60:10E68,11052:1105A,11066:1106F,110F0:110F9,11136:1113F,111D0:111D9,112F0:112F9,11450:11459,114D0:114D9,11650:11659,116C0:116C9,116D0:116E3,11730:11739,118E0:118E9,11950:11959,11BF0:11BF9,11C50:11C59,11D50:11D59,11DA0:11DA9,11F50:11F59,16130:16139,16A60:16A69,16AC0:16AC9,16B50:16B59,16D70:16D79,1CCF0:1CCF9,1D7CE:1D7FF,1E140:1E149,1E2F0:1E2F9,1E4F0:1E4F9,1E5F1:1E5FA,1E950:1E959,1F100:1F10A,1FBF0:1FBF9 +isnumeric 30:39,B2:B3,B9:B9,BC:BE,660:669,6F0:6F9,7C0:7C9,966:96F,9E6:9EF,9F4:9F9,A66:A6F,AE6:AEF,B66:B6F,B72:B77,BE6:BF2,C66:C6F,C78:C7E,CE6:CEF,D58:D5E,D66:D78,DE6:DEF,E50:E59,ED0:ED9,F20:F33,1040:1049,1090:1099,1369:137C,16EE:16F0,17E0:17E9,17F0:17F9,1810:1819,1946:194F,19D0:19DA,1A80:1A89,1A90:1A99,1B50:1B59,1BB0:1BB9,1C40:1C49,1C50:1C59,2070:2070,2074:2079,2080:2089,2150:2182,2185:2189,2460:249B,24EA:24FF,2776:2793,2CFD:2CFD,3007:3007,3021:3029,3038:303A,3192:3195,3220:3229,3248:324F,3251:325F,3280:3289,32B1:32BF,3405:3405,3483:3483,382A:382A,3B4D:3B4D,4E00:4E00,4E03:4E03,4E07:4E07,4E09:4E09,4E24:4E24,4E5D:4E5D,4E8C:4E8C,4E94:4E94,4E96:4E96,4EAC:4EAC,4EBF:4EC0,4EDF:4EDF,4EE8:4EE8,4F0D:4F0D,4F70:4F70,4FE9:4FE9,5006:5006,5104:5104,5146:5146,5169:5169,516B:516B,516D:516D,5341:5341,5343:5345,534C:534C,53C1:53C4,56DB:56DB,58F1:58F1,58F9:58F9,5E7A:5E7A,5EFE:5EFF,5F0C:5F0E,5F10:5F10,62D0:62D0,62FE:62FE,634C:634C,67D2:67D2,6D1E:6D1E,6F06:6F06,7396:7396,767E:767E,7695:7695,79ED:79ED,8086:8086,842C:842C,8CAE:8CAE,8CB3:8CB3,8D30:8D30,920E:920E,94A9:94A9,9621:9621,9646:9646,964C:964C,9678:9678,96F6:96F6,A620:A629,A6E6:A6EF,A830:A835,A8D0:A8D9,A900:A909,A9D0:A9D9,A9F0:A9F9,AA50:AA59,ABF0:ABF9,F96B:F96B,F973:F973,F978:F978,F9B2:F9B2,F9D1:F9D1,F9D3:F9D3,F9FD:F9FD,FF10:FF19,10107:10133,10140:10178,1018A:1018B,102E1:102FB,10320:10323,10341:10341,1034A:1034A,103D1:103D5,104A0:104A9,10858:1085F,10879:1087F,108A7:108AF,108FB:108FF,10916:1091B,109BC:109BD,109C0:109CF,109D2:109FF,10A40:10A48,10A7D:10A7E,10A9D:10A9F,10AEB:10AEF,10B58:10B5F,10B78:10B7F,10BA9:10BAF,10CFA:10CFF,10D30:10D39,10D40:10D49,10E60:10E7E,10F1D:10F26,10F51:10F54,10FC5:10FCB,11052:1106F,110F0:110F9,11136:1113F,111D0:111D9,111E1:111F4,112F0:112F9,11450:11459,114D0:114D9,11650:11659,116C0:116C9,116D0:116E3,11730:1173B,118E0:118F2,11950:11959,11BF0:11BF9,11C50:11C6C,11D50:11D59,11DA0:11DA9,11F50:11F59,11FC0:11FD4,12400:1246E,16130:16139,16A60:16A69,16AC0:16AC9,16B50:16B59,16B5B:16B61,16D70:16D79,16E80:16E96,1CCF0:1CCF9,1D2C0:1D2D3,1D2E0:1D2F3,1D360:1D378,1D7CE:1D7FF,1E140:1E149,1E2F0:1E2F9,1E4F0:1E4F9,1E5F1:1E5FA,1E8C7:1E8CF,1E950:1E959,1EC71:1ECAB,1ECAD:1ECAF,1ECB1:1ECB4,1ED01:1ED2D,1ED2F:1ED3D,1F100:1F10C,1FBF0:1FBF9,20001:20001,20064:20064,200E2:200E2,20121:20121,2092A:2092A,20983:20983,2098C:2098C,2099C:2099C,20AEA:20AEA,20AFD:20AFD,20B19:20B19,22390:22390,22998:22998,23B1B:23B1B,2626D:2626D,2F890:2F890 +isspace 9:D,1C:20,85:85,A0:A0,1680:1680,2000:200A,2028:2029,202F:202F,205F:205F,3000:3000 +isprintable 20:7E,A1:AC,AE:377,37A:37F,384:38A,38C:38C,38E:3A1,3A3:52F,531:556,559:58A,58D:58F,591:5C7,5D0:5EA,5EF:5F4,606:61B,61D:6DC,6DE:70D,710:74A,74D:7B1,7C0:7FA,7FD:82D,830:83E,840:85B,85E:85E,860:86A,870:88E,897:8E1,8E3:983,985:98C,98F:990,993:9A8,9AA:9B0,9B2:9B2,9B6:9B9,9BC:9C4,9C7:9C8,9CB:9CE,9D7:9D7,9DC:9DD,9DF:9E3,9E6:9FE,A01:A03,A05:A0A,A0F:A10,A13:A28,A2A:A30,A32:A33,A35:A36,A38:A39,A3C:A3C,A3E:A42,A47:A48,A4B:A4D,A51:A51,A59:A5C,A5E:A5E,A66:A76,A81:A83,A85:A8D,A8F:A91,A93:AA8,AAA:AB0,AB2:AB3,AB5:AB9,ABC:AC5,AC7:AC9,ACB:ACD,AD0:AD0,AE0:AE3,AE6:AF1,AF9:AFF,B01:B03,B05:B0C,B0F:B10,B13:B28,B2A:B30,B32:B33,B35:B39,B3C:B44,B47:B48,B4B:B4D,B55:B57,B5C:B5D,B5F:B63,B66:B77,B82:B83,B85:B8A,B8E:B90,B92:B95,B99:B9A,B9C:B9C,B9E:B9F,BA3:BA4,BA8:BAA,BAE:BB9,BBE:BC2,BC6:BC8,BCA:BCD,BD0:BD0,BD7:BD7,BE6:BFA,C00:C0C,C0E:C10,C12:C28,C2A:C39,C3C:C44,C46:C48,C4A:C4D,C55:C56,C58:C5A,C5D:C5D,C60:C63,C66:C6F,C77:C8C,C8E:C90,C92:CA8,CAA:CB3,CB5:CB9,CBC:CC4,CC6:CC8,CCA:CCD,CD5:CD6,CDD:CDE,CE0:CE3,CE6:CEF,CF1:CF3,D00:D0C,D0E:D10,D12:D44,D46:D48,D4A:D4F,D54:D63,D66:D7F,D81:D83,D85:D96,D9A:DB1,DB3:DBB,DBD:DBD,DC0:DC6,DCA:DCA,DCF:DD4,DD6:DD6,DD8:DDF,DE6:DEF,DF2:DF4,E01:E3A,E3F:E5B,E81:E82,E84:E84,E86:E8A,E8C:EA3,EA5:EA5,EA7:EBD,EC0:EC4,EC6:EC6,EC8:ECE,ED0:ED9,EDC:EDF,F00:F47,F49:F6C,F71:F97,F99:FBC,FBE:FCC,FCE:FDA,1000:10C5,10C7:10C7,10CD:10CD,10D0:1248,124A:124D,1250:1256,1258:1258,125A:125D,1260:1288,128A:128D,1290:12B0,12B2:12B5,12B8:12BE,12C0:12C0,12C2:12C5,12C8:12D6,12D8:1310,1312:1315,1318:135A,135D:137C,1380:1399,13A0:13F5,13F8:13FD,1400:167F,1681:169C,16A0:16F8,1700:1715,171F:1736,1740:1753,1760:176C,176E:1770,1772:1773,1780:17DD,17E0:17E9,17F0:17F9,1800:180D,180F:1819,1820:1878,1880:18AA,18B0:18F5,1900:191E,1920:192B,1930:193B,1940:1940,1944:196D,1970:1974,1980:19AB,19B0:19C9,19D0:19DA,19DE:1A1B,1A1E:1A5E,1A60:1A7C,1A7F:1A89,1A90:1A99,1AA0:1AAD,1AB0:1ACE,1B00:1B4C,1B4E:1BF3,1BFC:1C37,1C3B:1C49,1C4D:1C8A,1C90:1CBA,1CBD:1CC7,1CD0:1CFA,1D00:1F15,1F18:1F1D,1F20:1F45,1F48:1F4D,1F50:1F57,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F7D,1F80:1FB4,1FB6:1FC4,1FC6:1FD3,1FD6:1FDB,1FDD:1FEF,1FF2:1FF4,1FF6:1FFE,2010:2027,2030:205E,2070:2071,2074:208E,2090:209C,20A0:20C0,20D0:20F0,2100:218B,2190:2429,2440:244A,2460:2B73,2B76:2B95,2B97:2CF3,2CF9:2D25,2D27:2D27,2D2D:2D2D,2D30:2D67,2D6F:2D70,2D7F:2D96,2DA0:2DA6,2DA8:2DAE,2DB0:2DB6,2DB8:2DBE,2DC0:2DC6,2DC8:2DCE,2DD0:2DD6,2DD8:2DDE,2DE0:2E5D,2E80:2E99,2E9B:2EF3,2F00:2FD5,2FF0:2FFF,3001:303F,3041:3096,3099:30FF,3105:312F,3131:318E,3190:31E5,31EF:321E,3220:A48C,A490:A4C6,A4D0:A62B,A640:A6F7,A700:A7CD,A7D0:A7D1,A7D3:A7D3,A7D5:A7DC,A7F2:A82C,A830:A839,A840:A877,A880:A8C5,A8CE:A8D9,A8E0:A953,A95F:A97C,A980:A9CD,A9CF:A9D9,A9DE:A9FE,AA00:AA36,AA40:AA4D,AA50:AA59,AA5C:AAC2,AADB:AAF6,AB01:AB06,AB09:AB0E,AB11:AB16,AB20:AB26,AB28:AB2E,AB30:AB6B,AB70:ABED,ABF0:ABF9,AC00:D7A3,D7B0:D7C6,D7CB:D7FB,F900:FA6D,FA70:FAD9,FB00:FB06,FB13:FB17,FB1D:FB36,FB38:FB3C,FB3E:FB3E,FB40:FB41,FB43:FB44,FB46:FBC2,FBD3:FD8F,FD92:FDC7,FDCF:FDCF,FDF0:FE19,FE20:FE52,FE54:FE66,FE68:FE6B,FE70:FE74,FE76:FEFC,FF01:FFBE,FFC2:FFC7,FFCA:FFCF,FFD2:FFD7,FFDA:FFDC,FFE0:FFE6,FFE8:FFEE,FFFC:FFFD,10000:1000B,1000D:10026,10028:1003A,1003C:1003D,1003F:1004D,10050:1005D,10080:100FA,10100:10102,10107:10133,10137:1018E,10190:1019C,101A0:101A0,101D0:101FD,10280:1029C,102A0:102D0,102E0:102FB,10300:10323,1032D:1034A,10350:1037A,10380:1039D,1039F:103C3,103C8:103D5,10400:1049D,104A0:104A9,104B0:104D3,104D8:104FB,10500:10527,10530:10563,1056F:1057A,1057C:1058A,1058C:10592,10594:10595,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,105C0:105F3,10600:10736,10740:10755,10760:10767,10780:10785,10787:107B0,107B2:107BA,10800:10805,10808:10808,1080A:10835,10837:10838,1083C:1083C,1083F:10855,10857:1089E,108A7:108AF,108E0:108F2,108F4:108F5,108FB:1091B,1091F:10939,1093F:1093F,10980:109B7,109BC:109CF,109D2:10A03,10A05:10A06,10A0C:10A13,10A15:10A17,10A19:10A35,10A38:10A3A,10A3F:10A48,10A50:10A58,10A60:10A9F,10AC0:10AE6,10AEB:10AF6,10B00:10B35,10B39:10B55,10B58:10B72,10B78:10B91,10B99:10B9C,10BA9:10BAF,10C00:10C48,10C80:10CB2,10CC0:10CF2,10CFA:10D27,10D30:10D39,10D40:10D65,10D69:10D85,10D8E:10D8F,10E60:10E7E,10E80:10EA9,10EAB:10EAD,10EB0:10EB1,10EC2:10EC4,10EFC:10F27,10F30:10F59,10F70:10F89,10FB0:10FCB,10FE0:10FF6,11000:1104D,11052:11075,1107F:110BC,110BE:110C2,110D0:110E8,110F0:110F9,11100:11134,11136:11147,11150:11176,11180:111DF,111E1:111F4,11200:11211,11213:11241,11280:11286,11288:11288,1128A:1128D,1128F:1129D,1129F:112A9,112B0:112EA,112F0:112F9,11300:11303,11305:1130C,1130F:11310,11313:11328,1132A:11330,11332:11333,11335:11339,1133B:11344,11347:11348,1134B:1134D,11350:11350,11357:11357,1135D:11363,11366:1136C,11370:11374,11380:11389,1138B:1138B,1138E:1138E,11390:113B5,113B7:113C0,113C2:113C2,113C5:113C5,113C7:113CA,113CC:113D5,113D7:113D8,113E1:113E2,11400:1145B,1145D:11461,11480:114C7,114D0:114D9,11580:115B5,115B8:115DD,11600:11644,11650:11659,11660:1166C,11680:116B9,116C0:116C9,116D0:116E3,11700:1171A,1171D:1172B,11730:11746,11800:1183B,118A0:118F2,118FF:11906,11909:11909,1190C:11913,11915:11916,11918:11935,11937:11938,1193B:11946,11950:11959,119A0:119A7,119AA:119D7,119DA:119E4,11A00:11A47,11A50:11AA2,11AB0:11AF8,11B00:11B09,11BC0:11BE1,11BF0:11BF9,11C00:11C08,11C0A:11C36,11C38:11C45,11C50:11C6C,11C70:11C8F,11C92:11CA7,11CA9:11CB6,11D00:11D06,11D08:11D09,11D0B:11D36,11D3A:11D3A,11D3C:11D3D,11D3F:11D47,11D50:11D59,11D60:11D65,11D67:11D68,11D6A:11D8E,11D90:11D91,11D93:11D98,11DA0:11DA9,11EE0:11EF8,11F00:11F10,11F12:11F3A,11F3E:11F5A,11FB0:11FB0,11FC0:11FF1,11FFF:12399,12400:1246E,12470:12474,12480:12543,12F90:12FF2,13000:1342F,13440:13455,13460:143FA,14400:14646,16100:16139,16800:16A38,16A40:16A5E,16A60:16A69,16A6E:16ABE,16AC0:16AC9,16AD0:16AED,16AF0:16AF5,16B00:16B45,16B50:16B59,16B5B:16B61,16B63:16B77,16B7D:16B8F,16D40:16D79,16E40:16E9A,16F00:16F4A,16F4F:16F87,16F8F:16F9F,16FE0:16FE4,16FF0:16FF1,17000:187F7,18800:18CD5,18CFF:18D08,1AFF0:1AFF3,1AFF5:1AFFB,1AFFD:1AFFE,1B000:1B122,1B132:1B132,1B150:1B152,1B155:1B155,1B164:1B167,1B170:1B2FB,1BC00:1BC6A,1BC70:1BC7C,1BC80:1BC88,1BC90:1BC99,1BC9C:1BC9F,1CC00:1CCF9,1CD00:1CEB3,1CF00:1CF2D,1CF30:1CF46,1CF50:1CFC3,1D000:1D0F5,1D100:1D126,1D129:1D172,1D17B:1D1EA,1D200:1D245,1D2C0:1D2D3,1D2E0:1D2F3,1D300:1D356,1D360:1D378,1D400:1D454,1D456:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D51E:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D552:1D6A5,1D6A8:1D7CB,1D7CE:1DA8B,1DA9B:1DA9F,1DAA1:1DAAF,1DF00:1DF1E,1DF25:1DF2A,1E000:1E006,1E008:1E018,1E01B:1E021,1E023:1E024,1E026:1E02A,1E030:1E06D,1E08F:1E08F,1E100:1E12C,1E130:1E13D,1E140:1E149,1E14E:1E14F,1E290:1E2AE,1E2C0:1E2F9,1E2FF:1E2FF,1E4D0:1E4F9,1E5D0:1E5FA,1E5FF:1E5FF,1E7E0:1E7E6,1E7E8:1E7EB,1E7ED:1E7EE,1E7F0:1E7FE,1E800:1E8C4,1E8C7:1E8D6,1E900:1E94B,1E950:1E959,1E95E:1E95F,1EC71:1ECB4,1ED01:1ED3D,1EE00:1EE03,1EE05:1EE1F,1EE21:1EE22,1EE24:1EE24,1EE27:1EE27,1EE29:1EE32,1EE34:1EE37,1EE39:1EE39,1EE3B:1EE3B,1EE42:1EE42,1EE47:1EE47,1EE49:1EE49,1EE4B:1EE4B,1EE4D:1EE4F,1EE51:1EE52,1EE54:1EE54,1EE57:1EE57,1EE59:1EE59,1EE5B:1EE5B,1EE5D:1EE5D,1EE5F:1EE5F,1EE61:1EE62,1EE64:1EE64,1EE67:1EE6A,1EE6C:1EE72,1EE74:1EE77,1EE79:1EE7C,1EE7E:1EE7E,1EE80:1EE89,1EE8B:1EE9B,1EEA1:1EEA3,1EEA5:1EEA9,1EEAB:1EEBB,1EEF0:1EEF1,1F000:1F02B,1F030:1F093,1F0A0:1F0AE,1F0B1:1F0BF,1F0C1:1F0CF,1F0D1:1F0F5,1F100:1F1AD,1F1E6:1F202,1F210:1F23B,1F240:1F248,1F250:1F251,1F260:1F265,1F300:1F6D7,1F6DC:1F6EC,1F6F0:1F6FC,1F700:1F776,1F77B:1F7D9,1F7E0:1F7EB,1F7F0:1F7F0,1F800:1F80B,1F810:1F847,1F850:1F859,1F860:1F887,1F890:1F8AD,1F8B0:1F8BB,1F8C0:1F8C1,1F900:1FA53,1FA60:1FA6D,1FA70:1FA7C,1FA80:1FA89,1FA8F:1FAC6,1FACE:1FADC,1FADF:1FAE9,1FAF0:1FAF8,1FB00:1FB92,1FB94:1FBF9,20000:2A6DF,2A700:2B739,2B740:2B81D,2B820:2CEA1,2CEB0:2EBE0,2EBF0:2EE5D,2F800:2FA1D,30000:3134A,31350:323AF,E0100:E01EF +isidentifier 41:5A,5F:5F,61:7A,AA:AA,B5:B5,BA:BA,C0:D6,D8:F6,F8:2C1,2C6:2D1,2E0:2E4,2EC:2EC,2EE:2EE,370:374,376:377,37B:37D,37F:37F,386:386,388:38A,38C:38C,38E:3A1,3A3:3F5,3F7:481,48A:52F,531:556,559:559,560:588,5D0:5EA,5EF:5F2,620:64A,66E:66F,671:6D3,6D5:6D5,6E5:6E6,6EE:6EF,6FA:6FC,6FF:6FF,710:710,712:72F,74D:7A5,7B1:7B1,7CA:7EA,7F4:7F5,7FA:7FA,800:815,81A:81A,824:824,828:828,840:858,860:86A,870:887,889:88E,8A0:8C9,904:939,93D:93D,950:950,958:961,971:980,985:98C,98F:990,993:9A8,9AA:9B0,9B2:9B2,9B6:9B9,9BD:9BD,9CE:9CE,9DC:9DD,9DF:9E1,9F0:9F1,9FC:9FC,A05:A0A,A0F:A10,A13:A28,A2A:A30,A32:A33,A35:A36,A38:A39,A59:A5C,A5E:A5E,A72:A74,A85:A8D,A8F:A91,A93:AA8,AAA:AB0,AB2:AB3,AB5:AB9,ABD:ABD,AD0:AD0,AE0:AE1,AF9:AF9,B05:B0C,B0F:B10,B13:B28,B2A:B30,B32:B33,B35:B39,B3D:B3D,B5C:B5D,B5F:B61,B71:B71,B83:B83,B85:B8A,B8E:B90,B92:B95,B99:B9A,B9C:B9C,B9E:B9F,BA3:BA4,BA8:BAA,BAE:BB9,BD0:BD0,C05:C0C,C0E:C10,C12:C28,C2A:C39,C3D:C3D,C58:C5A,C5D:C5D,C60:C61,C80:C80,C85:C8C,C8E:C90,C92:CA8,CAA:CB3,CB5:CB9,CBD:CBD,CDD:CDE,CE0:CE1,CF1:CF2,D04:D0C,D0E:D10,D12:D3A,D3D:D3D,D4E:D4E,D54:D56,D5F:D61,D7A:D7F,D85:D96,D9A:DB1,DB3:DBB,DBD:DBD,DC0:DC6,E01:E30,E32:E32,E40:E46,E81:E82,E84:E84,E86:E8A,E8C:EA3,EA5:EA5,EA7:EB0,EB2:EB2,EBD:EBD,EC0:EC4,EC6:EC6,EDC:EDF,F00:F00,F40:F47,F49:F6C,F88:F8C,1000:102A,103F:103F,1050:1055,105A:105D,1061:1061,1065:1066,106E:1070,1075:1081,108E:108E,10A0:10C5,10C7:10C7,10CD:10CD,10D0:10FA,10FC:1248,124A:124D,1250:1256,1258:1258,125A:125D,1260:1288,128A:128D,1290:12B0,12B2:12B5,12B8:12BE,12C0:12C0,12C2:12C5,12C8:12D6,12D8:1310,1312:1315,1318:135A,1380:138F,13A0:13F5,13F8:13FD,1401:166C,166F:167F,1681:169A,16A0:16EA,16EE:16F8,1700:1711,171F:1731,1740:1751,1760:176C,176E:1770,1780:17B3,17D7:17D7,17DC:17DC,1820:1878,1880:18A8,18AA:18AA,18B0:18F5,1900:191E,1950:196D,1970:1974,1980:19AB,19B0:19C9,1A00:1A16,1A20:1A54,1AA7:1AA7,1B05:1B33,1B45:1B4C,1B83:1BA0,1BAE:1BAF,1BBA:1BE5,1C00:1C23,1C4D:1C4F,1C5A:1C7D,1C80:1C8A,1C90:1CBA,1CBD:1CBF,1CE9:1CEC,1CEE:1CF3,1CF5:1CF6,1CFA:1CFA,1D00:1DBF,1E00:1F15,1F18:1F1D,1F20:1F45,1F48:1F4D,1F50:1F57,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F7D,1F80:1FB4,1FB6:1FBC,1FBE:1FBE,1FC2:1FC4,1FC6:1FCC,1FD0:1FD3,1FD6:1FDB,1FE0:1FEC,1FF2:1FF4,1FF6:1FFC,2071:2071,207F:207F,2090:209C,2102:2102,2107:2107,210A:2113,2115:2115,2118:211D,2124:2124,2126:2126,2128:2128,212A:2139,213C:213F,2145:2149,214E:214E,2160:2188,2C00:2CE4,2CEB:2CEE,2CF2:2CF3,2D00:2D25,2D27:2D27,2D2D:2D2D,2D30:2D67,2D6F:2D6F,2D80:2D96,2DA0:2DA6,2DA8:2DAE,2DB0:2DB6,2DB8:2DBE,2DC0:2DC6,2DC8:2DCE,2DD0:2DD6,2DD8:2DDE,3005:3007,3021:3029,3031:3035,3038:303C,3041:3096,309D:309F,30A1:30FA,30FC:30FF,3105:312F,3131:318E,31A0:31BF,31F0:31FF,3400:4DBF,4E00:A48C,A4D0:A4FD,A500:A60C,A610:A61F,A62A:A62B,A640:A66E,A67F:A69D,A6A0:A6EF,A717:A71F,A722:A788,A78B:A7CD,A7D0:A7D1,A7D3:A7D3,A7D5:A7DC,A7F2:A801,A803:A805,A807:A80A,A80C:A822,A840:A873,A882:A8B3,A8F2:A8F7,A8FB:A8FB,A8FD:A8FE,A90A:A925,A930:A946,A960:A97C,A984:A9B2,A9CF:A9CF,A9E0:A9E4,A9E6:A9EF,A9FA:A9FE,AA00:AA28,AA40:AA42,AA44:AA4B,AA60:AA76,AA7A:AA7A,AA7E:AAAF,AAB1:AAB1,AAB5:AAB6,AAB9:AABD,AAC0:AAC0,AAC2:AAC2,AADB:AADD,AAE0:AAEA,AAF2:AAF4,AB01:AB06,AB09:AB0E,AB11:AB16,AB20:AB26,AB28:AB2E,AB30:AB5A,AB5C:AB69,AB70:ABE2,AC00:D7A3,D7B0:D7C6,D7CB:D7FB,F900:FA6D,FA70:FAD9,FB00:FB06,FB13:FB17,FB1D:FB1D,FB1F:FB28,FB2A:FB36,FB38:FB3C,FB3E:FB3E,FB40:FB41,FB43:FB44,FB46:FBB1,FBD3:FC5D,FC64:FD3D,FD50:FD8F,FD92:FDC7,FDF0:FDF9,FE71:FE71,FE73:FE73,FE77:FE77,FE79:FE79,FE7B:FE7B,FE7D:FE7D,FE7F:FEFC,FF21:FF3A,FF41:FF5A,FF66:FF9D,FFA0:FFBE,FFC2:FFC7,FFCA:FFCF,FFD2:FFD7,FFDA:FFDC,10000:1000B,1000D:10026,10028:1003A,1003C:1003D,1003F:1004D,10050:1005D,10080:100FA,10140:10174,10280:1029C,102A0:102D0,10300:1031F,1032D:1034A,10350:10375,10380:1039D,103A0:103C3,103C8:103CF,103D1:103D5,10400:1049D,104B0:104D3,104D8:104FB,10500:10527,10530:10563,10570:1057A,1057C:1058A,1058C:10592,10594:10595,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,105C0:105F3,10600:10736,10740:10755,10760:10767,10780:10785,10787:107B0,107B2:107BA,10800:10805,10808:10808,1080A:10835,10837:10838,1083C:1083C,1083F:10855,10860:10876,10880:1089E,108E0:108F2,108F4:108F5,10900:10915,10920:10939,10980:109B7,109BE:109BF,10A00:10A00,10A10:10A13,10A15:10A17,10A19:10A35,10A60:10A7C,10A80:10A9C,10AC0:10AC7,10AC9:10AE4,10B00:10B35,10B40:10B55,10B60:10B72,10B80:10B91,10C00:10C48,10C80:10CB2,10CC0:10CF2,10D00:10D23,10D4A:10D65,10D6F:10D85,10E80:10EA9,10EB0:10EB1,10EC2:10EC4,10F00:10F1C,10F27:10F27,10F30:10F45,10F70:10F81,10FB0:10FC4,10FE0:10FF6,11003:11037,11071:11072,11075:11075,11083:110AF,110D0:110E8,11103:11126,11144:11144,11147:11147,11150:11172,11176:11176,11183:111B2,111C1:111C4,111DA:111DA,111DC:111DC,11200:11211,11213:1122B,1123F:11240,11280:11286,11288:11288,1128A:1128D,1128F:1129D,1129F:112A8,112B0:112DE,11305:1130C,1130F:11310,11313:11328,1132A:11330,11332:11333,11335:11339,1133D:1133D,11350:11350,1135D:11361,11380:11389,1138B:1138B,1138E:1138E,11390:113B5,113B7:113B7,113D1:113D1,113D3:113D3,11400:11434,11447:1144A,1145F:11461,11480:114AF,114C4:114C5,114C7:114C7,11580:115AE,115D8:115DB,11600:1162F,11644:11644,11680:116AA,116B8:116B8,11700:1171A,11740:11746,11800:1182B,118A0:118DF,118FF:11906,11909:11909,1190C:11913,11915:11916,11918:1192F,1193F:1193F,11941:11941,119A0:119A7,119AA:119D0,119E1:119E1,119E3:119E3,11A00:11A00,11A0B:11A32,11A3A:11A3A,11A50:11A50,11A5C:11A89,11A9D:11A9D,11AB0:11AF8,11BC0:11BE0,11C00:11C08,11C0A:11C2E,11C40:11C40,11C72:11C8F,11D00:11D06,11D08:11D09,11D0B:11D30,11D46:11D46,11D60:11D65,11D67:11D68,11D6A:11D89,11D98:11D98,11EE0:11EF2,11F02:11F02,11F04:11F10,11F12:11F33,11FB0:11FB0,12000:12399,12400:1246E,12480:12543,12F90:12FF0,13000:1342F,13441:13446,13460:143FA,14400:14646,16100:1611D,16800:16A38,16A40:16A5E,16A70:16ABE,16AD0:16AED,16B00:16B2F,16B40:16B43,16B63:16B77,16B7D:16B8F,16D40:16D6C,16E40:16E7F,16F00:16F4A,16F50:16F50,16F93:16F9F,16FE0:16FE1,16FE3:16FE3,17000:187F7,18800:18CD5,18CFF:18D08,1AFF0:1AFF3,1AFF5:1AFFB,1AFFD:1AFFE,1B000:1B122,1B132:1B132,1B150:1B152,1B155:1B155,1B164:1B167,1B170:1B2FB,1BC00:1BC6A,1BC70:1BC7C,1BC80:1BC88,1BC90:1BC99,1D400:1D454,1D456:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D51E:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D552:1D6A5,1D6A8:1D6C0,1D6C2:1D6DA,1D6DC:1D6FA,1D6FC:1D714,1D716:1D734,1D736:1D74E,1D750:1D76E,1D770:1D788,1D78A:1D7A8,1D7AA:1D7C2,1D7C4:1D7CB,1DF00:1DF1E,1DF25:1DF2A,1E030:1E06D,1E100:1E12C,1E137:1E13D,1E14E:1E14E,1E290:1E2AD,1E2C0:1E2EB,1E4D0:1E4EB,1E5D0:1E5ED,1E5F0:1E5F0,1E7E0:1E7E6,1E7E8:1E7EB,1E7ED:1E7EE,1E7F0:1E7FE,1E800:1E8C4,1E900:1E943,1E94B:1E94B,1EE00:1EE03,1EE05:1EE1F,1EE21:1EE22,1EE24:1EE24,1EE27:1EE27,1EE29:1EE32,1EE34:1EE37,1EE39:1EE39,1EE3B:1EE3B,1EE42:1EE42,1EE47:1EE47,1EE49:1EE49,1EE4B:1EE4B,1EE4D:1EE4F,1EE51:1EE52,1EE54:1EE54,1EE57:1EE57,1EE59:1EE59,1EE5B:1EE5B,1EE5D:1EE5D,1EE5F:1EE5F,1EE61:1EE62,1EE64:1EE64,1EE67:1EE6A,1EE6C:1EE72,1EE74:1EE77,1EE79:1EE7C,1EE7E:1EE7E,1EE80:1EE89,1EE8B:1EE9B,1EEA1:1EEA3,1EEA5:1EEA9,1EEAB:1EEBB,20000:2A6DF,2A700:2B739,2B740:2B81D,2B820:2CEA1,2CEB0:2EBE0,2EBF0:2EE5D,2F800:2FA1D,30000:3134A,31350:323AF +is_lowercase 61:7A,AA:AA,B5:B5,BA:BA,DF:F6,F8:FF,101:101,103:103,105:105,107:107,109:109,10B:10B,10D:10D,10F:10F,111:111,113:113,115:115,117:117,119:119,11B:11B,11D:11D,11F:11F,121:121,123:123,125:125,127:127,129:129,12B:12B,12D:12D,12F:12F,131:131,133:133,135:135,137:138,13A:13A,13C:13C,13E:13E,140:140,142:142,144:144,146:146,148:149,14B:14B,14D:14D,14F:14F,151:151,153:153,155:155,157:157,159:159,15B:15B,15D:15D,15F:15F,161:161,163:163,165:165,167:167,169:169,16B:16B,16D:16D,16F:16F,171:171,173:173,175:175,177:177,17A:17A,17C:17C,17E:180,183:183,185:185,188:188,18C:18D,192:192,195:195,199:19B,19E:19E,1A1:1A1,1A3:1A3,1A5:1A5,1A8:1A8,1AA:1AB,1AD:1AD,1B0:1B0,1B4:1B4,1B6:1B6,1B9:1BA,1BD:1BF,1C6:1C6,1C9:1C9,1CC:1CC,1CE:1CE,1D0:1D0,1D2:1D2,1D4:1D4,1D6:1D6,1D8:1D8,1DA:1DA,1DC:1DD,1DF:1DF,1E1:1E1,1E3:1E3,1E5:1E5,1E7:1E7,1E9:1E9,1EB:1EB,1ED:1ED,1EF:1F0,1F3:1F3,1F5:1F5,1F9:1F9,1FB:1FB,1FD:1FD,1FF:1FF,201:201,203:203,205:205,207:207,209:209,20B:20B,20D:20D,20F:20F,211:211,213:213,215:215,217:217,219:219,21B:21B,21D:21D,21F:21F,221:221,223:223,225:225,227:227,229:229,22B:22B,22D:22D,22F:22F,231:231,233:239,23C:23C,23F:240,242:242,247:247,249:249,24B:24B,24D:24D,24F:293,295:2B8,2C0:2C1,2E0:2E4,345:345,371:371,373:373,377:377,37A:37D,390:390,3AC:3CE,3D0:3D1,3D5:3D7,3D9:3D9,3DB:3DB,3DD:3DD,3DF:3DF,3E1:3E1,3E3:3E3,3E5:3E5,3E7:3E7,3E9:3E9,3EB:3EB,3ED:3ED,3EF:3F3,3F5:3F5,3F8:3F8,3FB:3FC,430:45F,461:461,463:463,465:465,467:467,469:469,46B:46B,46D:46D,46F:46F,471:471,473:473,475:475,477:477,479:479,47B:47B,47D:47D,47F:47F,481:481,48B:48B,48D:48D,48F:48F,491:491,493:493,495:495,497:497,499:499,49B:49B,49D:49D,49F:49F,4A1:4A1,4A3:4A3,4A5:4A5,4A7:4A7,4A9:4A9,4AB:4AB,4AD:4AD,4AF:4AF,4B1:4B1,4B3:4B3,4B5:4B5,4B7:4B7,4B9:4B9,4BB:4BB,4BD:4BD,4BF:4BF,4C2:4C2,4C4:4C4,4C6:4C6,4C8:4C8,4CA:4CA,4CC:4CC,4CE:4CF,4D1:4D1,4D3:4D3,4D5:4D5,4D7:4D7,4D9:4D9,4DB:4DB,4DD:4DD,4DF:4DF,4E1:4E1,4E3:4E3,4E5:4E5,4E7:4E7,4E9:4E9,4EB:4EB,4ED:4ED,4EF:4EF,4F1:4F1,4F3:4F3,4F5:4F5,4F7:4F7,4F9:4F9,4FB:4FB,4FD:4FD,4FF:4FF,501:501,503:503,505:505,507:507,509:509,50B:50B,50D:50D,50F:50F,511:511,513:513,515:515,517:517,519:519,51B:51B,51D:51D,51F:51F,521:521,523:523,525:525,527:527,529:529,52B:52B,52D:52D,52F:52F,560:588,10D0:10FA,10FC:10FF,13F8:13FD,1C80:1C88,1C8A:1C8A,1D00:1DBF,1E01:1E01,1E03:1E03,1E05:1E05,1E07:1E07,1E09:1E09,1E0B:1E0B,1E0D:1E0D,1E0F:1E0F,1E11:1E11,1E13:1E13,1E15:1E15,1E17:1E17,1E19:1E19,1E1B:1E1B,1E1D:1E1D,1E1F:1E1F,1E21:1E21,1E23:1E23,1E25:1E25,1E27:1E27,1E29:1E29,1E2B:1E2B,1E2D:1E2D,1E2F:1E2F,1E31:1E31,1E33:1E33,1E35:1E35,1E37:1E37,1E39:1E39,1E3B:1E3B,1E3D:1E3D,1E3F:1E3F,1E41:1E41,1E43:1E43,1E45:1E45,1E47:1E47,1E49:1E49,1E4B:1E4B,1E4D:1E4D,1E4F:1E4F,1E51:1E51,1E53:1E53,1E55:1E55,1E57:1E57,1E59:1E59,1E5B:1E5B,1E5D:1E5D,1E5F:1E5F,1E61:1E61,1E63:1E63,1E65:1E65,1E67:1E67,1E69:1E69,1E6B:1E6B,1E6D:1E6D,1E6F:1E6F,1E71:1E71,1E73:1E73,1E75:1E75,1E77:1E77,1E79:1E79,1E7B:1E7B,1E7D:1E7D,1E7F:1E7F,1E81:1E81,1E83:1E83,1E85:1E85,1E87:1E87,1E89:1E89,1E8B:1E8B,1E8D:1E8D,1E8F:1E8F,1E91:1E91,1E93:1E93,1E95:1E9D,1E9F:1E9F,1EA1:1EA1,1EA3:1EA3,1EA5:1EA5,1EA7:1EA7,1EA9:1EA9,1EAB:1EAB,1EAD:1EAD,1EAF:1EAF,1EB1:1EB1,1EB3:1EB3,1EB5:1EB5,1EB7:1EB7,1EB9:1EB9,1EBB:1EBB,1EBD:1EBD,1EBF:1EBF,1EC1:1EC1,1EC3:1EC3,1EC5:1EC5,1EC7:1EC7,1EC9:1EC9,1ECB:1ECB,1ECD:1ECD,1ECF:1ECF,1ED1:1ED1,1ED3:1ED3,1ED5:1ED5,1ED7:1ED7,1ED9:1ED9,1EDB:1EDB,1EDD:1EDD,1EDF:1EDF,1EE1:1EE1,1EE3:1EE3,1EE5:1EE5,1EE7:1EE7,1EE9:1EE9,1EEB:1EEB,1EED:1EED,1EEF:1EEF,1EF1:1EF1,1EF3:1EF3,1EF5:1EF5,1EF7:1EF7,1EF9:1EF9,1EFB:1EFB,1EFD:1EFD,1EFF:1F07,1F10:1F15,1F20:1F27,1F30:1F37,1F40:1F45,1F50:1F57,1F60:1F67,1F70:1F7D,1F80:1F87,1F90:1F97,1FA0:1FA7,1FB0:1FB4,1FB6:1FB7,1FBE:1FBE,1FC2:1FC4,1FC6:1FC7,1FD0:1FD3,1FD6:1FD7,1FE0:1FE7,1FF2:1FF4,1FF6:1FF7,2071:2071,207F:207F,2090:209C,210A:210A,210E:210F,2113:2113,212F:212F,2134:2134,2139:2139,213C:213D,2146:2149,214E:214E,2170:217F,2184:2184,24D0:24E9,2C30:2C5F,2C61:2C61,2C65:2C66,2C68:2C68,2C6A:2C6A,2C6C:2C6C,2C71:2C71,2C73:2C74,2C76:2C7D,2C81:2C81,2C83:2C83,2C85:2C85,2C87:2C87,2C89:2C89,2C8B:2C8B,2C8D:2C8D,2C8F:2C8F,2C91:2C91,2C93:2C93,2C95:2C95,2C97:2C97,2C99:2C99,2C9B:2C9B,2C9D:2C9D,2C9F:2C9F,2CA1:2CA1,2CA3:2CA3,2CA5:2CA5,2CA7:2CA7,2CA9:2CA9,2CAB:2CAB,2CAD:2CAD,2CAF:2CAF,2CB1:2CB1,2CB3:2CB3,2CB5:2CB5,2CB7:2CB7,2CB9:2CB9,2CBB:2CBB,2CBD:2CBD,2CBF:2CBF,2CC1:2CC1,2CC3:2CC3,2CC5:2CC5,2CC7:2CC7,2CC9:2CC9,2CCB:2CCB,2CCD:2CCD,2CCF:2CCF,2CD1:2CD1,2CD3:2CD3,2CD5:2CD5,2CD7:2CD7,2CD9:2CD9,2CDB:2CDB,2CDD:2CDD,2CDF:2CDF,2CE1:2CE1,2CE3:2CE4,2CEC:2CEC,2CEE:2CEE,2CF3:2CF3,2D00:2D25,2D27:2D27,2D2D:2D2D,A641:A641,A643:A643,A645:A645,A647:A647,A649:A649,A64B:A64B,A64D:A64D,A64F:A64F,A651:A651,A653:A653,A655:A655,A657:A657,A659:A659,A65B:A65B,A65D:A65D,A65F:A65F,A661:A661,A663:A663,A665:A665,A667:A667,A669:A669,A66B:A66B,A66D:A66D,A681:A681,A683:A683,A685:A685,A687:A687,A689:A689,A68B:A68B,A68D:A68D,A68F:A68F,A691:A691,A693:A693,A695:A695,A697:A697,A699:A699,A69B:A69D,A723:A723,A725:A725,A727:A727,A729:A729,A72B:A72B,A72D:A72D,A72F:A731,A733:A733,A735:A735,A737:A737,A739:A739,A73B:A73B,A73D:A73D,A73F:A73F,A741:A741,A743:A743,A745:A745,A747:A747,A749:A749,A74B:A74B,A74D:A74D,A74F:A74F,A751:A751,A753:A753,A755:A755,A757:A757,A759:A759,A75B:A75B,A75D:A75D,A75F:A75F,A761:A761,A763:A763,A765:A765,A767:A767,A769:A769,A76B:A76B,A76D:A76D,A76F:A778,A77A:A77A,A77C:A77C,A77F:A77F,A781:A781,A783:A783,A785:A785,A787:A787,A78C:A78C,A78E:A78E,A791:A791,A793:A795,A797:A797,A799:A799,A79B:A79B,A79D:A79D,A79F:A79F,A7A1:A7A1,A7A3:A7A3,A7A5:A7A5,A7A7:A7A7,A7A9:A7A9,A7AF:A7AF,A7B5:A7B5,A7B7:A7B7,A7B9:A7B9,A7BB:A7BB,A7BD:A7BD,A7BF:A7BF,A7C1:A7C1,A7C3:A7C3,A7C8:A7C8,A7CA:A7CA,A7CD:A7CD,A7D1:A7D1,A7D3:A7D3,A7D5:A7D5,A7D7:A7D7,A7D9:A7D9,A7DB:A7DB,A7F2:A7F4,A7F6:A7F6,A7F8:A7FA,AB30:AB5A,AB5C:AB69,AB70:ABBF,FB00:FB06,FB13:FB17,FF41:FF5A,10428:1044F,104D8:104FB,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,10780:10780,10783:10785,10787:107B0,107B2:107BA,10CC0:10CF2,10D70:10D85,118C0:118DF,16E60:16E7F,1D41A:1D433,1D44E:1D454,1D456:1D467,1D482:1D49B,1D4B6:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D4CF,1D4EA:1D503,1D51E:1D537,1D552:1D56B,1D586:1D59F,1D5BA:1D5D3,1D5EE:1D607,1D622:1D63B,1D656:1D66F,1D68A:1D6A5,1D6C2:1D6DA,1D6DC:1D6E1,1D6FC:1D714,1D716:1D71B,1D736:1D74E,1D750:1D755,1D770:1D788,1D78A:1D78F,1D7AA:1D7C2,1D7C4:1D7C9,1D7CB:1D7CB,1DF00:1DF09,1DF0B:1DF1E,1DF25:1DF2A,1E030:1E06D,1E922:1E943 +is_uppercase 41:5A,C0:D6,D8:DE,100:100,102:102,104:104,106:106,108:108,10A:10A,10C:10C,10E:10E,110:110,112:112,114:114,116:116,118:118,11A:11A,11C:11C,11E:11E,120:120,122:122,124:124,126:126,128:128,12A:12A,12C:12C,12E:12E,130:130,132:132,134:134,136:136,139:139,13B:13B,13D:13D,13F:13F,141:141,143:143,145:145,147:147,14A:14A,14C:14C,14E:14E,150:150,152:152,154:154,156:156,158:158,15A:15A,15C:15C,15E:15E,160:160,162:162,164:164,166:166,168:168,16A:16A,16C:16C,16E:16E,170:170,172:172,174:174,176:176,178:179,17B:17B,17D:17D,181:182,184:184,186:187,189:18B,18E:191,193:194,196:198,19C:19D,19F:1A0,1A2:1A2,1A4:1A4,1A6:1A7,1A9:1A9,1AC:1AC,1AE:1AF,1B1:1B3,1B5:1B5,1B7:1B8,1BC:1BC,1C4:1C4,1C7:1C7,1CA:1CA,1CD:1CD,1CF:1CF,1D1:1D1,1D3:1D3,1D5:1D5,1D7:1D7,1D9:1D9,1DB:1DB,1DE:1DE,1E0:1E0,1E2:1E2,1E4:1E4,1E6:1E6,1E8:1E8,1EA:1EA,1EC:1EC,1EE:1EE,1F1:1F1,1F4:1F4,1F6:1F8,1FA:1FA,1FC:1FC,1FE:1FE,200:200,202:202,204:204,206:206,208:208,20A:20A,20C:20C,20E:20E,210:210,212:212,214:214,216:216,218:218,21A:21A,21C:21C,21E:21E,220:220,222:222,224:224,226:226,228:228,22A:22A,22C:22C,22E:22E,230:230,232:232,23A:23B,23D:23E,241:241,243:246,248:248,24A:24A,24C:24C,24E:24E,370:370,372:372,376:376,37F:37F,386:386,388:38A,38C:38C,38E:38F,391:3A1,3A3:3AB,3CF:3CF,3D2:3D4,3D8:3D8,3DA:3DA,3DC:3DC,3DE:3DE,3E0:3E0,3E2:3E2,3E4:3E4,3E6:3E6,3E8:3E8,3EA:3EA,3EC:3EC,3EE:3EE,3F4:3F4,3F7:3F7,3F9:3FA,3FD:42F,460:460,462:462,464:464,466:466,468:468,46A:46A,46C:46C,46E:46E,470:470,472:472,474:474,476:476,478:478,47A:47A,47C:47C,47E:47E,480:480,48A:48A,48C:48C,48E:48E,490:490,492:492,494:494,496:496,498:498,49A:49A,49C:49C,49E:49E,4A0:4A0,4A2:4A2,4A4:4A4,4A6:4A6,4A8:4A8,4AA:4AA,4AC:4AC,4AE:4AE,4B0:4B0,4B2:4B2,4B4:4B4,4B6:4B6,4B8:4B8,4BA:4BA,4BC:4BC,4BE:4BE,4C0:4C1,4C3:4C3,4C5:4C5,4C7:4C7,4C9:4C9,4CB:4CB,4CD:4CD,4D0:4D0,4D2:4D2,4D4:4D4,4D6:4D6,4D8:4D8,4DA:4DA,4DC:4DC,4DE:4DE,4E0:4E0,4E2:4E2,4E4:4E4,4E6:4E6,4E8:4E8,4EA:4EA,4EC:4EC,4EE:4EE,4F0:4F0,4F2:4F2,4F4:4F4,4F6:4F6,4F8:4F8,4FA:4FA,4FC:4FC,4FE:4FE,500:500,502:502,504:504,506:506,508:508,50A:50A,50C:50C,50E:50E,510:510,512:512,514:514,516:516,518:518,51A:51A,51C:51C,51E:51E,520:520,522:522,524:524,526:526,528:528,52A:52A,52C:52C,52E:52E,531:556,10A0:10C5,10C7:10C7,10CD:10CD,13A0:13F5,1C89:1C89,1C90:1CBA,1CBD:1CBF,1E00:1E00,1E02:1E02,1E04:1E04,1E06:1E06,1E08:1E08,1E0A:1E0A,1E0C:1E0C,1E0E:1E0E,1E10:1E10,1E12:1E12,1E14:1E14,1E16:1E16,1E18:1E18,1E1A:1E1A,1E1C:1E1C,1E1E:1E1E,1E20:1E20,1E22:1E22,1E24:1E24,1E26:1E26,1E28:1E28,1E2A:1E2A,1E2C:1E2C,1E2E:1E2E,1E30:1E30,1E32:1E32,1E34:1E34,1E36:1E36,1E38:1E38,1E3A:1E3A,1E3C:1E3C,1E3E:1E3E,1E40:1E40,1E42:1E42,1E44:1E44,1E46:1E46,1E48:1E48,1E4A:1E4A,1E4C:1E4C,1E4E:1E4E,1E50:1E50,1E52:1E52,1E54:1E54,1E56:1E56,1E58:1E58,1E5A:1E5A,1E5C:1E5C,1E5E:1E5E,1E60:1E60,1E62:1E62,1E64:1E64,1E66:1E66,1E68:1E68,1E6A:1E6A,1E6C:1E6C,1E6E:1E6E,1E70:1E70,1E72:1E72,1E74:1E74,1E76:1E76,1E78:1E78,1E7A:1E7A,1E7C:1E7C,1E7E:1E7E,1E80:1E80,1E82:1E82,1E84:1E84,1E86:1E86,1E88:1E88,1E8A:1E8A,1E8C:1E8C,1E8E:1E8E,1E90:1E90,1E92:1E92,1E94:1E94,1E9E:1E9E,1EA0:1EA0,1EA2:1EA2,1EA4:1EA4,1EA6:1EA6,1EA8:1EA8,1EAA:1EAA,1EAC:1EAC,1EAE:1EAE,1EB0:1EB0,1EB2:1EB2,1EB4:1EB4,1EB6:1EB6,1EB8:1EB8,1EBA:1EBA,1EBC:1EBC,1EBE:1EBE,1EC0:1EC0,1EC2:1EC2,1EC4:1EC4,1EC6:1EC6,1EC8:1EC8,1ECA:1ECA,1ECC:1ECC,1ECE:1ECE,1ED0:1ED0,1ED2:1ED2,1ED4:1ED4,1ED6:1ED6,1ED8:1ED8,1EDA:1EDA,1EDC:1EDC,1EDE:1EDE,1EE0:1EE0,1EE2:1EE2,1EE4:1EE4,1EE6:1EE6,1EE8:1EE8,1EEA:1EEA,1EEC:1EEC,1EEE:1EEE,1EF0:1EF0,1EF2:1EF2,1EF4:1EF4,1EF6:1EF6,1EF8:1EF8,1EFA:1EFA,1EFC:1EFC,1EFE:1EFE,1F08:1F0F,1F18:1F1D,1F28:1F2F,1F38:1F3F,1F48:1F4D,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F5F,1F68:1F6F,1FB8:1FBB,1FC8:1FCB,1FD8:1FDB,1FE8:1FEC,1FF8:1FFB,2102:2102,2107:2107,210B:210D,2110:2112,2115:2115,2119:211D,2124:2124,2126:2126,2128:2128,212A:212D,2130:2133,213E:213F,2145:2145,2160:216F,2183:2183,24B6:24CF,2C00:2C2F,2C60:2C60,2C62:2C64,2C67:2C67,2C69:2C69,2C6B:2C6B,2C6D:2C70,2C72:2C72,2C75:2C75,2C7E:2C80,2C82:2C82,2C84:2C84,2C86:2C86,2C88:2C88,2C8A:2C8A,2C8C:2C8C,2C8E:2C8E,2C90:2C90,2C92:2C92,2C94:2C94,2C96:2C96,2C98:2C98,2C9A:2C9A,2C9C:2C9C,2C9E:2C9E,2CA0:2CA0,2CA2:2CA2,2CA4:2CA4,2CA6:2CA6,2CA8:2CA8,2CAA:2CAA,2CAC:2CAC,2CAE:2CAE,2CB0:2CB0,2CB2:2CB2,2CB4:2CB4,2CB6:2CB6,2CB8:2CB8,2CBA:2CBA,2CBC:2CBC,2CBE:2CBE,2CC0:2CC0,2CC2:2CC2,2CC4:2CC4,2CC6:2CC6,2CC8:2CC8,2CCA:2CCA,2CCC:2CCC,2CCE:2CCE,2CD0:2CD0,2CD2:2CD2,2CD4:2CD4,2CD6:2CD6,2CD8:2CD8,2CDA:2CDA,2CDC:2CDC,2CDE:2CDE,2CE0:2CE0,2CE2:2CE2,2CEB:2CEB,2CED:2CED,2CF2:2CF2,A640:A640,A642:A642,A644:A644,A646:A646,A648:A648,A64A:A64A,A64C:A64C,A64E:A64E,A650:A650,A652:A652,A654:A654,A656:A656,A658:A658,A65A:A65A,A65C:A65C,A65E:A65E,A660:A660,A662:A662,A664:A664,A666:A666,A668:A668,A66A:A66A,A66C:A66C,A680:A680,A682:A682,A684:A684,A686:A686,A688:A688,A68A:A68A,A68C:A68C,A68E:A68E,A690:A690,A692:A692,A694:A694,A696:A696,A698:A698,A69A:A69A,A722:A722,A724:A724,A726:A726,A728:A728,A72A:A72A,A72C:A72C,A72E:A72E,A732:A732,A734:A734,A736:A736,A738:A738,A73A:A73A,A73C:A73C,A73E:A73E,A740:A740,A742:A742,A744:A744,A746:A746,A748:A748,A74A:A74A,A74C:A74C,A74E:A74E,A750:A750,A752:A752,A754:A754,A756:A756,A758:A758,A75A:A75A,A75C:A75C,A75E:A75E,A760:A760,A762:A762,A764:A764,A766:A766,A768:A768,A76A:A76A,A76C:A76C,A76E:A76E,A779:A779,A77B:A77B,A77D:A77E,A780:A780,A782:A782,A784:A784,A786:A786,A78B:A78B,A78D:A78D,A790:A790,A792:A792,A796:A796,A798:A798,A79A:A79A,A79C:A79C,A79E:A79E,A7A0:A7A0,A7A2:A7A2,A7A4:A7A4,A7A6:A7A6,A7A8:A7A8,A7AA:A7AE,A7B0:A7B4,A7B6:A7B6,A7B8:A7B8,A7BA:A7BA,A7BC:A7BC,A7BE:A7BE,A7C0:A7C0,A7C2:A7C2,A7C4:A7C7,A7C9:A7C9,A7CB:A7CC,A7D0:A7D0,A7D6:A7D6,A7D8:A7D8,A7DA:A7DA,A7DC:A7DC,A7F5:A7F5,FF21:FF3A,10400:10427,104B0:104D3,10570:1057A,1057C:1058A,1058C:10592,10594:10595,10C80:10CB2,10D50:10D65,118A0:118BF,16E40:16E5F,1D400:1D419,1D434:1D44D,1D468:1D481,1D49C:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B5,1D4D0:1D4E9,1D504:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D538:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D56C:1D585,1D5A0:1D5B9,1D5D4:1D5ED,1D608:1D621,1D63C:1D655,1D670:1D689,1D6A8:1D6C0,1D6E2:1D6FA,1D71C:1D734,1D756:1D76E,1D790:1D7A8,1D7CA:1D7CA,1E900:1E921,1F130:1F149,1F150:1F169,1F170:1F189 +is_titlecase 1C5:1C5,1C8:1C8,1CB:1CB,1F2:1F2,1F88:1F8F,1F98:1F9F,1FA8:1FAF,1FBC:1FBC,1FCC:1FCC,1FFC:1FFC +is_cased 41:5A,61:7A,B5:B5,C0:D6,D8:F6,F8:137,139:18C,18E:1A9,1AC:1B9,1BC:1BD,1BF:1BF,1C4:220,222:233,23A:254,256:257,259:259,25B:25C,260:261,263:266,268:26C,26F:26F,271:272,275:275,27D:27D,280:280,282:283,287:28C,292:292,29D:29E,345:345,370:373,376:377,37B:37D,37F:37F,386:386,388:38A,38C:38C,38E:3A1,3A3:3D1,3D5:3F5,3F7:3FB,3FD:481,48A:52F,531:556,561:587,10A0:10C5,10C7:10C7,10CD:10CD,10D0:10FA,10FD:10FF,13A0:13F5,13F8:13FD,1C80:1C8A,1C90:1CBA,1CBD:1CBF,1D79:1D79,1D7D:1D7D,1D8E:1D8E,1E00:1E9B,1E9E:1E9E,1EA0:1F15,1F18:1F1D,1F20:1F45,1F48:1F4D,1F50:1F57,1F59:1F59,1F5B:1F5B,1F5D:1F5D,1F5F:1F7D,1F80:1FB4,1FB6:1FBC,1FBE:1FBE,1FC2:1FC4,1FC6:1FCC,1FD0:1FD3,1FD6:1FDB,1FE0:1FEC,1FF2:1FF4,1FF6:1FFC,2126:2126,212A:212B,2132:2132,214E:214E,2160:217F,2183:2184,24B6:24E9,2C00:2C70,2C72:2C73,2C75:2C76,2C7E:2CE3,2CEB:2CEE,2CF2:2CF3,2D00:2D25,2D27:2D27,2D2D:2D2D,A640:A66D,A680:A69B,A722:A72F,A732:A76F,A779:A787,A78B:A78D,A790:A794,A796:A7AE,A7B0:A7CD,A7D0:A7D1,A7D6:A7DC,A7F5:A7F6,AB53:AB53,AB70:ABBF,FB00:FB06,FB13:FB17,FF21:FF3A,FF41:FF5A,10400:1044F,104B0:104D3,104D8:104FB,10570:1057A,1057C:1058A,1058C:10592,10594:10595,10597:105A1,105A3:105B1,105B3:105B9,105BB:105BC,10C80:10CB2,10CC0:10CF2,10D50:10D65,10D70:10D85,118A0:118DF,16E40:16E7F,1E900:1E943 diff --git a/crates/unicode/tests/data/version_skew_cpython3.14.txt b/crates/unicode/tests/data/version_skew_cpython3.14.txt new file mode 100644 index 00000000000..e135c422fb4 --- /dev/null +++ b/crates/unicode/tests/data/version_skew_cpython3.14.txt @@ -0,0 +1,14 @@ +# Code points whose classification differs between CPython 3.14 (Unicode 16.0.0) +# and the Rust std / icu4x build used here (a later Unicode release assigns them). +# Regenerate with RUSTPYTHON_UNICODE_REGEN_SKEW=1. +# Format: `predicate start:end,...` with inclusive hex ranges. +is_cased AA:AA,BA:BA,138:138,18D:18D,1AA:1AB,1BA:1BA,1BE:1BE,221:221,234:239,255:255,258:258,25A:25A,25D:25F,262:262,267:267,26D:26E,270:270,273:274,276:27C,27E:27F,281:281,284:286,28D:291,293:293,296:29C,29F:2B8,2C0:2C1,2E0:2E4,37A:37A,3D2:3D4,3FC:3FC,560:560,588:588,10FC:10FC,1D00:1D78,1D7A:1D7C,1D7E:1D8D,1D8F:1DBF,1E9C:1E9D,1E9F:1E9F,2071:2071,207F:207F,2090:209C,2102:2102,2107:2107,210A:2113,2115:2115,2119:211D,2124:2124,2128:2128,212C:212D,212F:2131,2133:2134,2139:2139,213C:213F,2145:2149,2C71:2C71,2C74:2C74,2C77:2C7D,2CE4:2CE4,A69C:A69D,A730:A731,A770:A778,A78E:A78E,A795:A795,A7AF:A7AF,A7CE:A7CF,A7D2:A7D5,A7F1:A7F4,A7F8:A7FA,AB30:AB52,AB54:AB5A,AB5C:AB69,10780:10780,10783:10785,10787:107B0,107B2:107BA,16EA0:16EB8,16EBB:16ED3,1D400:1D454,1D456:1D49C,1D49E:1D49F,1D4A2:1D4A2,1D4A5:1D4A6,1D4A9:1D4AC,1D4AE:1D4B9,1D4BB:1D4BB,1D4BD:1D4C3,1D4C5:1D505,1D507:1D50A,1D50D:1D514,1D516:1D51C,1D51E:1D539,1D53B:1D53E,1D540:1D544,1D546:1D546,1D54A:1D550,1D552:1D6A5,1D6A8:1D6C0,1D6C2:1D6DA,1D6DC:1D6FA,1D6FC:1D714,1D716:1D734,1D736:1D74E,1D750:1D76E,1D770:1D788,1D78A:1D7A8,1D7AA:1D7C2,1D7C4:1D7CB,1DF00:1DF09,1DF0B:1DF1E,1DF25:1DF2A,1E030:1E06D,1F130:1F149,1F150:1F169,1F170:1F189 +is_lowercase 295:295,A7CF:A7CF,A7F1:A7F1,16EBB:16ED3 +is_uppercase A7CE:A7CE,A7D2:A7D2,A7D4:A7D4,16EA0:16EB8 +isalnum 88F:88F,C5C:C5C,CDC:CDC,A7CE:A7CF,A7D2:A7D2,A7D4:A7D4,A7F1:A7F1,10940:10959,10EC5:10EC7,11DB0:11DDB,11DE0:11DE9,16EA0:16EB8,16EBB:16ED3,16FF2:16FF6,187F8:187FF,18D09:18D1E,18D80:18DF2,1E6C0:1E6DE,1E6E0:1E6E2,1E6E4:1E6E5,1E6E7:1E6ED,1E6F0:1E6F4,1E6FE:1E6FF,2B73A:2B73F,2CEA2:2CEAD,323B0:33479 +isalpha 88F:88F,C5C:C5C,CDC:CDC,A7CE:A7CF,A7D2:A7D2,A7D4:A7D4,A7F1:A7F1,10940:10959,10EC5:10EC7,11DB0:11DDB,16EA0:16EB8,16EBB:16ED3,16FF2:16FF3,187F8:187FF,18D09:18D1E,18D80:18DF2,1E6C0:1E6DE,1E6E0:1E6E2,1E6E4:1E6E5,1E6E7:1E6ED,1E6F0:1E6F4,1E6FE:1E6FF,2B73A:2B73F,2CEA2:2CEAD,323B0:33479 +isdecimal 11DE0:11DE9 +isdigit 11DE0:11DE9 +isidentifier 88F:88F,C5C:C5C,CDC:CDC,A7CE:A7CF,A7D2:A7D2,A7D4:A7D4,A7F1:A7F1,10940:10959,10EC5:10EC7,11DB0:11DDB,16EA0:16EB8,16EBB:16ED3,16FF2:16FF6,187F8:187FF,18D09:18D1E,18D80:18DF2,1E6C0:1E6DE,1E6E0:1E6E2,1E6E4:1E6E5,1E6E7:1E6ED,1E6F0:1E6F4,1E6FE:1E6FF,2B73A:2B73F,2CEA2:2CEAD,323B0:33479 +isnumeric 11DE0:11DE9,12038:12039,12079:12079,12226:12226,1222B:1222B,1230B:1230B,1230D:1230D,12399:12399,16FF4:16FF6 +isprintable 88F:88F,C5C:C5C,CDC:CDC,1ACF:1ADD,1AE0:1AEB,20C1:20C1,2B96:2B96,A7CE:A7CF,A7D2:A7D2,A7D4:A7D4,A7F1:A7F1,FBC3:FBD2,FD90:FD91,FDC8:FDCE,10940:10959,10EC5:10EC7,10ED0:10ED8,10EFA:10EFB,11B60:11B67,11DB0:11DDB,11DE0:11DE9,16EA0:16EB8,16EBB:16ED3,16FF2:16FF6,187F8:187FF,18D09:18D1E,18D80:18DF2,1CCFA:1CCFC,1CEBA:1CED0,1CEE0:1CEF0,1E6C0:1E6DE,1E6E0:1E6F5,1E6FE:1E6FF,1F6D8:1F6D8,1F777:1F77A,1F8D0:1F8D8,1FA54:1FA57,1FA8A:1FA8A,1FA8E:1FA8E,1FAC8:1FAC8,1FACD:1FACD,1FAEA:1FAEA,1FAEF:1FAEF,1FBFA:1FBFA,2B73A:2B73F,2CEA2:2CEAD,323B0:33479 diff --git a/crates/unicode/tests/data/version_skew_mappings_cpython3.14.txt b/crates/unicode/tests/data/version_skew_mappings_cpython3.14.txt new file mode 100644 index 00000000000..8992b18989c --- /dev/null +++ b/crates/unicode/tests/data/version_skew_mappings_cpython3.14.txt @@ -0,0 +1,5 @@ +# Code points whose simple case mapping differs between CPython 3.14 +# (Unicode 16.0.0) and the icu4x build used here (a later Unicode release +# assigns the pair). Regenerate with RUSTPYTHON_UNICODE_REGEN_SKEW=1. +# Format: `mapping start:end,...` with inclusive hex ranges. +tolower A7CE:A7CE,A7D2:A7D2,A7D4:A7D4,16EA0:16EB8 diff --git a/crates/unicode/tests/differential.rs b/crates/unicode/tests/differential.rs new file mode 100644 index 00000000000..1bed13e7faf --- /dev/null +++ b/crates/unicode/tests/differential.rs @@ -0,0 +1,424 @@ +//! Differential sweep of the classification predicates over the full scalar +//! range `0..0x110000` against a committed CPython reference dataset. +//! +//! CPython 3.14 ships Unicode 16.0.0 while the Rust standard library / icu4x +//! build used here may be a later release. Code points whose classification +//! changed between those Unicode versions are expected to differ; those are +//! recorded in `data/version_skew_cpython3.14.txt` as an explicit allow-list. +//! Any divergence outside that list fails the test — a real regression, not a +//! version bump. +//! +//! Both data files use the same run-length format: one `predicate` line per +//! str method, followed by comma-separated hex `start:end` inclusive ranges. + +// spell-checker:ignore recategorized recategorizations + +#[cfg(test)] +mod tests { + extern crate alloc; + + use alloc::collections::{BTreeMap, BTreeSet}; + + use rustpython_unicode::{case, classify}; + + const MAX: u32 = 0x110000; + const REFERENCE: &str = include_str!("data/cpython3.14_predicates.txt"); + const VERSION_SKEW: &str = include_str!("data/version_skew_cpython3.14.txt"); + const MAPPINGS: &str = include_str!("data/cpython3.14_mappings.txt"); + const MAPPING_SKEW: &str = include_str!("data/version_skew_mappings_cpython3.14.txt"); + + fn crate_predicate(name: &str, cp: u32) -> bool { + let Some(c) = char::from_u32(cp) else { + // Lone surrogates are not scalars; every str predicate is false. + return false; + }; + match name { + "isalpha" => classify::is_alpha(c), + "isalnum" => classify::is_alnum(c), + "isdecimal" => classify::is_decimal(c), + "isdigit" => classify::is_digit(c), + "isnumeric" => classify::is_numeric(c), + "isspace" => classify::is_space(c), + "isprintable" => classify::is_printable(c), + "isidentifier" => { + // str.isidentifier is a whole-string predicate; for a single char it + // is "may start an identifier". + classify_is_identifier_char(c) + } + "is_lowercase" => case::is_lowercase(c), + "is_uppercase" => case::is_uppercase(c), + "is_titlecase" => case::is_titlecase(c), + "is_cased" => case::is_cased(c), + other => panic!("unknown predicate {other}"), + } + } + + /// The crate's simple case mapping for `name` at `cp`, as a code point. + fn crate_mapping(name: &str, cp: u32) -> u32 { + let Some(c) = char::from_u32(cp) else { + return cp; + }; + match name { + "tolower" => case::simple_lowercase(c) as u32, + other => panic!("unknown mapping {other}"), + } + } + + fn classify_is_identifier_char(c: char) -> bool { + rustpython_unicode::identifier::is_start(c) + } + + /// Parse a `name -> sorted set of code points` map from a run-length file. + /// + /// Each non-comment line is `predicate start:end,start:end,...` with inclusive + /// hex ranges; a predicate with no members is a bare `predicate`. + fn parse_ranges(text: &str) -> BTreeMap> { + let mut map = BTreeMap::new(); + for line in text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let (name, packed) = match line.split_once(' ') { + Some((name, packed)) => (name, packed.trim()), + None => (line, ""), + }; + let mut set = BTreeSet::new(); + if !packed.is_empty() { + for run in packed.split(',') { + let (s, e) = run.split_once(':').expect("run is start:end"); + let start = u32::from_str_radix(s, 16).unwrap(); + let end = u32::from_str_radix(e, 16).unwrap(); + for cp in start..=end { + set.insert(cp); + } + } + } + map.insert(name.to_string(), set); + } + map + } + + /// Parse a `name -> {code point -> mapped code point}` table. + /// + /// Each non-comment line is `name cp:mapped,cp:mapped,...` listing only the + /// code points whose mapping differs from identity. + fn parse_mappings(text: &str) -> BTreeMap> { + let mut map = BTreeMap::new(); + for line in text.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let (name, packed) = match line.split_once(' ') { + Some((name, packed)) => (name, packed.trim()), + None => (line, ""), + }; + let mut table = BTreeMap::new(); + if !packed.is_empty() { + for pair in packed.split(',') { + let (cp, mapped) = pair.split_once(':').expect("pair is cp:mapped"); + table.insert( + u32::from_str_radix(cp, 16).unwrap(), + u32::from_str_radix(mapped, 16).unwrap(), + ); + } + } + map.insert(name.to_string(), table); + } + map + } + + /// Collapse a sorted code-point set into inclusive `start:end` runs. + fn encode_ranges(set: &BTreeSet) -> String { + let mut runs = Vec::new(); + let mut iter = set.iter().copied(); + if let Some(first) = iter.next() { + let (mut start, mut end) = (first, first); + for cp in iter { + if cp == end + 1 { + end = cp; + } else { + runs.push((start, end)); + start = cp; + end = cp; + } + } + runs.push((start, end)); + } + runs.iter() + .map(|(s, e)| format!("{s:X}:{e:X}")) + .collect::>() + .join(",") + } + + /// Recompute the full divergence set. Every entry is a `(predicate, code + /// point)` where the crate and the CPython reference disagree. + fn all_divergences(reference: &BTreeMap>) -> Vec<(String, u32, bool)> { + let mut out = Vec::new(); + for (name, truth) in reference { + for cp in 0..MAX { + let expected = truth.contains(&cp); + let actual = crate_predicate(name, cp); + if expected != actual { + out.push((name.clone(), cp, expected)); + } + } + } + out + } + + /// Documented `cpython=true/crate=false` divergences: a code point that had a + /// property in Unicode 16.0.0 but lost it in the release icu4x ships, because + /// the code point was recategorized (not a regression in this crate). + /// + /// * U+0295 LATIN LETTER PHARYNGEAL VOICED FRICATIVE was general category `Ll` + /// in Unicode 16.0.0 and `Lo` from 17.0.0, so it is no longer `Lowercase`. + const KNOWN_RECATEGORIZATIONS: &[(&str, u32)] = &[("is_lowercase", 0x0295)]; + + /// Regenerate `data/version_skew_cpython3.14.txt` from the current toolchain. + /// + /// Run with `RUSTPYTHON_UNICODE_REGEN_SKEW=1 cargo test -p rustpython-unicode + /// --test differential` after bumping the Rust/icu toolchain. Divergences are + /// normally one-directional (crate=true, cpython=false) — newly-assigned code + /// points from a later Unicode release. A `cpython=true, crate=false` entry + /// means a code point lost a property; that is a real regression unless it is + /// an explicit entry in `KNOWN_RECATEGORIZATIONS`, so this refuses to record + /// any other reverse-direction divergence. + #[test] + fn regen_version_skew() { + if std::env::var_os("RUSTPYTHON_UNICODE_REGEN_SKEW").is_none() { + return; + } + let reference = parse_ranges(REFERENCE); + let divergences = all_divergences(&reference); + + let regressions: Vec<_> = divergences + .iter() + .filter(|(name, cp, expected)| { + *expected && !KNOWN_RECATEGORIZATIONS.contains(&(name.as_str(), *cp)) + }) + .collect(); + assert!( + regressions.is_empty(), + "refusing to record {} cpython=true/crate=false divergence(s) — these are \ + regressions, not version skew: {:?}", + regressions.len(), + ®ressions[..regressions.len().min(20)] + ); + + let mut by_predicate: BTreeMap> = BTreeMap::new(); + for (name, cp, _) in &divergences { + by_predicate.entry(name.clone()).or_default().insert(*cp); + } + + let mut body = String::from( + "# Code points whose classification differs between CPython 3.14 (Unicode 16.0.0)\n\ + # and the Rust std / icu4x build used here (a later Unicode release assigns them).\n\ + # Regenerate with RUSTPYTHON_UNICODE_REGEN_SKEW=1.\n\ + # Format: `predicate start:end,...` with inclusive hex ranges.\n", + ); + for (name, set) in &by_predicate { + body.push_str(&format!("{name} {}\n", encode_ranges(set))); + } + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/version_skew_cpython3.14.txt" + ); + std::fs::write(path, body).unwrap(); + eprintln!( + "wrote {} skew code points across {} predicates to {path}", + divergences.len(), + by_predicate.len() + ); + } + + #[test] + fn predicates_match_cpython_except_documented_version_skew() { + let reference = parse_ranges(REFERENCE); + let skew = parse_ranges(VERSION_SKEW); + + let allowed = |name: &str, cp: u32| skew.get(name).is_some_and(|set| set.contains(&cp)); + + let mut unexpected: Vec<(String, u32, bool, bool)> = Vec::new(); + + for (name, truth) in &reference { + for cp in 0..MAX { + let expected = truth.contains(&cp); + let actual = crate_predicate(name, cp); + if expected != actual && !allowed(name, cp) { + unexpected.push((name.clone(), cp, expected, actual)); + } + } + } + + // Also flag stale allow-list entries: code points that no longer diverge. + let mut stale: Vec<(String, u32)> = Vec::new(); + for (name, set) in &skew { + let Some(truth) = reference.get(name) else { + continue; + }; + for &cp in set { + let expected = truth.contains(&cp); + let actual = crate_predicate(name, cp); + if expected == actual { + stale.push((name.clone(), cp)); + } + } + } + + if !unexpected.is_empty() || !stale.is_empty() { + let mut msg = String::new(); + if !unexpected.is_empty() { + msg.push_str(&format!( + "{} undocumented divergence(s) from CPython:\n", + unexpected.len() + )); + for (name, cp, expected, actual) in unexpected.iter().take(50) { + msg.push_str(&format!( + " {name} U+{cp:04X}: cpython={expected} crate={actual}\n" + )); + } + } + if !stale.is_empty() { + msg.push_str(&format!( + "{} stale version_skew_cpython3.14.txt entries that now agree:\n", + stale.len() + )); + for (name, cp) in stale.iter().take(50) { + msg.push_str(&format!(" {name} U+{cp:04X}\n")); + } + } + panic!("{msg}"); + } + } + + /// All `(mapping, code point)` where the crate and CPython map differently. + fn all_mapping_divergences( + reference: &BTreeMap>, + ) -> Vec<(String, u32)> { + let mut out = Vec::new(); + for (name, table) in reference { + for cp in 0..MAX { + let expected = table.get(&cp).copied().unwrap_or(cp); + if crate_mapping(name, cp) != expected { + out.push((name.clone(), cp)); + } + } + } + out + } + + /// Regenerate `data/version_skew_mappings_cpython3.14.txt` from the current + /// toolchain (`RUSTPYTHON_UNICODE_REGEN_SKEW=1`). + /// + /// Divergences are normally the crate gaining a mapping a later Unicode + /// release assigns. A code point that CPython maps but the crate leaves + /// unmapped is a regression, not version skew, so this refuses to record it. + #[test] + fn regen_mapping_version_skew() { + if std::env::var_os("RUSTPYTHON_UNICODE_REGEN_SKEW").is_none() { + return; + } + let reference = parse_mappings(MAPPINGS); + let divergences = all_mapping_divergences(&reference); + + let regressions: Vec<_> = divergences + .iter() + .filter(|(name, cp)| { + let expected = reference.get(name).and_then(|t| t.get(cp)).copied(); + expected.is_some_and(|e| e != *cp) && crate_mapping(name, *cp) == *cp + }) + .collect(); + assert!( + regressions.is_empty(), + "refusing to record {} cpython-maps/crate-unmapped divergence(s) — these are \ + regressions, not version skew: {:?}", + regressions.len(), + ®ressions[..regressions.len().min(20)] + ); + + let mut by_mapping: BTreeMap> = BTreeMap::new(); + for (name, cp) in &divergences { + by_mapping.entry(name.clone()).or_default().insert(*cp); + } + + let mut body = String::from( + "# Code points whose simple case mapping differs between CPython 3.14\n\ + # (Unicode 16.0.0) and the icu4x build used here (a later Unicode release\n\ + # assigns the pair). Regenerate with RUSTPYTHON_UNICODE_REGEN_SKEW=1.\n\ + # Format: `mapping start:end,...` with inclusive hex ranges.\n", + ); + for (name, set) in &by_mapping { + body.push_str(&format!("{name} {}\n", encode_ranges(set))); + } + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/version_skew_mappings_cpython3.14.txt" + ); + std::fs::write(path, body).unwrap(); + eprintln!( + "wrote {} skew code points across {} mappings to {path}", + divergences.len(), + by_mapping.len() + ); + } + + #[test] + fn simple_mappings_match_cpython_except_documented_version_skew() { + let reference = parse_mappings(MAPPINGS); + let skew = parse_ranges(MAPPING_SKEW); + + let allowed = |name: &str, cp: u32| skew.get(name).is_some_and(|set| set.contains(&cp)); + + let mut unexpected: Vec<(String, u32, u32, u32)> = Vec::new(); + for (name, table) in &reference { + for cp in 0..MAX { + let expected = table.get(&cp).copied().unwrap_or(cp); + let actual = crate_mapping(name, cp); + if expected != actual && !allowed(name, cp) { + unexpected.push((name.clone(), cp, expected, actual)); + } + } + } + + let mut stale: Vec<(String, u32)> = Vec::new(); + for (name, set) in &skew { + for &cp in set { + let expected = reference + .get(name) + .and_then(|t| t.get(&cp)) + .copied() + .unwrap_or(cp); + if crate_mapping(name, cp) == expected { + stale.push((name.clone(), cp)); + } + } + } + + if !unexpected.is_empty() || !stale.is_empty() { + let mut msg = String::new(); + if !unexpected.is_empty() { + msg.push_str(&format!( + "{} undocumented mapping divergence(s) from CPython:\n", + unexpected.len() + )); + for (name, cp, expected, actual) in unexpected.iter().take(50) { + msg.push_str(&format!( + " {name} U+{cp:04X}: cpython=U+{expected:04X} crate=U+{actual:04X}\n" + )); + } + } + if !stale.is_empty() { + msg.push_str(&format!( + "{} stale version_skew_mappings_cpython3.14.txt entries that now agree:\n", + stale.len() + )); + for (name, cp) in stale.iter().take(50) { + msg.push_str(&format!(" {name} U+{cp:04X}\n")); + } + } + panic!("{msg}"); + } + } +} diff --git a/crates/unicode/tests/generate_reference.py b/crates/unicode/tests/generate_reference.py new file mode 100644 index 00000000000..6a78230de75 --- /dev/null +++ b/crates/unicode/tests/generate_reference.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3.14 +"""Generate the CPython reference dataset for the differential Unicode sweep. + +Run with a CPython interpreter whose ``unicodedata.unidata_version`` matches the +Unicode release this crate targets (16.0.0 for CPython 3.14). The output is a +compact run-length encoding of every predicate's true-set over the full scalar +range, consumed by ``tests/differential.rs``. + +Usage: + python3.14 crates/unicode/tests/generate_reference.py + +Writes ``tests/data/cpython3.14_predicates.txt``. Commit the result. +""" + +from __future__ import annotations + +import _sre +import pathlib +import sys +import unicodedata + +MAX = 0x110000 + +# str predicates: name -> single-char method. +STR_PREDICATES = { + "isalpha": str.isalpha, + "isalnum": str.isalnum, + "isdecimal": str.isdecimal, + "isdigit": str.isdigit, + "isnumeric": str.isnumeric, + "isspace": str.isspace, + "isprintable": str.isprintable, + "isidentifier": str.isidentifier, +} + +# Casing predicates, keyed by the crate function each one exercises. Each is +# sourced from the exact property that function computes: +# * is_lowercase / is_uppercase mirror Py_UNICODE_ISLOWER / ISUPPER, which for a +# single character are str.islower() / str.isupper(). +# * is_titlecase is the Lt general category (NOT str.istitle(), which also +# reports plain uppercase letters as titlecased). +# * is_cased is the Cased property (Py_UNICODE_ISCASED), via _sre. +CASE_PREDICATES = { + "is_lowercase": lambda c: c.islower(), + "is_uppercase": lambda c: c.isupper(), + "is_titlecase": lambda c: unicodedata.category(c) == "Lt", + "is_cased": lambda c: _sre.unicode_iscased(ord(c)), +} + +# Simple one-to-one lowercase mapping (Py_UNICODE_TOLOWER via _sre). This is the +# mapping the regex IGNORECASE path depends on. Emitted as `cp:mapping,...` for +# code points that map to something other than themselves. CPython exposes no +# Python-level simple-uppercase oracle (_sre has unicode_tolower only), so +# toupper is left to the SRE unit tests. +CASE_MAPPINGS = { + "tolower": _sre.unicode_tolower, +} + + +def encode_ranges(is_true) -> list[tuple[int, int]]: + """Collapse the true-set of ``is_true`` into inclusive ``[start, end]`` runs.""" + ranges: list[tuple[int, int]] = [] + start: int | None = None + for cp in range(MAX): + if is_true(cp): + if start is None: + start = cp + elif start is not None: + ranges.append((start, cp - 1)) + start = None + if start is not None: + ranges.append((start, MAX - 1)) + return ranges + + +def main() -> int: + if unicodedata.unidata_version != "16.0.0": + sys.stderr.write( + f"warning: unidata_version is {unicodedata.unidata_version}, " + "expected 16.0.0 (CPython 3.14); regenerating anyway\n" + ) + + data = pathlib.Path(__file__).parent / "data" + data.mkdir(parents=True, exist_ok=True) + + lines = [f"# unidata_version {unicodedata.unidata_version}"] + for name, method in STR_PREDICATES.items(): + ranges = encode_ranges(lambda cp, m=method: m(chr(cp))) + packed = ",".join(f"{s:X}:{e:X}" for s, e in ranges) + lines.append(f"{name} {packed}") + for name, method in CASE_PREDICATES.items(): + ranges = encode_ranges(lambda cp, m=method: m(chr(cp))) + packed = ",".join(f"{s:X}:{e:X}" for s, e in ranges) + lines.append(f"{name} {packed}") + + predicates = data / "cpython3.14_predicates.txt" + predicates.write_text("\n".join(lines) + "\n") + print(f"wrote {predicates} ({predicates.stat().st_size} bytes)") + + mapping_lines = [f"# unidata_version {unicodedata.unidata_version}"] + for name, method in CASE_MAPPINGS.items(): + pairs = [ + f"{cp:X}:{mapped:X}" for cp in range(MAX) if (mapped := method(cp)) != cp + ] + mapping_lines.append(f"{name} {','.join(pairs)}") + + mappings = data / "cpython3.14_mappings.txt" + mappings.write_text("\n".join(mapping_lines) + "\n") + print(f"wrote {mappings} ({mappings.stat().st_size} bytes)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/crates/stdlib/unicode/README.md b/crates/unicode/unicode/README.md similarity index 100% rename from crates/stdlib/unicode/README.md rename to crates/unicode/unicode/README.md diff --git a/crates/stdlib/unicode/latest/DerivedNumericValues.txt b/crates/unicode/unicode/latest/DerivedNumericValues.txt similarity index 100% rename from crates/stdlib/unicode/latest/DerivedNumericValues.txt rename to crates/unicode/unicode/latest/DerivedNumericValues.txt diff --git a/crates/stdlib/unicode/latest/NormalizationCorrections.txt b/crates/unicode/unicode/latest/NormalizationCorrections.txt similarity index 100% rename from crates/stdlib/unicode/latest/NormalizationCorrections.txt rename to crates/unicode/unicode/latest/NormalizationCorrections.txt diff --git a/crates/stdlib/unicode/latest/UnicodeData.txt b/crates/unicode/unicode/latest/UnicodeData.txt similarity index 100% rename from crates/stdlib/unicode/latest/UnicodeData.txt rename to crates/unicode/unicode/latest/UnicodeData.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedBidiClass-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedBidiClass-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedBidiClass-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedBidiClass-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedBinaryProperties-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedCombiningClass-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedCombiningClass-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedCombiningClass-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedCombiningClass-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedEastAsianWidth-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedGeneralCategory-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedNumericType-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedNumericType-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedNumericType-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedNumericType-3.2.0.txt diff --git a/crates/stdlib/unicode/ucd32/DerivedNumericValues-3.2.0.txt b/crates/unicode/unicode/ucd32/DerivedNumericValues-3.2.0.txt similarity index 100% rename from crates/stdlib/unicode/ucd32/DerivedNumericValues-3.2.0.txt rename to crates/unicode/unicode/ucd32/DerivedNumericValues-3.2.0.txt diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 83e41fa1f5f..cc7c8dec2f3 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -26,7 +26,7 @@ ast = ["ruff_python_ast", "ruff_text_size"] codegen = ["rustpython-codegen", "ast"] parser = ["ast"] serde = ["dep:serde"] -wasmbind = ["rustpython-common/wasm_js", "chrono/wasmbind", "wasm-bindgen"] +wasmbind = ["rustpython-common/wasm_js", "jiff/js", "wasm-bindgen"] [dependencies] rustpython-compiler = { workspace = true, optional = true } @@ -42,18 +42,20 @@ ruff_text_size = { workspace = true, optional = true } rustpython-compiler-core = { workspace = true } rustpython-literal = { workspace = true } rustpython-sre_engine = { workspace = true } +rustpython-unicode = { workspace = true } ascii = { workspace = true } bitflags = { workspace = true } bstr = { workspace = true } crossbeam-utils = { workspace = true } -chrono = { workspace = true } constant_time_eq = { workspace = true } flame = { workspace = true, optional = true } hex = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } +itoa = { workspace = true } is-macro = { workspace = true } +jiff = { workspace = true } libc = { workspace = true } log = { workspace = true } malachite-bigint = { workspace = true } @@ -70,6 +72,7 @@ static_assertions = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } thiserror = { workspace = true } +thin-vec = { workspace = true } memchr = { workspace = true } flamer = { workspace = true, optional = true } @@ -77,29 +80,20 @@ half = { workspace = true } psm = { workspace = true } optional = { workspace = true } result-like = { workspace = true } -timsort = { workspace = true } - -## unicode stuff -icu_casemap = { workspace = true } -icu_locale = { workspace = true } -icu_properties = { workspace = true } -writeable = { workspace = true } [target.'cfg(unix)'.dependencies] exitcode = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -rustyline = { workspace = true } -which = { workspace = true } widestring = { workspace = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] wasm-bindgen = { workspace = true, optional = true } [build-dependencies] -chrono = { workspace = true } glob = { workspace = true } itertools = { workspace = true } +jiff = { workspace = true } [lints] workspace = true diff --git a/crates/vm/build.rs b/crates/vm/build.rs index 36e7a5d9d27..2846adfe5f9 100644 --- a/crates/vm/build.rs +++ b/crates/vm/build.rs @@ -3,9 +3,8 @@ reason = "build scripts cannot use rustpython-host_env" )] -use chrono::{Local, prelude::DateTime}; -use core::time::Duration; use itertools::Itertools; +use jiff::{Timestamp, Zoned, tz::TimeZone}; use std::{ env, io::{self, prelude::*}, @@ -123,24 +122,27 @@ fn git_identifier() -> String { } } -fn get_git_timestamp_datetime() -> DateTime { - let timestamp = git_timestamp().parse::().unwrap_or_default(); - let datetime = UNIX_EPOCH + Duration::from_secs(timestamp); - datetime.into() +fn get_git_timestamp_raw() -> Option { + let git_timestamp = git_timestamp().parse::().ok()?; + Timestamp::from_second(git_timestamp).ok() +} + +fn get_git_timestamp_datetime() -> Zoned { + get_git_timestamp_raw().map_or_else(Zoned::now, |timestamp| { + Zoned::new(timestamp, TimeZone::system()) + }) } #[must_use] fn get_git_date() -> String { let datetime = get_git_timestamp_datetime(); - - datetime.format("%b %e %Y").to_string() + datetime.strftime("%b %e %Y").to_string() } #[must_use] fn get_git_time() -> String { let datetime = get_git_timestamp_datetime(); - - datetime.format("%H:%M:%S").to_string() + datetime.strftime("%H:%M:%S").to_string() } fn rustc_version() -> String { diff --git a/crates/vm/src/anystr.rs b/crates/vm/src/anystr.rs index f1c35ecd65f..4896f2789bd 100644 --- a/crates/vm/src/anystr.rs +++ b/crates/vm/src/anystr.rs @@ -1,15 +1,14 @@ +use core::ops::Range; + +use num_traits::{cast::ToPrimitive, sign::Signed}; +use rustpython_unicode::case; + use crate::{ - Py, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine, + AsObject, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine, builtins::{PyIntRef, PyTuple}, convert::TryFromBorrowedObject, function::OptionalOption, }; -use icu_properties::props::{ - BinaryProperty, EnumeratedProperty, GeneralCategory, GeneralCategoryGroup, -}; -use num_traits::{cast::ToPrimitive, sign::Signed}; - -use core::ops::Range; #[derive(FromArgs)] pub struct SplitArgs { @@ -28,7 +27,7 @@ pub struct SplitLinesArgs { #[derive(FromArgs)] pub struct ExpandTabsArgs { #[pyarg(any, default = 8)] - tabsize: isize, + tabsize: i32, } impl ExpandTabsArgs { @@ -133,6 +132,11 @@ where { fn new() -> Self; fn with_capacity(capacity: usize) -> Self; + /// `with_capacity`, reporting a capacity that cannot be allocated instead + /// of aborting the process on it. + fn try_with_capacity(capacity: usize) -> Option + where + Self: Sized; fn push_str(&mut self, s: &S); } @@ -148,7 +152,11 @@ pub(crate) trait AnyStr { fn as_bytes(&self) -> &[u8]; fn elements(&self) -> impl Iterator; fn get_bytes(&self, range: Range) -> &Self; - // FIXME: get_chars is expensive for str + /// The characters in `range`, which for a `str` payload means walking to + /// both bounds -- the payload does not carry the string's character index. + /// `PyStr` therefore converts its own ranges and does not reach the search + /// helpers below through this; what remains are the byte strings, where a + /// character range is already a byte range. fn get_chars(&self, range: Range) -> &Self; fn bytes_len(&self) -> usize; // NOTE: str::chars().count() consumes the O(n) time. But pystr::char_len does cache. @@ -282,27 +290,29 @@ pub(crate) trait AnyStr { } } - fn py_pad(&self, left: usize, right: usize, fillchar: Self::Char) -> Self::Container { - let mut u = Self::Container::with_capacity( - (left + right) * fillchar.bytes_len() + self.bytes_len(), - ); + fn py_pad(&self, left: usize, right: usize, fillchar: Self::Char) -> Option { + let capacity = left + .checked_add(right)? + .checked_mul(fillchar.bytes_len())? + .checked_add(self.bytes_len())?; + let mut u = Self::Container::try_with_capacity(capacity)?; u.extend(core::iter::repeat_n(fillchar, left)); u.push_str(self); u.extend(core::iter::repeat_n(fillchar, right)); - u + Some(u) } - fn py_center(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container { + fn py_center(&self, width: usize, fillchar: Self::Char, len: usize) -> Option { let marg = width - len; let left = marg / 2 + (marg & width & 1); self.py_pad(left, marg - left, fillchar) } - fn py_ljust(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container { + fn py_ljust(&self, width: usize, fillchar: Self::Char, len: usize) -> Option { self.py_pad(0, width - len, fillchar) } - fn py_rjust(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container { + fn py_rjust(&self, width: usize, fillchar: Self::Char, len: usize) -> Option { self.py_pad(width - len, 0, fillchar) } @@ -399,7 +409,7 @@ pub(crate) trait AnyStr { elements } - fn py_zfill(&self, width: isize) -> Vec { + fn py_zfill(&self, width: isize) -> Option> { let width = width.to_usize().unwrap_or(0); let char_len = self.elements().count(); let width = self @@ -422,6 +432,7 @@ pub(crate) trait AnyStr { } lower = true; } + lower } @@ -439,32 +450,29 @@ pub(crate) trait AnyStr { } upper = true; } + upper } // Unified form of CPython functions: // unicode_isupper_impl // unicode_islower_impl - fn is_cased(&self) -> bool - where - VALID: BinaryProperty, - INVALID: BinaryProperty, - { + fn is_cased(&self, valid: fn(char) -> bool, invalid: fn(char) -> bool) -> bool { let mut all_cased = false; for c in self .as_bytes() .utf8_chunks() .flat_map(|c| c.valid().chars()) { - if INVALID::for_char(c) - || GeneralCategoryGroup::TitlecaseLetter.contains(GeneralCategory::for_char(c)) - { + if invalid(c) || case::is_titlecase(c) { return false; } - if !all_cased && VALID::for_char(c) { + + if !all_cased && valid(c) { all_cased = true; } } + all_cased } } @@ -484,18 +492,25 @@ where F: Fn(T) -> PyResult, M: Fn(&PyObject) -> String, { - match obj.try_to_value::(vm) { - Ok(single) => (predicate)(single), - Err(_) => { - let tuple: &Py = obj - .try_to_value(vm) - .map_err(|_| vm.new_type_error((message)(obj)))?; - for obj in tuple { - if single_or_tuple_any(obj, predicate, message, vm)? { - return Ok(true); - } + // _Py_bytes_tailmatch: a tuple is taken apart before anything is converted, and + // each item is converted on its own terms, so a tuple of tuples is not an affix. + if let Some(tuple) = obj.downcast_ref::() { + for item in tuple { + if (predicate)(item.try_to_value::(vm)?)? { + return Ok(true); } - Ok(false) } + return Ok(false); } + + // Only the argument simply being the wrong kind of object is reported as such; + // whatever the conversion itself raised belongs to the caller. + let single = obj.try_to_value::(vm).map_err(|exc| { + if exc.fast_isinstance(vm.ctx.exceptions.type_error) { + vm.new_type_error((message)(obj)) + } else { + exc + } + })?; + (predicate)(single) } diff --git a/crates/vm/src/buffer.rs b/crates/vm/src/buffer.rs index 255250485b1..038e7cae9f3 100644 --- a/crates/vm/src/buffer.rs +++ b/crates/vm/src/buffer.rs @@ -3,17 +3,20 @@ use crate::{ builtins::{PyBaseExceptionRef, PyBytesRef, PyTuple, PyTupleRef, PyTypeRef}, common::{static_cell, str::wchar_t}, convert::ToPyObject, + exceptions, function::{ArgBytesLike, ArgIntoBool, ArgIntoFloat}, }; -use alloc::fmt; -use core::{iter::Peekable, mem}; + +use rustpython_common::wtf8::Wtf8Buf; + +use core::{fmt, iter::Peekable, mem}; use half::f16; use itertools::Itertools; use malachite_bigint::BigInt; use num_traits::{PrimInt, ToPrimitive}; use std::os::raw; -type PackFunc = fn(&VirtualMachine, PyObjectRef, &mut [u8]) -> PyResult<()>; +type PackFunc = fn(&VirtualMachine, FormatType, PyObjectRef, &mut [u8]) -> PyResult<()>; type UnpackFunc = fn(&VirtualMachine, &[u8]) -> PyObjectRef; static OVERFLOW_MSG: &str = "total struct size too long"; // not a const to reduce code size @@ -83,6 +86,7 @@ pub(crate) enum FormatType { UByte = b'B', Char = b'c', WideChar = b'u', + Ucs4Char = b'w', Str = b's', Pascal = b'p', Short = b'h', @@ -187,6 +191,7 @@ impl FormatType { unpack: Some(unpack_char), }, Self::WideChar => native_info!(wchar_t), + Self::Ucs4Char => native_info!(u32), Self::Short => native_info!(raw::c_short), Self::UShort => native_info!(raw::c_ushort), Self::Int => native_info!(raw::c_int), @@ -278,7 +283,7 @@ impl FormatCode { // Check for embedded null character if c == 0 { - return Err("embedded null character".to_owned()); + return Err(exceptions::NulError.to_string()); } // PEP3118: Handle extended format specifiers @@ -342,9 +347,10 @@ impl FormatCode { let code = FormatType::try_from(c) .ok() .filter(|c| match c { - FormatType::SSizeT | FormatType::SizeT | FormatType::VoidP => { - endianness == Endianness::Native - } + FormatType::SSizeT + | FormatType::SizeT + | FormatType::VoidP + | FormatType::Ucs4Char => endianness == Endianness::Native, _ => true, }) .ok_or_else(|| "bad char in struct format".to_owned())?; @@ -484,7 +490,7 @@ impl FormatSpec { let pack = code.info.pack.unwrap(); for arg in args.by_ref().take(code.repeat) { let (item_buf, rest) = buffer.split_at_mut(code.info.size); - pack(vm, arg, item_buf)?; + pack(vm, code.code, arg, item_buf)?; buffer = rest; } } @@ -543,7 +549,12 @@ impl FormatSpec { } trait Packable { - fn pack(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()>; + fn pack( + vm: &VirtualMachine, + code: FormatType, + arg: PyObjectRef, + data: &mut [u8], + ) -> PyResult<()>; fn unpack(vm: &VirtualMachine, data: &[u8]) -> PyObjectRef; } @@ -570,10 +581,11 @@ macro_rules! make_pack_prim_int { impl Packable for $T { fn pack( vm: &VirtualMachine, + code: FormatType, arg: PyObjectRef, data: &mut [u8], ) -> PyResult<()> { - let i: $T = get_int_or_index(vm, arg)?; + let i: $T = get_int_or_index(vm, code, arg)?; i.pack_int::(data); Ok(()) } @@ -586,16 +598,28 @@ macro_rules! make_pack_prim_int { }; } -fn get_int_or_index(vm: &VirtualMachine, arg: PyObjectRef) -> PyResult +fn get_int_or_index(vm: &VirtualMachine, code: FormatType, arg: PyObjectRef) -> PyResult where - T: PrimInt + for<'a> TryFrom<&'a BigInt>, + T: PrimInt + fmt::Display + for<'a> TryFrom<&'a BigInt>, { let index = arg .try_index_opt(vm) .unwrap_or_else(|| Err(new_struct_error(vm, "required argument is not an integer")))?; - index - .try_to_primitive(vm) - .map_err(|_| new_struct_error(vm, "argument out of range")) + index.try_to_primitive(vm).map_err(|_| { + // A pointer is converted rather than checked against the range of a + // named format, so what it reports is the conversion failing. + let msg = if code == FormatType::VoidP { + "int too large to convert".to_owned() + } else { + format!( + "'{}' format requires {} <= number <= {}", + code as u8 as char, + T::min_value(), + T::max_value() + ) + }; + new_struct_error(vm, msg) + }) } make_pack_prim_int!(i8); @@ -610,14 +634,23 @@ make_pack_prim_int!(usize); make_pack_prim_int!(isize); macro_rules! make_pack_float { - ($T:ty) => { + ($T:ty, $fmt:literal) => { impl Packable for $T { fn pack( vm: &VirtualMachine, + _code: FormatType, arg: PyObjectRef, data: &mut [u8], ) -> PyResult<()> { - let f = ArgIntoFloat::try_from_object(vm, arg)?.into_float() as $T; + let f_64 = ArgIntoFloat::try_from_object(vm, arg)?.into_float(); + let f = f_64 as $T; + if f.is_infinite() != f_64.is_infinite() { + return Err(vm.new_overflow_error(concat!( + "float too large to pack with ", + $fmt, + " format" + ))); + } f.to_bits().pack_int::(data); Ok(()) } @@ -630,11 +663,16 @@ macro_rules! make_pack_float { }; } -make_pack_float!(f32); -make_pack_float!(f64); +make_pack_float!(f32, "f"); +make_pack_float!(f64, "d"); impl Packable for f16 { - fn pack(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()> { + fn pack( + vm: &VirtualMachine, + _code: FormatType, + arg: PyObjectRef, + data: &mut [u8], + ) -> PyResult<()> { let f_64 = ArgIntoFloat::try_from_object(vm, arg)?.into_float(); // "from_f64 should be preferred in any non-`const` context" except it gives the wrong result :/ let f_16 = Self::from_f64_const(f_64); @@ -652,8 +690,13 @@ impl Packable for f16 { } impl Packable for *mut raw::c_void { - fn pack(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()> { - usize::pack::(vm, arg, data) + fn pack( + vm: &VirtualMachine, + code: FormatType, + arg: PyObjectRef, + data: &mut [u8], + ) -> PyResult<()> { + usize::pack::(vm, code, arg, data) } fn unpack(vm: &VirtualMachine, rdr: &[u8]) -> PyObjectRef { @@ -662,7 +705,12 @@ impl Packable for *mut raw::c_void { } impl Packable for bool { - fn pack(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()> { + fn pack( + vm: &VirtualMachine, + _code: FormatType, + arg: PyObjectRef, + data: &mut [u8], + ) -> PyResult<()> { let v = ArgIntoBool::try_from_object(vm, arg)?.into_bool() as u8; v.pack_int::(data); Ok(()) @@ -674,7 +722,12 @@ impl Packable for bool { } } -fn pack_char(vm: &VirtualMachine, arg: PyObjectRef, data: &mut [u8]) -> PyResult<()> { +fn pack_char( + vm: &VirtualMachine, + _code: FormatType, + arg: PyObjectRef, + data: &mut [u8], +) -> PyResult<()> { let v = PyBytesRef::try_from_object(vm, arg)?; let ch = *v .as_bytes() @@ -736,9 +789,8 @@ pub fn struct_error_type(vm: &VirtualMachine) -> &'static PyTypeRef { INSTANCE.get_or_init(|| vm.ctx.new_exception_type("struct", "error", None)) } -pub fn new_struct_error(vm: &VirtualMachine, msg: impl Into) -> PyBaseExceptionRef { +pub fn new_struct_error>(vm: &VirtualMachine, msg: T) -> PyBaseExceptionRef { // can't just STRUCT_ERROR.get().unwrap() cause this could be called before from buffer // machinery, independent of whether _struct was ever imported - let msg: String = msg.into(); vm.new_exception_msg(struct_error_type(vm).clone(), msg.into()) } diff --git a/crates/vm/src/builtins/asyncgenerator.rs b/crates/vm/src/builtins/asyncgenerator.rs index dea40062a9a..7ea43f389c6 100644 --- a/crates/vm/src/builtins/asyncgenerator.rs +++ b/crates/vm/src/builtins/asyncgenerator.rs @@ -5,7 +5,7 @@ use crate::{ class::PyClassImpl, common::lock::PyMutex, coroutine::{Coro, warn_deprecated_throw_signature}, - frame::FrameRef, + frame::FrameObjectRef, function::OptionalArg, object::{Traverse, TraverseFn}, protocol::PyIterReturn, @@ -50,7 +50,7 @@ impl PyAsyncGen { } #[must_use] - pub fn new(frame: FrameRef, name: PyStrRef, qualname: PyStrRef) -> Self { + pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { Self { inner: Coro::new(frame, name, qualname), running_async: AtomicCell::new(false), @@ -127,7 +127,7 @@ impl PyAsyncGen { self.inner.frame().yield_from_target() } #[pygetset] - fn ag_frame(&self, _vm: &VirtualMachine) -> Option { + fn ag_frame(&self, _vm: &VirtualMachine) -> Option { if self.inner.closed() { None } else { @@ -140,11 +140,15 @@ impl PyAsyncGen { } #[pygetset] fn ag_code(&self, _vm: &VirtualMachine) -> PyRef { - self.inner.frame().code.clone() + self.inner.frame().iframe().code().to_owned() } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -688,7 +692,8 @@ impl PyAnextAwaitable { && generator .as_coro() .frame() - .code + .iframe() + .code() .flags .contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE) { diff --git a/crates/vm/src/builtins/bool.rs b/crates/vm/src/builtins/bool.rs index 4bb980d71a2..1cfa8cc27ee 100644 --- a/crates/vm/src/builtins/bool.rs +++ b/crates/vm/src/builtins/bool.rs @@ -34,6 +34,7 @@ impl<'a> TryFromBorrowedObject<'a> for bool { impl PyObjectRef { /// Convert Python bool into Rust bool. + #[inline(always)] pub fn try_to_bool(self, vm: &VirtualMachine) -> PyResult { if self.is(&vm.ctx.true_value) { return Ok(true); @@ -41,6 +42,12 @@ impl PyObjectRef { return Ok(false); } + self.try_to_bool_slow(vm) + } + + #[cold] + #[inline(never)] + fn try_to_bool_slow(self, vm: &VirtualMachine) -> PyResult { let slots = &self.class().slots; // 1. Try nb_bool slot first diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index d3195aa0eab..b34447b79bf 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -157,7 +157,7 @@ impl PyNativeFunction { // m_self is an instance: use Py_TYPE(m_self).__qualname__ bound.class().name().to_string() }; - vm.ctx.new_str(format!("{}.{}", prefix, &zelf.value.name)) + vm.ctx.new_str(format!("{}.{}", prefix, zelf.value.name)) } else { vm.ctx.intern_str(zelf.value.name).to_owned() }; @@ -220,7 +220,7 @@ impl fmt::Debug for PyNativeMethod { f, "builtin method of {:?} with {:?}", &*self.class.name(), - &self.func + self.func ) } } @@ -247,9 +247,9 @@ fn vectorcall_native_function( let mut all_args = Vec::with_capacity(args.len() + 1); all_args.push(self_obj); all_args.extend(args); - FuncArgs::from_vectorcall(&all_args, nargs + 1, kwnames) + FuncArgs::from_vectorcall_owned(all_args, nargs + 1, kwnames) } else { - FuncArgs::from_vectorcall(&args, nargs, kwnames) + FuncArgs::from_vectorcall_owned(args, nargs, kwnames) }; (zelf.value.func)(vm, func_args) diff --git a/crates/vm/src/builtins/bytearray.rs b/crates/vm/src/builtins/bytearray.rs index 63b10552f50..93046b4932e 100644 --- a/crates/vm/src/builtins/bytearray.rs +++ b/crates/vm/src/builtins/bytearray.rs @@ -1,7 +1,7 @@ //! Implementation of the python bytearray object. use super::{ - PositionIterInternal, PyBytes, PyBytesRef, PyDictRef, PyGenericAlias, PyIntRef, PyStrRef, - PyTuple, PyTupleRef, PyType, PyTypeRef, iter::builtins_iter, + PositionIterInternal, PyBytes, PyDictRef, PyGenericAlias, PyStrRef, PyTuple, PyTupleRef, + PyType, PyTypeRef, iter::builtins_iter, }; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, @@ -10,8 +10,9 @@ use crate::{ atomic_func, byte::{bytes_from_object, value_from_object}, bytes_inner::{ - ByteInnerFindOptions, ByteInnerNewOptions, ByteInnerPaddingOptions, ByteInnerSplitOptions, - ByteInnerTranslateOptions, DecodeArgs, PyBytesInner, bytes_decode, + ByteInnerFindOptions, ByteInnerHexOptions, ByteInnerNewOptions, ByteInnerPaddingOptions, + ByteInnerSplitOptions, ByteInnerSub, ByteInnerTranslateOptions, DecodeArgs, PyBytesInner, + bytes_decode, }, class::PyClassImpl, common::{ @@ -23,10 +24,10 @@ use crate::{ }, convert::{ToPyObject, ToPyResult}, function::{ - ArgBytesLike, ArgIterable, ArgSize, Either, OptionalArg, OptionalOption, PyComparisonValue, + ArgBytesLike, ArgIterable, ArgSize, OptionalArg, OptionalOption, PyComparisonValue, }, protocol::{ - BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, + BufferDescriptor, BufferFlags, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods, }, sliceable::{SequenceIndex, SliceableSequenceMutOp, SliceableSequenceOp}, @@ -228,11 +229,8 @@ impl PyByteArray { self.inner().add(&other.borrow_buf()).into() } - fn __contains__( - &self, - needle: Either, - vm: &VirtualMachine, - ) -> PyResult { + fn __contains__(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let needle = ByteInnerSub::from_contains_arg(needle, vm)?; self.inner().contains(needle, vm) } @@ -321,12 +319,8 @@ impl PyByteArray { } #[pymethod] - fn hex( - &self, - sep: OptionalArg>, - bytes_per_sep: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { + fn hex(&self, options: ByteInnerHexOptions, vm: &VirtualMachine) -> PyResult { + let ByteInnerHexOptions { sep, bytes_per_sep } = options; self.inner().hex(sep, bytes_per_sep, vm) } @@ -360,7 +354,10 @@ impl PyByteArray { #[pymethod] fn join(&self, iter: ArgIterable, vm: &VirtualMachine) -> PyResult { - Ok(self.inner().join(iter, vm)?.into()) + // Driving the iterable runs Python, which can reach this bytearray, + // so the separator is taken by value rather than left borrowed. + let separator = self.inner().clone(); + Ok(separator.join(iter, vm)?.into()) } #[pymethod] @@ -503,8 +500,8 @@ impl PyByteArray { } #[pymethod] - fn zfill(&self, width: isize) -> Self { - self.inner().zfill(width).into() + fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult { + Ok(self.inner().zfill(width, vm)?.into()) } #[pymethod] @@ -538,7 +535,10 @@ impl PyByteArray { } fn __mod__(&self, values: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let formatted = self.inner().cformat(values, vm)?; + // Formatting calls the values' conversion methods, which can reach + // this bytearray, so the format is taken by value. + let format = self.inner().clone(); + let formatted = format.cformat(values, vm)?; Ok(formatted.into()) } @@ -558,7 +558,11 @@ impl PyByteArray { // TODO: Uncomment when Python adds __class_getitem__ to bytearray // #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -613,12 +617,34 @@ impl Py { #[pymethod] fn extend(&self, object: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { if self.is(&object) { - PyByteArray::irepeat(self, 2, vm) - } else { - let items = bytes_from_object(vm, &object)?; - self.try_resizable(vm)?.elements.extend(items); - Ok(()) + return PyByteArray::irepeat(self, 2, vm); } + // bytearray_setslice keeps the export alive across the resize, so a value + // looking at this bytearray is what stops it from growing. + let buffer = object + .check_buffer() + .then(|| { + PyBuffer::from_object(vm, &object, BufferFlags::SIMPLE).map_err(|_| { + // What an exporter refuses to hand out leaves the value simply + // not usable here, whatever the exporter's own complaint was. + vm.new_type_error(format!( + "can't set bytearray slice from {}", + object.class().name() + )) + }) + }) + .transpose()?; + let items = match &buffer { + Some(buffer) => buffer + .as_contiguous() + .ok_or_else(|| { + vm.new_buffer_error("non-contiguous buffer is not a bytes-like object") + })? + .to_vec(), + None => bytes_from_object(vm, &object)?, + }; + self.try_resizable(vm)?.elements.extend(items); + Ok(()) } #[pymethod] @@ -731,6 +757,20 @@ static BUFFER_METHODS: BufferMethods = BufferMethods { }; impl AsBuffer for PyByteArray { + const RELEASE_BUFFER: bool = true; + + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + let zelf = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("unexpected payload for as_buffer"))?; + flags.fill_info_check(false, vm)?; + Self::as_buffer(zelf, vm) + } + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { Ok(PyBuffer::new( zelf.to_owned().into(), @@ -744,8 +784,9 @@ impl BufferResizeGuard for PyByteArray { type Resizable<'a> = PyRwLockWriteGuard<'a, PyBytesInner>; fn try_resizable_opt(&self) -> Option> { - let w = self.inner.write(); - (self.exports.load(Ordering::SeqCst) == 0).then_some(w) + // An export is a borrow someone else still holds, so it is answered + // before the lock rather than by waiting on it. + (self.exports.load(Ordering::SeqCst) == 0).then(|| self.inner.write()) } } @@ -801,9 +842,7 @@ impl AsSequence for PyByteArray { } }), contains: atomic_func!(|seq, other, vm| { - let other = - >::try_from_object(vm, other.to_owned())?; - PyByteArray::sequence_downcast(seq).__contains__(other, vm) + PyByteArray::sequence_downcast(seq).__contains__(other.to_owned(), vm) }), inplace_concat: atomic_func!(|seq, other, vm| { let other = ArgBytesLike::try_from_object(vm, other.to_owned())?; diff --git a/crates/vm/src/builtins/bytes.rs b/crates/vm/src/builtins/bytes.rs index f28e8ddbbd8..48c0e431229 100644 --- a/crates/vm/src/builtins/bytes.rs +++ b/crates/vm/src/builtins/bytes.rs @@ -1,27 +1,28 @@ use super::{ - PositionIterInternal, PyDictRef, PyGenericAlias, PyIntRef, PyStrRef, PyTuple, PyTupleRef, - PyType, PyTypeRef, iter::builtins_iter, + PositionIterInternal, PyDictRef, PyGenericAlias, PyStrRef, PyTuple, PyTupleRef, PyType, + PyTypeRef, iter::builtins_iter, }; use crate::common::lock::LazyLock; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, - TryFromBorrowedObject, TryFromObject, VirtualMachine, + TryFromBorrowedObject, VirtualMachine, anystr::{self, AnyStr}, atomic_func, bytes_inner::{ - ByteInnerFindOptions, ByteInnerNewOptions, ByteInnerPaddingOptions, ByteInnerSplitOptions, - ByteInnerTranslateOptions, DecodeArgs, PyBytesInner, bytes_decode, + ByteInnerFindOptions, ByteInnerHexOptions, ByteInnerNewOptions, ByteInnerPaddingOptions, + ByteInnerSplitOptions, ByteInnerSub, ByteInnerTranslateOptions, DecodeArgs, PyBytesInner, + bytes_decode, }, class::PyClassImpl, common::{hash::PyHash, lock::PyMutex}, convert::{ToPyObject, ToPyResult}, function::{ - ArgBytesLike, ArgIndex, ArgIterable, Either, FuncArgs, OptionalArg, OptionalOption, + ArgBytesLike, ArgIndex, ArgIterable, FuncArgs, OptionalArg, OptionalOption, PyComparisonValue, }, protocol::{ - BufferDescriptor, BufferMethods, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, - PySequenceMethods, + BufferDescriptor, BufferFlags, BufferMethods, PyBuffer, PyIterReturn, PyMappingMethods, + PyNumberMethods, PySequenceMethods, }, sliceable::{SequenceIndex, SliceableSequenceOp}, types::{ @@ -31,6 +32,7 @@ use crate::{ }; use bstr::ByteSlice; use core::{mem::size_of, ops::Deref}; +use memchr::memchr; #[pyclass(module = false, name = "bytes")] #[derive(Clone, Debug)] @@ -169,6 +171,13 @@ impl PyBytes { .map(|x| vm.ctx.new_bytes(x).into()), } } + + /// Check bytes for interior NULs. + #[inline] + #[must_use] + pub fn contains_nuls(&self) -> bool { + memchr(b'\0', self.as_bytes()).is_some() + } } impl PyRef { @@ -218,7 +227,7 @@ impl PyBytes { #[inline] #[must_use] - pub fn as_bytes(&self) -> &[u8] { + pub const fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() } @@ -238,11 +247,8 @@ impl PyBytes { self.inner.add(&other.borrow_buf()) } - fn __contains__( - &self, - needle: Either, - vm: &VirtualMachine, - ) -> PyResult { + fn __contains__(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let needle = ByteInnerSub::from_contains_arg(needle, vm)?; self.inner.contains(needle, vm) } @@ -318,10 +324,10 @@ impl PyBytes { #[pymethod] pub(crate) fn hex( &self, - sep: OptionalArg>, - bytes_per_sep: OptionalArg, + options: ByteInnerHexOptions, vm: &VirtualMachine, ) -> PyResult { + let ByteInnerHexOptions { sep, bytes_per_sep } = options; self.inner.hex(sep, bytes_per_sep, vm) } @@ -499,8 +505,8 @@ impl PyBytes { } #[pymethod] - fn zfill(&self, width: isize) -> Self { - self.inner.zfill(width).into() + fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult { + Ok(self.inner.zfill(width, vm)?.into()) } #[pymethod] @@ -536,7 +542,11 @@ impl PyBytes { // TODO: Uncomment when Python adds __class_getitem__ to bytes // #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -615,6 +625,18 @@ static BUFFER_METHODS: BufferMethods = BufferMethods { }; impl AsBuffer for PyBytes { + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + let zelf = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("unexpected payload for as_buffer"))?; + flags.fill_info_check(true, vm)?; + Self::as_buffer(zelf, vm) + } + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { let buf = PyBuffer::new( zelf.to_owned().into(), @@ -661,9 +683,7 @@ impl AsSequence for PyBytes { .map(|x| vm.ctx.new_bytes(vec![x]).into()) }), contains: atomic_func!(|seq, other, vm| { - let other = - >::try_from_object(vm, other.to_owned())?; - PyBytes::sequence_downcast(seq).__contains__(other, vm) + PyBytes::sequence_downcast(seq).__contains__(other.to_owned(), vm) }), ..PySequenceMethods::NOT_IMPLEMENTED }); diff --git a/crates/vm/src/builtins/capsule.rs b/crates/vm/src/builtins/capsule.rs index 43efa0fb214..19260dba82f 100644 --- a/crates/vm/src/builtins/capsule.rs +++ b/crates/vm/src/builtins/capsule.rs @@ -76,6 +76,9 @@ impl Representable for PyCapsule { impl Destructor for PyCapsule { fn del(zelf: &Py, _vm: &VirtualMachine) -> PyResult<()> { + if zelf.pointer().is_null() { + return Ok(()); + } if let Some(destructor) = zelf.destructor() { unsafe { destructor(zelf.as_object().as_raw().cast_mut()) }; } diff --git a/crates/vm/src/builtins/classmethod.rs b/crates/vm/src/builtins/classmethod.rs index a6c69eb50c7..26dcd251251 100644 --- a/crates/vm/src/builtins/classmethod.rs +++ b/crates/vm/src/builtins/classmethod.rs @@ -27,7 +27,7 @@ use crate::{ /// /// Class methods are different than C++ or Java static methods. /// If you want those, see the staticmethod builtin. -#[pyclass(module = false, name = "classmethod")] +#[pyclass(module = false, name = "classmethod", traverse)] #[derive(Debug)] pub struct PyClassMethod { callable: PyMutex, @@ -187,7 +187,11 @@ impl PyClassMethod { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -195,7 +199,7 @@ impl PyClassMethod { impl Representable for PyClassMethod { #[inline] fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock().repr(vm).unwrap(); + let callable = zelf.callable.lock().repr(vm)?; let class = Self::class(&vm.ctx); let repr = match ( diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index e30813bf87d..b25c9d5c499 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -471,6 +471,11 @@ pub struct PyCode { pub monitoring_data: PyMutex>, /// Whether adaptive counters have been initialized (lazy quickening). pub quickened: core::sync::atomic::AtomicBool, + /// Whether the bytecode contains any instruction that mutates the current + /// exc_info slot (`vm.set_exception`). When false, a normal frame call for + /// this code cannot leave the slot unbalanced, so `with_frame` skips the + /// exc_info save/restore. Computed once by scanning the instruction stream. + pub has_exc_handling: bool, } impl Deref for PyCode { @@ -483,12 +488,26 @@ impl Deref for PyCode { impl PyCode { pub fn new(code: CodeObject) -> Self { let sp = code.source_path as *const PyStrInterned as *mut PyStrInterned; + // The only opcodes that call `vm.set_exception` (mutating the shared + // exc_info slot); instrumented variants only replace these base opcodes + // in place, so scanning the freshly-built stream is a sound predicate. + let has_exc_handling = code.instructions.iter().any(|u| { + matches!( + u.op, + Instruction::PushExcInfo + | Instruction::PopExcept + | Instruction::CheckEgMatch + | Instruction::EndAsyncFor + | Instruction::InstrumentedEndAsyncFor + ) + }); Self { code, source_path: AtomicPtr::new(sp), instrumentation_version: AtomicU64::new(0), monitoring_data: PyMutex::new(None), quickened: core::sync::atomic::AtomicBool::new(false), + has_exc_handling, } } @@ -1347,19 +1366,25 @@ impl PyCode { OptionalArg::Missing => self.code.instructions.clone(), }; + let intern_all = |objs: Vec, field: &str| -> PyResult> { + objs.into_iter() + .map(|o| { + let s = o.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!("{field} must be a tuple of strings")) + })?; + Ok(vm.ctx.intern_str(s.as_wtf8())) + }) + .collect::>>() + .map(Vec::into_boxed_slice) + }; + let cellvars = match co_cellvars { - OptionalArg::Present(cellvars) => cellvars - .into_iter() - .map(|o| o.as_interned_str(vm).unwrap()) - .collect(), + OptionalArg::Present(cellvars) => intern_all(cellvars, "co_cellvars")?, OptionalArg::Missing => self.code.cellvars.clone(), }; let freevars = match co_freevars { - OptionalArg::Present(freevars) => freevars - .into_iter() - .map(|o| o.as_interned_str(vm).unwrap()) - .collect(), + OptionalArg::Present(freevars) => intern_all(freevars, "co_freevars")?, OptionalArg::Missing => self.code.freevars.clone(), }; @@ -1392,10 +1417,10 @@ impl PyCode { posonlyarg_count, arg_count, kwonlyarg_count, - source_path: source_path.as_object().as_interned_str(vm).unwrap(), + source_path: vm.ctx.intern_str(source_path.as_wtf8()), first_line_number, - obj_name: obj_name.as_object().as_interned_str(vm).unwrap(), - qualname: qualname.as_object().as_interned_str(vm).unwrap(), + obj_name: vm.ctx.intern_str(obj_name.as_wtf8()), + qualname: vm.ctx.intern_str(qualname.as_wtf8()), max_stackdepth, instructions, @@ -1403,14 +1428,8 @@ impl PyCode { // It can be removed once we move every other code to use linetable only. locations: self.code.locations.clone(), constants: constants.into_iter().map(Literal).collect(), - names: names - .into_iter() - .map(|o| o.as_interned_str(vm).unwrap()) - .collect(), - varnames: varnames - .into_iter() - .map(|o| o.as_interned_str(vm).unwrap()) - .collect(), + names: intern_all(names, "co_names")?, + varnames: intern_all(varnames, "co_varnames")?, cellvars, freevars, localspluskinds: self.code.localspluskinds.clone(), diff --git a/crates/vm/src/builtins/coroutine.rs b/crates/vm/src/builtins/coroutine.rs index 1780370b43c..0fc50fb1356 100644 --- a/crates/vm/src/builtins/coroutine.rs +++ b/crates/vm/src/builtins/coroutine.rs @@ -3,7 +3,7 @@ use crate::{ AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, coroutine::{Coro, warn_deprecated_throw_signature}, - frame::FrameRef, + frame::FrameObjectRef, function::OptionalArg, object::{Traverse, TraverseFn}, protocol::PyIterReturn, @@ -41,7 +41,7 @@ impl PyCoroutine { } #[must_use] - pub fn new(frame: FrameRef, name: PyStrRef, qualname: PyStrRef) -> Self { + pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { Self { inner: Coro::new(frame, name, qualname), } @@ -80,7 +80,7 @@ impl PyCoroutine { self.inner.frame().yield_from_target() } #[pygetset] - fn cr_frame(&self, _vm: &VirtualMachine) -> Option { + fn cr_frame(&self, _vm: &VirtualMachine) -> Option { if self.inner.closed() { None } else { @@ -93,7 +93,7 @@ impl PyCoroutine { } #[pygetset] fn cr_code(&self, _vm: &VirtualMachine) -> PyRef { - self.inner.frame().code.clone() + self.inner.frame().iframe().code().to_owned() } // TODO: coroutine origin tracking: // https://docs.python.org/3/library/sys.html#sys.set_coroutine_origin_tracking_depth @@ -103,7 +103,11 @@ impl PyCoroutine { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 350adf6d768..50bdc841abf 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -79,7 +79,10 @@ impl GetDescriptor for PyMethodDescriptor { let bound = match obj { Some(obj) => { if descr.method.flags.contains(PyMethodFlags::METHOD) { - if cls.is_some_and(|c| c.fast_isinstance(vm.ctx.types.type_type)) { + if cls + .as_ref() + .is_none_or(|c| c.fast_isinstance(vm.ctx.types.type_type)) + { obj } else { return Err(vm.new_type_error(format!( @@ -127,7 +130,7 @@ impl PyMethodDescriptor { #[pygetset] fn __qualname__(&self) -> String { - format!("{}.{}", self.common.typ.name(), &self.common.name) + format!("{}.{}", self.common.typ.name(), self.common.name) } #[pygetset] @@ -164,7 +167,7 @@ impl Representable for PyMethodDescriptor { fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { Ok(format!( "", - &zelf.method.name, + zelf.method.name, zelf.common.typ.name() )) } @@ -539,6 +542,10 @@ pub enum SlotFunc { NumBinaryRight(PyNumberBinaryFunc), // __radd__, __rsub__, etc. (swapped args) NumTernary(PyNumberTernaryFunc), // __pow__ NumTernaryRight(PyNumberTernaryFunc), // __rpow__ (swapped first two args) + + // Buffer protocol + GetBuffer(crate::types::AsBufferFunc), // __buffer__ + ReleaseBuffer, // __release_buffer__ } impl core::fmt::Debug for SlotFunc { @@ -579,6 +586,8 @@ impl core::fmt::Debug for SlotFunc { Self::NumBinaryRight(_) => write!(f, "SlotFunc::NumBinaryRight(...)"), Self::NumTernary(_) => write!(f, "SlotFunc::NumTernary(...)"), Self::NumTernaryRight(_) => write!(f, "SlotFunc::NumTernaryRight(...)"), + Self::GetBuffer(_) => write!(f, "SlotFunc::GetBuffer(...)"), + Self::ReleaseBuffer => write!(f, "SlotFunc::ReleaseBuffer"), } } } @@ -755,10 +764,41 @@ impl SlotFunc { let z = z.unwrap_or_else(|| vm.ctx.none()); func(&y, &obj, &z, vm) // Swapped: y ** obj % z } + // Buffer protocol + Self::GetBuffer(func) => { + let (flags_obj,): (PyObjectRef,) = args.bind(vm)?; + let buffer = func(&obj, parse_buffer_flags(flags_obj, vm)?, vm)?; + crate::builtins::PyMemoryView::from_buffer(buffer, vm) + .map(|mv| mv.into_pyobject(vm)) + } + Self::ReleaseBuffer => { + let (mv_obj,): (PyObjectRef,) = args.bind(vm)?; + let mv = mv_obj + .downcast::() + .map_err(|_| vm.new_type_error("expected a memoryview object"))?; + crate::builtins::memory::release_buffer_from_python(&obj, mv, vm)?; + Ok(vm.ctx.none()) + } } } } +/// Parse the `flags` argument of `__buffer__`. wrap_buffer +fn parse_buffer_flags( + arg: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + use num_traits::ToPrimitive; + let idx = arg.try_index(vm)?; + let flags = idx + .as_bigint() + .to_isize() + .ok_or_else(|| vm.new_overflow_error("cannot fit 'int' into an index-sized integer"))?; + let flags = + i32::try_from(flags).map_err(|_| vm.new_overflow_error("buffer flags out of range"))?; + Ok(crate::protocol::BufferFlags::from_bits_retain(flags as u32)) +} + /// wrapper_descriptor: wraps a slot function as a Python method // = PyWrapperDescrObject #[pyclass(name = "wrapper_descriptor", module = false)] diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index db97045f07c..d2b9dea31fa 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -1,19 +1,15 @@ use super::{ IterStatus, PositionIterInternal, PyBaseExceptionRef, PyGenericAlias, PyMappingProxy, PySet, - PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, set::PySetInner, + PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, set, set::PySetInner, }; use crate::common::lock::LazyLock; use crate::object::{Traverse, TraverseFn}; use crate::{ AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, TryFromObject, atomic_func, - builtins::{ - PyTuple, - iter::{builtins_iter, builtins_reversed}, - type_::PyAttributes, - }, + builtins::{PyList, PyTuple, iter::builtins_iter, type_::PyAttributes}, class::{PyClassDef, PyClassImpl}, - common::ascii, + common::{ascii, hash::PyHash}, dict_inner::{self, DictKey}, function::{ArgIterable, FuncArgs, KwArgs, OptionalArg, PyArithmeticValue, PyComparisonValue}, iter::PyExactSizeIterator, @@ -118,11 +114,6 @@ impl PyDict { &self.entries } - /// Monotonically increasing version for mutation tracking. - pub(crate) fn version(&self) -> u64 { - self.entries.version() - } - /// Returns all keys as a Vec, atomically under a single read lock. /// Thread-safe: prevents "dictionary changed size during iteration" errors. pub fn keys_vec(&self) -> Vec { @@ -187,6 +178,76 @@ impl PyDict { self.merge_object_with_override(other, false, vm) } + fn add_update_sequence_note( + exc: PyBaseExceptionRef, + index: usize, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + if !exc.fast_isinstance(vm.ctx.exceptions.type_error) { + return exc; + } + + let note = + format!("Cannot convert dictionary update sequence element #{index} to a sequence"); + match vm.call_method(exc.as_object(), "add_note", (vm.ctx.new_str(note),)) { + Ok(_) => exc, + Err(note_err) => { + note_err.set___context__(Some(exc)); + note_err + } + } + } + + fn update_sequence_pair_from_slice( + elements: &[PyObjectRef], + index: usize, + vm: &VirtualMachine, + ) -> PyResult<(PyObjectRef, PyObjectRef)> { + let [key, value] = elements else { + return Err(vm.new_value_error(format!( + "dictionary update sequence element #{index} has length {}; 2 is required", + elements.len() + ))); + }; + Ok((key.clone(), value.clone())) + } + + fn update_sequence_pair( + element: PyObjectRef, + index: usize, + vm: &VirtualMachine, + ) -> PyResult<(PyObjectRef, PyObjectRef)> { + let element = match element.downcast_exact::(vm) { + Ok(list) => { + let elements = list.borrow_vec(); + return Self::update_sequence_pair_from_slice(&elements, index, vm); + } + Err(element) => element, + }; + let element = match element.downcast_exact::(vm) { + Ok(tuple) => { + return Self::update_sequence_pair_from_slice(tuple.as_slice(), index, vm); + } + Err(element) => element, + }; + + let elements = (|| { + let elem_iter = element.get_iter(vm).map_err(|exc| { + if exc.fast_isinstance(vm.ctx.exceptions.type_error) { + vm.new_type_error("object is not iterable") + } else { + exc + } + })?; + elem_iter + .into_iter::(vm)? + .collect::>>() + })() + .map_err(|exc| Self::add_update_sequence_note(exc, index, vm))?; + + Self::update_sequence_pair_from_slice(&elements, index, vm) + } + pub fn merge_from_seq2( &self, seq2: PyObjectRef, @@ -195,20 +256,10 @@ impl PyDict { ) -> PyResult<()> { let iter = seq2.get_iter(vm)?; let dict = &self.entries; - loop { - fn err(vm: &VirtualMachine) -> PyBaseExceptionRef { - vm.new_value_error("Iterator must have exactly two elements") - } - let element = match iter.next(vm)? { - PyIterReturn::Return(obj) => obj, - PyIterReturn::StopIteration(_) => break, - }; - let elem_iter = element.get_iter(vm)?; - let key = elem_iter.next(vm)?.into_result().map_err(|_| err(vm))?; - let value = elem_iter.next(vm)?.into_result().map_err(|_| err(vm))?; - if matches!(elem_iter.next(vm)?, PyIterReturn::Return(_)) { - return Err(err(vm)); - } + + for (index, element) in iter.iter_without_hint::(vm)?.enumerate() { + let (key, value) = Self::update_sequence_pair(element?, index, vm)?; + if !override_existing && dict.contains(vm, &*key)? { continue; } @@ -298,6 +349,20 @@ impl PyDict { ) -> PyResult> { self.entries.get(vm, key) } + + /// Keys of `obj` with their stored hashes, or `None` if it must be iterated + /// generically. Only exact dicts and sets qualify, as in CPython's + /// `_PyDict_FromKeys`: a subclass may override `__iter__`. + fn fromkeys_known_hashes( + obj: &PyObject, + vm: &VirtualMachine, + ) -> Option> { + if let Some(dict) = obj.downcast_ref_if_exact::(vm) { + Some(dict.entries.keys_with_hashes()) + } else { + set::exact_set_keys_with_hashes(obj, vm) + } + } } // Python dict methods: @@ -328,8 +393,16 @@ impl PyDict { let d = PyType::call(&class, ().into(), vm)?; match d.downcast_exact::(vm) { Ok(pydict) => { - for key in iterable.iter(vm)? { - pydict.__setitem__(key?, value.clone(), vm)?; + if let Some(keys) = Self::fromkeys_known_hashes(iterable.as_object(), vm) { + for (key, hash) in keys { + pydict + .entries + .insert_known_hash(vm, &*key, hash, value.clone())?; + } + } else { + for key in iterable.iter(vm)? { + pydict.__setitem__(key?, value.clone(), vm)?; + } } Ok(pydict.into_pyref().into()) } @@ -387,7 +460,7 @@ impl PyDict { } #[pymethod] - fn setdefault( + pub(crate) fn setdefault( &self, key: PyObjectRef, default: OptionalArg, @@ -406,7 +479,7 @@ impl PyDict { } #[pymethod] - fn update( + pub(crate) fn update( &self, dict_obj: OptionalArg, kwargs: KwArgs, @@ -457,7 +530,11 @@ impl PyDict { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -739,17 +816,71 @@ impl Py { } } - /// Fast lookup using a cached entry index hint. - pub(crate) fn get_item_opt_hint( + /// Read a cached exact-dict entry after validating its key-layout stamp. + #[inline] + pub(crate) fn get_item_by_index_and_keys_version( + &self, + version: u16, + index: u16, + ) -> Option { + self.entries + .get_index_if_keys_version(u32::from(version), usize::from(index)) + } + + /// Lookup trying a cached entry index hint first. + /// + /// When the hint misses but the key is present, also returns a refreshed + /// hint (`None` when the hint hit or no hint is representable). + pub(crate) fn get_item_opt_refresh_hint( &self, key: &K, hint: u16, vm: &VirtualMachine, - ) -> PyResult> { + ) -> PyResult)>> { + if self.exact_dict(vm) { + if let Some(value) = self.entries.get_hint(vm, key, usize::from(hint))? { + return Ok(Some((value, None))); + } + self.entries.get_with_hint(vm, key) + } else { + Ok(self.get_item_opt(key, vm)?.map(|value| (value, None))) + } + } + + /// Store using a cached entry index hint for the value-replace fast path. + /// + /// On a hint miss, returns a refreshed hint for the key (`None` when the + /// hint hit or no hint is representable). + pub(crate) fn set_item_with_hint( + &self, + key: &K, + hint: u16, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + if self.exact_dict(vm) { + self.entries + .insert_with_hint(vm, key, usize::from(hint), value) + } else { + self.as_object().set_item(key, value, vm)?; + Ok(None) + } + } + + /// Current keys-version stamp of the underlying storage (0 if unset). + pub(crate) fn keys_version(&self) -> u32 { + self.entries.keys_version() + } + + /// Current keys-version stamp, assigning one if none is set. + /// + /// Returns 0 for dict subclasses: their lookup can be overridden, so a + /// key-set attestation on the raw storage must never be cached for them. + pub(crate) fn assign_keys_version(&self, vm: &VirtualMachine) -> u32 { if self.exact_dict(vm) { - self.entries.get_hint(vm, key, usize::from(hint)) + self.entries.assign_keys_version() } else { - self.get_item_opt(key, vm) + 0 } } @@ -959,6 +1090,7 @@ macro_rules! dict_view { $class_name: literal, $iter_class_name: literal, $reverse_iter_class_name: literal, + $project_fn: expr, $result_fn: expr ) => { #[pyclass(module = false, name = $class_name)] @@ -981,7 +1113,7 @@ macro_rules! dict_view { } fn item(vm: &VirtualMachine, key: PyObjectRef, value: PyObjectRef) -> PyObjectRef { - $result_fn(vm, key, value) + $result_fn(vm, $project_fn(&key, &value)) } fn __reversed__(&self) -> Self::ReverseIter { @@ -1061,10 +1193,17 @@ macro_rules! dict_view { let iter = builtins_iter(vm); let internal = self.internal.lock(); let entries = match &internal.status { - IterStatus::Active(dict) => dict - .into_iter() - .map(|(key, value)| ($result_fn)(vm, key, value)) - .collect::>(), + IterStatus::Active(dict) => { + let mut position = internal.position; + let mut entries = Vec::new(); + while let Some((next_position, key, value)) = + dict.entries.next_entry(position) + { + entries.push(($result_fn)(vm, ($project_fn)(&key, &value))); + position = next_position; + } + entries + } IterStatus::Exhausted => vec![], }; vm.new_tuple((iter, (vm.ctx.new_list(entries),))) @@ -1077,18 +1216,22 @@ macro_rules! dict_view { fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { let mut internal = zelf.internal.lock(); let next = if let IterStatus::Active(dict) = &internal.status { - if dict.entries.has_changed_size(&zelf.size) { - internal.status = IterStatus::Exhausted; - return Err( - vm.new_runtime_error("dictionary changed size during iteration") - ); - } - match dict.entries.next_entry(internal.position) { - Some((position, key, value)) => { + match dict.entries.next_entry_checked( + internal.position, + &zelf.size, + $project_fn, + ) { + Err(dict_inner::DictChanged) => { + internal.status = IterStatus::Exhausted; + return Err( + vm.new_runtime_error("dictionary changed size during iteration") + ); + } + Ok(Some((position, item))) => { internal.position = position; - PyIterReturn::Return(($result_fn)(vm, key, value)) + PyIterReturn::Return(($result_fn)(vm, item)) } - None => { + Ok(None) => { internal.status = IterStatus::Exhausted; PyIterReturn::StopIteration(None) } @@ -1127,14 +1270,23 @@ macro_rules! dict_view { #[pymethod] fn __reduce__(&self, vm: &VirtualMachine) -> PyTupleRef { - let iter = builtins_reversed(vm); + let iter = builtins_iter(vm); let internal = self.internal.lock(); - // TODO: entries must be reversed too let entries = match &internal.status { - IterStatus::Active(dict) => dict - .into_iter() - .map(|(key, value)| ($result_fn)(vm, key, value)) - .collect::>(), + IterStatus::Active(dict) => { + let mut position = internal.position; + let mut entries = Vec::new(); + while let Some((found_index, key, value)) = + dict.entries.prev_entry(position) + { + entries.push(($result_fn)(vm, ($project_fn)(&key, &value))); + if found_index == 0 { + break; + } + position = found_index - 1; + } + entries + } IterStatus::Exhausted => vec![], }; vm.new_tuple((iter, (vm.ctx.new_list(entries),))) @@ -1154,22 +1306,26 @@ macro_rules! dict_view { fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { let mut internal = zelf.internal.lock(); let next = if let IterStatus::Active(dict) = &internal.status { - if dict.entries.has_changed_size(&zelf.size) { - internal.status = IterStatus::Exhausted; - return Err( - vm.new_runtime_error("dictionary changed size during iteration") - ); - } - match dict.entries.prev_entry(internal.position) { - Some((position, key, value)) => { - if internal.position == position { + match dict.entries.prev_entry_checked( + internal.position, + &zelf.size, + $project_fn, + ) { + Err(dict_inner::DictChanged) => { + internal.status = IterStatus::Exhausted; + return Err( + vm.new_runtime_error("dictionary changed size during iteration") + ); + } + Ok(Some((found_index, item))) => { + if found_index == 0 { internal.status = IterStatus::Exhausted; } else { - internal.position = position; + internal.position = found_index - 1; } - PyIterReturn::Return(($result_fn)(vm, key, value)) + PyIterReturn::Return(($result_fn)(vm, item)) } - None => { + Ok(None) => { internal.status = IterStatus::Exhausted; PyIterReturn::StopIteration(None) } @@ -1193,7 +1349,8 @@ dict_view! { "dict_keys", "dict_keyiterator", "dict_reversekeyiterator", - |_vm: &VirtualMachine, key: PyObjectRef, _value: PyObjectRef| key + |key: &PyObjectRef, _value: &PyObjectRef| key.clone(), + |_vm: &VirtualMachine, key: PyObjectRef| key } dict_view! { @@ -1206,7 +1363,8 @@ dict_view! { "dict_values", "dict_valueiterator", "dict_reversevalueiterator", - |_vm: &VirtualMachine, _key: PyObjectRef, value: PyObjectRef| value + |_key: &PyObjectRef, value: &PyObjectRef| value.clone(), + |_vm: &VirtualMachine, value: PyObjectRef| value } dict_view! { @@ -1219,7 +1377,9 @@ dict_view! { "dict_items", "dict_itemiterator", "dict_reverseitemiterator", - |vm: &VirtualMachine, key: PyObjectRef, value: PyObjectRef| + |key: &PyObjectRef, value: &PyObjectRef| (key.clone(), value.clone()), + // Builds a tuple, so it runs after the dict's read guard is released. + |vm: &VirtualMachine, (key, value): (PyObjectRef, PyObjectRef)| vm.new_tuple((key, value)).into() } diff --git a/crates/vm/src/builtins/enumerate.rs b/crates/vm/src/builtins/enumerate.rs index 96073ba7667..95e144dad21 100644 --- a/crates/vm/src/builtins/enumerate.rs +++ b/crates/vm/src/builtins/enumerate.rs @@ -57,7 +57,11 @@ impl Constructor for PyEnumerate { #[pyclass(with(Py, IterNext, Iterable, Constructor), flags(BASETYPE))] impl PyEnumerate { #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/builtins/float.rs b/crates/vm/src/builtins/float.rs index db6478d668d..0b739694623 100644 --- a/crates/vm/src/builtins/float.rs +++ b/crates/vm/src/builtins/float.rs @@ -84,6 +84,7 @@ impl ToPyObject for f64 { vm.ctx.new_float(self).into() } } + impl ToPyObject for f32 { fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.new_float(f64::from(self)).into() @@ -156,8 +157,7 @@ fn inner_divmod(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult<(f64, f64)> { pub(crate) fn float_pow(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult { if v1.is_zero() && v2.is_sign_negative() { - let msg = "zero to a negative power"; - Err(vm.new_zero_division_error(msg.to_owned())) + Err(vm.new_zero_division_error("zero to a negative power")) } else if v1.is_sign_negative() && (v2.floor() - v2).abs() > f64::EPSILON { let v1 = Complex64::new(v1, 0.); let v2 = Complex64::new(v2, 0.); @@ -176,16 +176,18 @@ impl Constructor for PyFloat { type Args = OptionalArg; fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + // Bind before the fast path so FromArgs::arity decides how many arguments + // are acceptable, rather than a count repeated here. + let arg: Self::Args = args.bind(vm)?; + // Optimization: return exact float as-is if cls.is(vm.ctx.types.float_type) - && args.kwargs.is_empty() - && let Some(first) = args.args.first() + && let OptionalArg::Present(first) = &arg && first.class().is(vm.ctx.types.float_type) { return Ok(first.clone()); } - let arg: Self::Args = args.bind(vm)?; let payload = Self::py_new(&cls, arg, vm)?; payload.into_ref_with_type(vm, cls).map(Into::into) } @@ -205,7 +207,7 @@ impl Constructor for PyFloat { } } -fn float_from_string(val: PyObjectRef, vm: &VirtualMachine) -> PyResult { +pub fn float_from_string(val: PyObjectRef, vm: &VirtualMachine) -> PyResult { let (bytearray, buffer, buffer_lock, mapped_string); let b = if let Some(s) = val.downcast_ref::() { use crate::common::str::PyKindStr; @@ -254,6 +256,10 @@ fn float_from_string(val: PyObjectRef, vm: &VirtualMachine) -> PyResult { }) } +#[expect( + clippy::trivially_copy_pass_by_ref, + reason = "Needs to comply with a signature" +)] #[pyclass( flags(BASETYPE, _MATCH_SELF), with(Comparable, Hashable, Constructor, AsNumber, Representable) @@ -395,8 +401,16 @@ impl PyFloat { #[pyclassmethod] fn fromhex(cls: PyTypeRef, string: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - let result = crate::literal::float::from_hex(string.as_str().trim()) - .ok_or_else(|| vm.new_value_error("invalid hexadecimal floating-point string"))?; + use float_ops::HexFloatError; + let result = float_ops::from_hex(string.as_str()).map_err(|e| match e { + HexFloatError::Overflow => { + vm.new_overflow_error("hexadecimal value too large to represent as a float") + } + HexFloatError::TooLong => vm.new_value_error("hexadecimal string too long to convert"), + HexFloatError::Invalid => { + vm.new_value_error("invalid hexadecimal floating-point string") + } + })?; PyType::call(&cls, vec![vm.ctx.new_float(result).into()].into(), vm) } diff --git a/crates/vm/src/builtins/frame.rs b/crates/vm/src/builtins/frame.rs index 0cb16b2359f..e4b3aa2b184 100644 --- a/crates/vm/src/builtins/frame.rs +++ b/crates/vm/src/builtins/frame.rs @@ -4,13 +4,16 @@ use super::{PyCode, PyDictRef, PyIntRef, PyStrRef}; use crate::{ - Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine, + Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, - frame::{Frame, FrameOwner, FrameRef}, + frame::{FrameObject, FrameObjectRef, FrameOwner}, function::PySetterValue, types::Representable, }; +use core::sync::atomic::Ordering::Relaxed; use num_traits::Zero; +#[allow(unused_imports)] +use rustpython_common::atomic::Radium; use rustpython_compiler_core::bytecode::{self, Constant, Instruction, StackEffect}; use stack_analysis::*; @@ -426,10 +429,10 @@ pub(crate) mod stack_analysis { } pub(crate) fn init(context: &'static Context) { - Frame::extend_class(context, context.types.frame_type); + FrameObject::extend_class(context, context.types.frame_type); } -impl Representable for Frame { +impl Representable for FrameObject { #[inline] fn repr(_zelf: &Py, vm: &VirtualMachine) -> PyResult { const REPR: &str = ""; @@ -442,46 +445,86 @@ impl Representable for Frame { } } +impl FrameObject { + /// Find the live source InterpreterFrame on the TLS chain for a + /// materialized FrameObject. Returns the raw pointer if found, or null + /// if this FrameObject has no live source (already returned or not + /// currently executing on this thread). + pub(crate) fn find_live_source_iframe(&self) -> *const crate::frame::InterpreterFrame { + let self_py_ptr = unsafe { Py::::from_payload_ptr(self) } as usize; + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let materialized = unsafe { (*cur).materialized.load(Relaxed) }; + if materialized == self_py_ptr { + return cur; + } + cur = unsafe { &*cur }.previous(); + } + core::ptr::null() + } +} + #[pyclass(flags(DISALLOW_INSTANTIATION), with(Py))] -impl Frame { +impl FrameObject { #[pygetset] fn f_globals(&self) -> PyDictRef { - self.globals.clone() + self.iframe().globals().to_owned() } #[pygetset] fn f_builtins(&self) -> PyObjectRef { - self.builtins.clone() - } - - #[pygetset] - fn f_locals(&self, vm: &VirtualMachine) -> PyResult { - let result = self.f_locals_mapping(vm).map(Into::into); - self.locals_dirty - .store(true, core::sync::atomic::Ordering::Release); - result + self.iframe().builtins().to_owned() } #[pygetset] pub fn f_code(&self) -> PyRef { - self.code.clone() + self.iframe().code().to_owned() } #[pygetset] fn f_lasti(&self) -> u32 { - // Return byte offset (each instruction is 2 bytes) for compatibility - self.lasti() * 2 + // Return byte offset (each instruction is 2 bytes) for compatibility. + // For materialized frames, read live lasti from the source iframe on + // the TLS chain so f_lasti reflects the current execution position. + let live = self.find_live_source_iframe(); + let val = if !live.is_null() { + unsafe { (*live).lasti.load(Relaxed) } + } else { + self.lasti() + }; + val * 2 } #[pygetset] pub fn f_lineno(&self) -> usize { // If lasti is 0, execution hasn't started yet - use first line number - // Similar to PyCode_Addr2Line which returns co_firstlineno for addr_q < 0 if self.lasti() == 0 { - self.code.first_line_number.map_or(1, |n| n.get()) - } else { - self.current_location().line.get() + return self + .iframe() + .code() + .first_line_number + .map_or(1, |n| n.get()); } + // For executing frames (on the TLS chain), use prev_line which is + // updated at each bytecode instruction *before* the instruction + // runs. This gives the correct line even when observed mid-CALL + // (where lasti has already advanced past the CALL instruction). + let live = self.find_live_source_iframe(); + if !live.is_null() { + // Read live prev_line. Use read_volatile to bypass LLVM noalias + // on the &mut InterpreterFrame borrow held by the running frame. + let prev = unsafe { + let field_ptr = core::ptr::addr_of!((*live).prev_line); + core::ptr::read_volatile(field_ptr as *const u32) + }; + if prev > 0 { + return prev as usize; + } + } + // For returned frames, use lasti-based location lookup. This is + // correct for exception tracebacks where prev_line may have been + // updated by cleanup instructions after the exception. + self.current_location().line.get() } #[pygetset(setter)] @@ -500,7 +543,11 @@ impl Frame { } }; - let first_line = self.code.first_line_number.map_or(1, |n| n.get() as i32); + let first_line = self + .iframe() + .code() + .first_line_number + .map_or(1, |n| n.get() as i32); if l_new_lineno < first_line { return Err(vm.new_value_error(format!( @@ -508,7 +555,7 @@ impl Frame { ))); } - let py_code: &PyCode = &self.code; + let py_code: &PyCode = self.iframe().code(); let code = &py_code.code; let lines = mark_lines(code); @@ -521,13 +568,19 @@ impl Frame { } let stacks = mark_stacks(code); - let len = self.code.instructions.len(); + let len = self.iframe().code().instructions.len(); // lasti points past the current instruction (already incremented). // stacks[lasti - 1] gives the stack state before executing the // instruction that triggered this trace event, which is the current - // evaluation stack. - let current_lasti = self.lasti() as usize; + // evaluation stack. Read from the live iframe when available so the + // value reflects the actual execution position. + let live = self.find_live_source_iframe(); + let current_lasti = if !live.is_null() { + (unsafe { (*live).lasti.load(Relaxed) }) as usize + } else { + self.lasti() as usize + }; let start_idx = current_lasti.saturating_sub(1); let start_stack = if start_idx < stacks.len() { stacks[start_idx] @@ -575,36 +628,67 @@ impl Frame { } } - // Store the pending unwind for the execution loop to perform. - // We cannot pop stack entries here because the execution loop - // holds the state mutex, and trying to lock it again would deadlock. - self.set_pending_stack_pops(pop_count as u32); - self.set_pending_unwind_from_stack(start_stack); - - // Set lasti to best_addr. The executor will read lasti and execute - // the instruction at that index next. - self.set_lasti(best_addr as u32); + // Store the pending unwind and new lasti. When this frame is backed + // by a live stack-allocated iframe, write to the live iframe so the + // execution loop picks up the jump target. Reuse `live` from above. + let target = if !live.is_null() { + unsafe { &*live } + } else { + self.iframe() + }; + target + .cold() + .pending_stack_pops + .store(pop_count as u32, Relaxed); + target + .cold() + .pending_unwind_from_stack + .store(start_stack, Relaxed); + target.lasti.store(best_addr as u32, Relaxed); Ok(()) } #[pygetset] - fn f_trace(&self) -> PyObjectRef { - let boxed = self.trace.lock(); - boxed.clone() + fn f_trace(&self, vm: &VirtualMachine) -> PyObjectRef { + // Read from live source iframe if available. + let live = self.find_live_source_iframe(); + let trace = if !live.is_null() { + unsafe { &*live }.cold().trace.lock().clone() + } else { + self.iframe().cold().trace.lock().clone() + }; + trace.unwrap_or_else(|| vm.ctx.none()) } #[pygetset(setter)] fn set_f_trace(&self, value: PySetterValue, vm: &VirtualMachine) { - let mut storage = self.trace.lock(); - *storage = value.unwrap_or_none(vm); + let trace = match value { + PySetterValue::Assign(v) => { + if vm.is_none(&v) { + None + } else { + Some(v) + } + } + PySetterValue::Delete => None, + }; + // Set on the materialized FrameObject. + (*self.iframe().cold().trace.lock()).clone_from(&trace); + // Also propagate to the live source iframe if this is a + // materialized copy of a stack-allocated frame, so pdb's + // f_trace assignment takes effect on the executing frame. + let live = self.find_live_source_iframe(); + if !live.is_null() { + *unsafe { &*live }.cold().trace.lock() = trace; + } } #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] #[pymember(type = "bool")] fn f_trace_lines(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: FrameRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); + let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - let boxed = zelf.trace_lines.lock(); + let boxed = zelf.iframe().cold().trace_lines.lock(); Ok(vm.ctx.new_bool(*boxed).into()) } @@ -616,14 +700,19 @@ impl Frame { ) -> PyResult<()> { match value { PySetterValue::Assign(value) => { - let zelf: FrameRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); + let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); let value: PyIntRef = value .downcast() .map_err(|_| vm.new_type_error("attribute value type must be bool"))?; - let mut trace_lines = zelf.trace_lines.lock(); - *trace_lines = !value.as_bigint().is_zero(); + let val = !value.as_bigint().is_zero(); + *zelf.iframe().cold().trace_lines.lock() = val; + // Propagate to live source iframe. + let live = zelf.find_live_source_iframe(); + if !live.is_null() { + *unsafe { &*live }.cold().trace_lines.lock() = val; + } Ok(()) } @@ -634,8 +723,8 @@ impl Frame { #[expect(clippy::unnecessary_wraps, reason = "Needs to comply with a signature")] #[pymember(type = "bool")] fn f_trace_opcodes(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { - let zelf: FrameRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); - let trace_opcodes = zelf.trace_opcodes.lock(); + let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); + let trace_opcodes = zelf.iframe().cold().trace_opcodes.lock(); Ok(vm.ctx.new_bool(*trace_opcodes).into()) } @@ -647,14 +736,19 @@ impl Frame { ) -> PyResult<()> { match value { PySetterValue::Assign(value) => { - let zelf: FrameRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); + let zelf: FrameObjectRef = zelf.downcast().unwrap_or_else(|_| unreachable!()); let value: PyIntRef = value .downcast() .map_err(|_| vm.new_type_error("attribute value type must be bool"))?; - let mut trace_opcodes = zelf.trace_opcodes.lock(); - *trace_opcodes = !value.as_bigint().is_zero(); + let val = !value.as_bigint().is_zero(); + *zelf.iframe().cold().trace_opcodes.lock() = val; + // Propagate to live source iframe. + let live = zelf.find_live_source_iframe(); + if !live.is_null() { + *unsafe { &*live }.cold().trace_opcodes.lock() = val; + } // TODO: Implement the equivalent of _PyEval_SetOpcodeTrace() @@ -666,11 +760,15 @@ impl Frame { } #[pyclass] -impl Py { +impl Py { #[pymethod] // = frame_clear_impl fn clear(&self, vm: &VirtualMachine) -> PyResult<()> { - let owner = FrameOwner::from_i8(self.owner.load(core::sync::atomic::Ordering::Acquire)); + let owner = FrameOwner::from_i8( + self.iframe() + .owner + .load(core::sync::atomic::Ordering::Acquire), + ); match owner { FrameOwner::Generator => { // Generator frame: check if suspended (lasti > 0 means @@ -685,12 +783,16 @@ impl Py { return Err(vm.new_runtime_error("cannot clear an executing frame")); } FrameOwner::FrameObject => { - // Detached frame: safe to clear. + // Check if this materialized frame is backed by a live + // stack-allocated iframe — if so, the frame is executing. + if !self.find_live_source_iframe().is_null() { + return Err(vm.new_runtime_error("cannot clear an executing frame")); + } } } // Clear fastlocals - // SAFETY: Frame is not executing (detached or stopped). + // SAFETY: FrameObject is not executing (detached or stopped). { let fastlocals = unsafe { self.fastlocals_mut() }; for slot in fastlocals.iter_mut() { @@ -702,56 +804,115 @@ impl Py { self.clear_stack_and_cells(); // Clear temporary refs - self.temporary_refs.lock().clear(); - self.f_locals_hidden_overlay.lock().take(); + self.iframe().cold().temporary_refs.lock().clear(); + self.iframe().cold().f_locals_hidden_overlay.lock().take(); + self.iframe().cold().f_extra_locals.lock().take(); + self.iframe().cold().retained_back.lock().take(); Ok(()) } + #[pygetset] + fn f_locals(&self, vm: &VirtualMachine) -> PyResult { + // Optimized (function) frames expose a live write-through + // FrameLocalsProxy; class/module/exec frames expose their namespace + // mapping directly. + if self + .iframe() + .code() + .flags + .contains(bytecode::CodeFlags::OPTIMIZED) + { + self.check_locals_access(vm)?; + self.mark_escaped(); + let proxy = crate::builtins::FrameLocalsProxy::new(self.to_owned()); + Ok(proxy.into_ref(&vm.ctx).into()) + } else { + self.f_locals_mapping(vm).map(Into::into) + } + } + #[pygetset] fn f_generator(&self) -> Option { - self.generator.to_owned() + self.iframe().generator.to_owned() } #[pygetset] - pub fn f_back(&self, vm: &VirtualMachine) -> Option> { - let previous = self.previous_frame(); - if previous.is_null() { - return None; - } - - if let Some(frame) = vm - .frames - .borrow() - .iter() - .find(|fp| { - // SAFETY: the caller keeps the FrameRef alive while it's in the Vec - let py: &Self = unsafe { fp.as_ref() }; - let ptr: *const Frame = &**py; - core::ptr::eq(ptr, previous) - }) - .map(|fp| unsafe { fp.as_ref() }.to_owned()) + pub fn f_back(&self, #[allow(unused)] vm: &VirtualMachine) -> Option> { + let mut prev = self.previous_iframe(); + + // For materialized frames (previous == 0), find the source iframe on + // the TLS chain and use its `previous` instead. + if prev.is_null() { + // materialized stores `*const Py` as usize. + // `self` is `&Py` — compare addresses directly. + let self_py_ptr = self as *const Self as usize; + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let materialized = unsafe { (*cur).materialized.load(Relaxed) }; + if materialized == self_py_ptr { + // Found the source iframe — use its previous + prev = unsafe { (*cur).previous() }; + break; + } + cur = unsafe { (*cur).previous() }; + } + if prev.is_null() { + // Check retained_back for frames whose callers have returned + let retained = self.iframe().cold().retained_back.lock().clone(); + if let Some(frame) = retained { + frame.mark_escaped(); + return Some(frame); + } + return None; + } + } + + // Walk the TLS chain to find the prev iframe and materialize it. + // This handles both heap-allocated FrameObjects and stack-allocated + // iframes that haven't been observed yet. { + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + if core::ptr::eq(cur, prev) { + let iframe_ref = unsafe { &*cur }; + let fo = iframe_ref.materialize(vm); + fo.mark_escaped(); + return Some(fo.to_owned()); + } + cur = unsafe { (*cur).previous() }; + } + } + + // The caller already returned — check retained_back + let retained = self.iframe().cold().retained_back.lock().clone(); + if let Some(frame) = retained { + frame.mark_escaped(); return Some(frame); } + // The caller lives on another thread. Use stop-the-world to + // safely materialize the cross-thread frame chain. #[cfg(feature = "threading")] { - let registry = vm.state.thread_frames.lock(); - for slot in registry.values() { - let frames = slot.frames.lock(); - // SAFETY: the owning thread can't pop while we hold the Mutex, - // so FramePtr is valid for the duration of the lock. - if let Some(frame) = frames.iter().find_map(|fp| { - let f = unsafe { fp.as_ref() }; - let ptr: *const Frame = &**f; - core::ptr::eq(ptr, previous).then(|| f.to_owned()) - }) { - return Some(frame); - } + // Enter STW before dereferencing `prev` — the owning thread may + // return and free the stack-allocated iframe at any time. + vm.state.stop_the_world.stop_the_world(&vm.state); + scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); } + let prev_ref = unsafe { &*prev }; + // Fast path: already materialized. + if let Some(fo) = prev_ref.frame_obj() { + fo.mark_escaped(); + return Some(fo.to_owned()); } + // Slow path: copy the whole chain, linked through retained_back. + // SAFETY: the world is stopped, so the owning thread is parked. + let fo = unsafe { prev_ref.materialize_detached_chain(vm) }; + fo.mark_escaped(); + return Some(fo); } + #[allow(unreachable_code)] None } } diff --git a/crates/vm/src/builtins/frame_locals_proxy.rs b/crates/vm/src/builtins/frame_locals_proxy.rs new file mode 100644 index 00000000000..eaba0bca609 --- /dev/null +++ b/crates/vm/src/builtins/frame_locals_proxy.rs @@ -0,0 +1,326 @@ +//! The `FrameLocalsProxy` type returned by `frame.f_locals` for optimized +//! (function) frames. Implements PEP 667 write-through semantics on top of the +//! frame's fast-local slots and an extra-locals side dict. + +use super::{PyDict, PyDictRef, PyType}; +use crate::{ + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + atomic_func, + class::PyClassImpl, + frame::FrameObjectRef, + function::{FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue}, + object::{Traverse, TraverseFn}, + protocol::{PyMappingMethods, PyNumberMethods, PySequenceMethods}, + recursion::ReprGuard, + types::{ + AsMapping, AsNumber, AsSequence, Comparable, Constructor, Iterable, PyComparisonOp, + Representable, + }, +}; +use rustpython_common::lock::LazyLock; +use rustpython_common::wtf8::Wtf8Buf; + +#[pyclass(module = false, name = "FrameLocalsProxy", traverse = "manual")] +#[derive(Debug)] +pub struct FrameLocalsProxy { + frame: FrameObjectRef, +} + +unsafe impl Traverse for FrameLocalsProxy { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.frame.traverse(tracer_fn); + } +} + +impl PyPayload for FrameLocalsProxy { + #[inline] + fn class(ctx: &Context) -> &'static Py { + ctx.types.frame_locals_proxy_type + } +} + +impl FrameLocalsProxy { + pub(crate) fn new(frame: FrameObjectRef) -> Self { + Self { frame } + } + + fn snapshot(&self, vm: &VirtualMachine) -> PyResult { + self.frame.framelocalsproxy_snapshot(vm) + } + + fn keys_vec(&self, vm: &VirtualMachine) -> PyResult> { + Ok(self.snapshot(vm)?.into_iter().map(|(k, _)| k).collect()) + } +} + +impl Constructor for FrameLocalsProxy { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { + if !args.kwargs.is_empty() { + return Err(vm.new_type_error("FrameLocalsProxy() takes no keyword arguments")); + } + let mut args = args.args; + if args.len() != 1 { + return Err(vm.new_type_error(format!( + "FrameLocalsProxy expected 1 argument, got {}", + args.len() + ))); + } + let frame: FrameObjectRef = args + .pop() + .unwrap() + .downcast() + .map_err(|_| vm.new_type_error("FrameLocalsProxy expected a frame"))?; + Ok(Self::new(frame)) + } +} + +#[pyclass(with( + Constructor, + AsMapping, + AsSequence, + AsNumber, + Iterable, + Comparable, + Representable +))] +impl FrameLocalsProxy { + fn __getitem__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult { + self.frame.framelocalsproxy_getitem(key, vm) + } + + fn __setitem__( + &self, + key: PyObjectRef, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + self.frame.framelocalsproxy_setitem(key, value, vm) + } + + fn __delitem__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + self.frame.framelocalsproxy_delitem(key, vm) + } + + fn __contains__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult { + self.frame.framelocalsproxy_contains(key, vm) + } + + fn __len__(&self, vm: &VirtualMachine) -> PyResult { + Ok(self.snapshot(vm)?.__len__()) + } + + #[pymethod] + fn keys(&self, vm: &VirtualMachine) -> PyResult { + Ok(vm.ctx.new_list(self.keys_vec(vm)?).into()) + } + + #[pymethod] + fn values(&self, vm: &VirtualMachine) -> PyResult { + let values = self.snapshot(vm)?.into_iter().map(|(_, v)| v).collect(); + Ok(vm.ctx.new_list(values).into()) + } + + #[pymethod] + fn items(&self, vm: &VirtualMachine) -> PyResult { + let items = self + .snapshot(vm)? + .into_iter() + .map(|(k, v)| vm.ctx.new_tuple(vec![k, v]).into()) + .collect(); + Ok(vm.ctx.new_list(items).into()) + } + + #[pymethod] + fn get(&self, key: PyObjectRef, default: OptionalArg, vm: &VirtualMachine) -> PyResult { + match self.frame.framelocalsproxy_getitem(key, vm) { + Ok(value) => Ok(value), + Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => { + Ok(default.unwrap_or_none(vm)) + } + Err(e) => Err(e), + } + } + + #[pymethod] + fn pop(&self, key: PyObjectRef, default: OptionalArg, vm: &VirtualMachine) -> PyResult { + self.frame + .framelocalsproxy_pop(key, default.into_option(), vm) + } + + #[pymethod] + fn setdefault(&self, key: PyObjectRef, default: OptionalArg, vm: &VirtualMachine) -> PyResult { + self.frame + .framelocalsproxy_setdefault(key, default.unwrap_or_none(vm), vm) + } + + #[pymethod] + fn copy(&self, vm: &VirtualMachine) -> PyResult { + Ok(self.snapshot(vm)?.into()) + } + + #[pymethod] + fn update(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + if !args.kwargs.is_empty() { + return Err(vm.new_type_error("FrameLocalsProxy.update() takes no keyword arguments")); + } + if args.args.len() != 1 { + return Err(vm.new_type_error(format!( + "FrameLocalsProxy.update() takes exactly one argument ({} given)", + args.args.len() + ))); + } + self.update_from(&args.args[0], vm) + } + + fn update_from(&self, other: &PyObject, vm: &VirtualMachine) -> PyResult<()> { + let items: Vec<(PyObjectRef, PyObjectRef)> = + if let Some(dict) = other.downcast_ref::() { + dict.into_iter().collect() + } else if let Some(proxy) = other.downcast_ref::() { + proxy.snapshot(vm)?.into_iter().collect() + } else { + return Err( + vm.new_type_error("update() argument must be dict or another FrameLocalsProxy") + ); + }; + for (key, value) in items { + self.frame.framelocalsproxy_setitem(key, value, vm)?; + } + Ok(()) + } + + #[pymethod] + fn __reversed__(&self, vm: &VirtualMachine) -> PyResult { + let mut keys = self.keys_vec(vm)?; + keys.reverse(); + Ok(vm.ctx.new_list(keys).into()) + } + + fn __ior__(zelf: PyRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult { + zelf.update_from(&other, vm)?; + Ok(zelf.into()) + } + + fn __or__(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let base = self.snapshot(vm)?; + vm._or(base.as_object(), &other) + } + + fn __ror__(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let base = self.snapshot(vm)?; + vm._or(&other, base.as_object()) + } + + #[pymethod] + fn __reduce__(&self, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("cannot pickle 'FrameLocalsProxy' object")) + } + + #[pymethod] + fn __reduce_ex__(&self, _protocol: OptionalArg, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("cannot pickle 'FrameLocalsProxy' object")) + } +} + +impl AsMapping for FrameLocalsProxy { + fn as_mapping() -> &'static PyMappingMethods { + static AS_MAPPING: LazyLock = LazyLock::new(|| PyMappingMethods { + length: atomic_func!( + |mapping, vm| FrameLocalsProxy::mapping_downcast(mapping).__len__(vm) + ), + subscript: atomic_func!(|mapping, needle, vm| { + FrameLocalsProxy::mapping_downcast(mapping).__getitem__(needle.to_owned(), vm) + }), + ass_subscript: atomic_func!(|mapping, needle, value, vm| { + let zelf = FrameLocalsProxy::mapping_downcast(mapping); + match value { + Some(value) => zelf.__setitem__(needle.to_owned(), value, vm), + None => zelf.__delitem__(needle.to_owned(), vm), + } + }), + }); + &AS_MAPPING + } +} + +impl AsSequence for FrameLocalsProxy { + fn as_sequence() -> &'static PySequenceMethods { + static AS_SEQUENCE: LazyLock = LazyLock::new(|| PySequenceMethods { + contains: atomic_func!(|seq, target, vm| { + FrameLocalsProxy::sequence_downcast(seq).__contains__(target.to_owned(), vm) + }), + ..PySequenceMethods::NOT_IMPLEMENTED + }); + &AS_SEQUENCE + } +} + +impl AsNumber for FrameLocalsProxy { + fn as_number() -> &'static PyNumberMethods { + static AS_NUMBER: PyNumberMethods = PyNumberMethods { + or: Some(|a, b, vm| { + if let Some(proxy) = a.downcast_ref::() { + proxy.__or__(b.to_owned(), vm) + } else if let Some(proxy) = b.downcast_ref::() { + proxy.__ror__(a.to_owned(), vm) + } else { + Ok(vm.ctx.not_implemented()) + } + }), + inplace_or: Some(|a, b, vm| { + let proxy = a + .to_owned() + .downcast::() + .map_err(|_| vm.new_type_error("expected FrameLocalsProxy"))?; + FrameLocalsProxy::__ior__(proxy, b.to_owned(), vm) + }), + ..PyNumberMethods::NOT_IMPLEMENTED + }; + &AS_NUMBER + } +} + +impl Iterable for FrameLocalsProxy { + fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let keys = vm.ctx.new_list(zelf.keys_vec(vm)?); + keys.as_object().to_owned().get_iter(vm).map(Into::into) + } +} + +impl Comparable for FrameLocalsProxy { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + let self_dict: PyObjectRef = zelf.snapshot(vm)?.into(); + let other_obj = match other.downcast_ref::() { + Some(proxy) => proxy.snapshot(vm)?.into(), + None => other.to_owned(), + }; + let res = self_dict.rich_compare(other_obj, PyComparisonOp::Eq, vm)?; + PyArithmeticValue::from_object(vm, res) + .map(|o| o.try_to_bool(vm)) + .transpose() + }) + } +} + +impl Representable for FrameLocalsProxy { + fn repr_wtf8(zelf: &Py, vm: &VirtualMachine) -> PyResult { + if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) { + let dict = zelf.snapshot(vm)?; + Ok(dict.as_object().repr(vm)?.as_wtf8().to_owned()) + } else { + Ok(Wtf8Buf::from("{...}")) + } + } +} + +pub(crate) fn init(context: &'static Context) { + FrameLocalsProxy::extend_class(context, context.types.frame_locals_proxy_type); +} diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 4125ef4c4c6..a5342d1df3a 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -14,8 +14,8 @@ use crate::{ bytecode, class::PyClassImpl, common::wtf8::{Wtf8Buf, wtf8_concat}, - frame::{Frame, FrameRef}, - function::{FuncArgs, OptionalArg, PyComparisonValue, PySetterValue}, + frame::{FrameObject, FrameObjectRef}, + function::{Either, FuncArgs, OptionalArg, PyComparisonValue, PySetterValue}, scope::Scope, types::{ Callable, Comparable, Constructor, GetAttr, GetDescriptor, Hashable, PyComparisonOp, @@ -64,9 +64,9 @@ fn format_missing_args( #[pyclass(module = false, name = "function", traverse = "manual")] #[derive(Debug)] pub struct PyFunction { - code: PyAtomicRef, - globals: PyDictRef, - builtins: PyObjectRef, + pub(crate) code: PyAtomicRef, + pub(crate) globals: PyDictRef, + pub(crate) builtins: PyObjectRef, pub(crate) closure: Option>>, defaults_and_kwdefaults: PyMutex<(Option, Option)>, name: PyMutex, @@ -87,7 +87,7 @@ static FUNC_VERSION_COUNTER: AtomicU32 = AtomicU32::new(1); /// Once the counter wraps to 0, it stays at 0 permanently. fn next_func_version() -> u32 { FUNC_VERSION_COUNTER - .fetch_update(Relaxed, Relaxed, |v| (v != 0).then(|| v.wrapping_add(1))) + .try_update(Relaxed, Relaxed, |v| (v != 0).then(|| v.wrapping_add(1))) .unwrap_or(0) } @@ -95,7 +95,12 @@ unsafe impl Traverse for PyFunction { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { self.globals.traverse(tracer_fn); if let Some(closure) = self.closure.as_ref() { - closure.as_untyped().traverse(tracer_fn); + // Visit the closure tuple itself as an edge, not its cells: the + // tuple is a tracked object that can join a reference cycle, and + // `clear` releases the whole tuple. Visiting only the cells would + // leave the tuple's reference unaccounted, stranding it as a false + // GC root. + tracer_fn(closure.as_untyped().as_object()); } self.defaults_and_kwdefaults.traverse(tracer_fn); // Traverse additional fields that may contain references @@ -177,11 +182,7 @@ impl PyFunction { let module = vm.unwrap_or_none(globals.get_item_opt(identifier!(vm, __name__), vm)?); let builtins = globals.get_item("__builtins__", vm).unwrap_or_else(|_| { // If not in globals, inherit from current execution context - if let Some(frame) = vm.current_frame() { - frame.builtins.clone() - } else { - vm.builtins.dict().into() - } + crate::frame::current_builtins().unwrap_or_else(|| vm.builtins.dict().into()) }); // If builtins is a module, use its __dict__ instead let builtins = if let Some(module) = builtins.downcast_ref::() { @@ -223,7 +224,28 @@ impl PyFunction { fn fill_locals_from_args( &self, - frame: &Frame, + frame: &FrameObject, + func_args: FuncArgs, + vm: &VirtualMachine, + ) -> PyResult<()> { + // SAFETY: FrameObject was just created and not yet executing. + let fastlocals = unsafe { frame.fastlocals_mut() }; + self.fill_locals_from_args_inner(fastlocals, func_args, vm) + } + + fn fill_locals_from_args_iframe( + &self, + iframe: &mut crate::frame::InterpreterFrame, + func_args: FuncArgs, + vm: &VirtualMachine, + ) -> PyResult<()> { + let fastlocals = iframe.localsplus.fastlocals_mut(); + self.fill_locals_from_args_inner(fastlocals, func_args, vm) + } + + fn fill_locals_from_args_inner( + &self, + fastlocals: &mut [Option], func_args: FuncArgs, vm: &VirtualMachine, ) -> PyResult<()> { @@ -231,16 +253,6 @@ impl PyFunction { let nargs = func_args.args.len(); let n_expected_args = code.arg_count as usize; let total_args = code.arg_count as usize + code.kwonlyarg_count as usize; - // let arg_names = self.code.arg_names(); - - // This parses the arguments from args and kwargs into - // the proper variables keeping into account default values - // and star-args and kwargs. - // See also: PyEval_EvalCodeWithName in cpython: - // https://github.com/python/cpython/blob/main/Python/ceval.c#L3681 - - // SAFETY: Frame was just created and not yet executing. - let fastlocals = unsafe { frame.fastlocals_mut() }; let mut args_iter = func_args.args.into_iter(); @@ -332,8 +344,13 @@ impl PyFunction { let mut posonly_passed_as_kwarg = Vec::new(); // Handle keyword arguments for (name, value) in func_args.kwargs { + // Parameter names are plain identifiers, so a non-UTF-8 (surrogate) key + // can never match one and just falls through to **kwargs / the error path. + let name_str = name.as_str().ok(); // Check if we have a parameter with this name: - if let Some(pos) = arg_pos(code.posonlyarg_count as usize..total_args, &name) { + if let Some(pos) = + name_str.and_then(|s| arg_pos(code.posonlyarg_count as usize..total_args, s)) + { let slot = &mut fastlocals[pos]; if slot.is_some() { return Err(vm.new_type_error(format!( @@ -345,7 +362,9 @@ impl PyFunction { *slot = Some(value); } else if let Some(kwargs) = kwargs.as_ref() { kwargs.set_item(&name, value, vm)?; - } else if arg_pos(0..code.posonlyarg_count as usize, &name).is_some() { + } else if name_str + .is_some_and(|s| arg_pos(0..code.posonlyarg_count as usize, s).is_some()) + { posonly_passed_as_kwarg.push(name); } else { return Err(vm.new_type_error(format!( @@ -531,6 +550,20 @@ impl Py { self.code.flags.contains(bytecode::CodeFlags::OPTIMIZED) } + /// Whether this function currently has native JIT code. Adaptive Python + /// call specializations must yield to that entry point. + #[inline] + pub(crate) fn is_jitted(&self) -> bool { + #[cfg(feature = "jit")] + { + self.jitted_code.lock().is_some() + } + #[cfg(not(feature = "jit"))] + { + false + } + } + pub fn invoke_with_locals( &self, func_args: FuncArgs, @@ -552,59 +585,111 @@ impl Py { } } - let code: PyRef = (*self.code).to_owned(); - - let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { - None - } else if let Some(locals) = locals { - Some(locals) - } else { - Some(ArgMapping::from_dict_exact(self.globals.clone())) - }; + let code = &*self.code; let is_gen = code.flags.contains(bytecode::CodeFlags::GENERATOR); let is_coro = code.flags.contains(bytecode::CodeFlags::COROUTINE); let is_async_gen = code.flags.contains(bytecode::CodeFlags::ASYNC_GENERATOR); - let use_datastack = !(is_gen || is_coro || is_async_gen); - // Construct frame: - let frame = Frame::new( - code, - Scope::new(locals, self.globals.clone()), - self.builtins.clone(), - self.closure.as_ref().map_or(&[], |c| c.as_slice()), - Some(self.to_owned().into()), - use_datastack, - vm, - ) - .into_ref(&vm.ctx); - - self.fill_locals_from_args(&frame, func_args, vm)?; - if is_async_gen { - let obj = PyAsyncGen::new(frame.clone(), self.__name__(), self.__qualname__()) - .into_pyobject(vm); - frame.set_generator(&obj); - Ok(obj) - } else if is_gen { - let obj = PyGenerator::new(frame.clone(), self.__name__(), self.__qualname__()) - .into_pyobject(vm); - frame.set_generator(&obj); - Ok(obj) - } else if is_coro { - let obj = PyCoroutine::new(frame.clone(), self.__name__(), self.__qualname__()) - .into_pyobject(vm); - frame.set_generator(&obj); - Ok(obj) - } else { + let needs_heap_frame = is_gen || is_coro || is_async_gen || vm.use_tracing.get(); + + if needs_heap_frame { + // Heap-allocate FrameObject for generators/coroutines (lifetime + // exceeds call stack) or when tracing is active (trace callbacks + // need a FrameObject). + let code_owned: PyRef = code.to_owned(); + let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { + None + } else if let Some(locals) = locals { + Some(locals) + } else { + Some(ArgMapping::from_dict_exact(self.globals.clone())) + }; + let use_datastack = !is_gen && !is_coro && !is_async_gen; + let frame = FrameObject::new_ref( + code_owned, + Scope::new(locals, self.globals.clone()), + self.builtins.clone(), + self.closure.as_ref().map_or(&[], |c| c.as_slice()), + Some(self.to_owned().into()), + use_datastack, + vm, + ); + self.fill_locals_from_args(&frame, func_args, vm)?; + if is_gen || is_coro || is_async_gen { + return Ok(self.make_generator_or_coro(frame, vm)); + } + // Tracing active: use heap frame with full trace support. let result = vm.run_frame(frame.clone()); - // Release data stack memory after frame execution completes. unsafe { - if let Some(base) = frame.materialize_localsplus() { + if let Some(base) = frame.iframe_mut().localsplus.release_datastack() { vm.datastack_pop(base); } } - result + return result; + } + + // Fast path: stack-allocated InterpreterFrame, no FrameObject. + // No refcount inc for code — it's alive via self.code for the call duration. + let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { + crate::frame::FrameLocals::lazy() + } else if let Some(locals) = locals { + crate::frame::FrameLocals::with_locals(locals) + } else { + crate::frame::FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact( + self.globals.clone(), + )) + }; + + // Use self.as_object() as raw pointer — no refcount inc/dec. + // The function is alive on the caller's stack for the call duration. + let iframe = crate::frame::InterpreterFrame::new_on_datastack( + &self.code, + &self.globals, + &self.builtins, + Some(self.as_object()), + locals, + self.closure.as_ref().map_or(&[], |c| c.as_slice()), + vm, + ); + let result = self + .fill_locals_from_args_iframe(iframe, func_args, vm) + .and_then(|()| vm.run_frame_fast(iframe)); + // Release data stack memory — must happen on both success and error. + unsafe { + if let Some((base, size)) = iframe.release_datastack_frame() { + vm.datastack_pop_frame(base, size); + } } + result + } + + /// Create generator, coroutine, or async generator from a FrameObject. + fn make_generator_or_coro(&self, frame: FrameObjectRef, vm: &VirtualMachine) -> PyObjectRef { + let code = frame.iframe().code(); + let is_async_gen = code.flags.contains(bytecode::CodeFlags::ASYNC_GENERATOR); + let is_gen = code.flags.contains(bytecode::CodeFlags::GENERATOR); + + let obj = if is_async_gen { + PyAsyncGen::new(frame.clone(), self.__name__(), self.__qualname__()).into_pyobject(vm) + } else if is_gen { + PyGenerator::new(frame.clone(), self.__name__(), self.__qualname__()).into_pyobject(vm) + } else { + PyCoroutine::new(frame.clone(), self.__name__(), self.__qualname__()).into_pyobject(vm) + }; + debug_assert!( + !frame.localsplus_is_datastack_backed(), + "generator frame is data-stack-backed" + ); + // SAFETY: the frame is alive (held by `frame`) and untracked. + unsafe { + crate::gc_state::gc_state().track_object( + core::ptr::NonNull::from(frame.as_object()), + crate::gc_state::current_owner(), + ); + } + frame.set_generator(&obj); + obj } #[inline(always)] @@ -634,16 +719,6 @@ impl Py { new_v } - /// function_kind(SIMPLE_FUNCTION) equivalent for CALL specialization. - /// Returns true if: CO_OPTIMIZED, no VARARGS, no VARKEYWORDS, no kwonly args. - pub(crate) fn is_simple_for_call_specialization(&self) -> bool { - let code: &Py = &self.code; - let flags = code.flags; - flags.contains(bytecode::CodeFlags::OPTIMIZED) - && !flags.intersects(bytecode::CodeFlags::VARARGS | bytecode::CodeFlags::VARKEYWORDS) - && code.kwonlyarg_count == 0 - } - /// Check if this function is eligible for exact-args call specialization. /// Returns true if: CO_OPTIMIZED, no VARARGS, no VARKEYWORDS, no kwonly args, /// and effective_nargs matches co_argcount. @@ -656,6 +731,16 @@ impl Py { && code.arg_count == effective_nargs } + /// True if the code object is a generator, coroutine or async generator. + #[inline] + pub(crate) fn is_generator_like(&self) -> bool { + self.code.flags.intersects( + bytecode::CodeFlags::GENERATOR + | bytecode::CodeFlags::COROUTINE + | bytecode::CodeFlags::ASYNC_GENERATOR, + ) + } + /// Runtime guard for CALL_*_EXACT_ARGS specialization: check only argcount. /// Other invariants are guaranteed by function versioning and specialization-time checks. #[inline] @@ -672,9 +757,9 @@ impl Py { pub(crate) fn prepare_exact_args_frame( &self, - mut args: Vec, + args: impl ExactSizeIterator, vm: &VirtualMachine, - ) -> FrameRef { + ) -> FrameObjectRef { let code: PyRef = (*self.code).to_owned(); debug_assert_eq!(args.len(), code.arg_count as usize); @@ -697,7 +782,7 @@ impl Py { Some(ArgMapping::from_dict_exact(self.globals.clone())) }; - let frame = Frame::new( + let frame = FrameObject::new_ref( code, Scope::new(locals, self.globals.clone()), self.builtins.clone(), @@ -705,12 +790,11 @@ impl Py { Some(self.to_owned().into()), true, // Exact-args fast path is only used for non-gen/coro functions. vm, - ) - .into_ref(&vm.ctx); + ); { let fastlocals = unsafe { frame.fastlocals_mut() }; - for (slot, arg) in fastlocals.iter_mut().zip(args.drain(..)) { + for (slot, arg) in fastlocals.iter_mut().zip(args) { *slot = Some(arg); } } @@ -718,41 +802,85 @@ impl Py { frame } + pub(crate) fn invoke_prepared_exact_args( + &self, + args: impl ExactSizeIterator, + vm: &VirtualMachine, + ) -> PyResult { + let code = &*self.code; + + let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { + crate::frame::FrameLocals::lazy() + } else { + crate::frame::FrameLocals::with_locals(ArgMapping::from_dict_exact( + self.globals.clone(), + )) + }; + + let iframe = crate::frame::InterpreterFrame::new_on_datastack( + code, + &self.globals, + &self.builtins, + Some(self.as_object()), + locals, + self.closure.as_ref().map_or(&[], |c| c.as_slice()), + vm, + ); + + // Fill arguments directly into fastlocals + { + let fastlocals = iframe.localsplus.fastlocals_mut(); + for (slot, arg) in fastlocals.iter_mut().zip(args) { + *slot = Some(arg); + } + } + + let result = vm.run_frame_fast(iframe); + unsafe { + if let Some((base, size)) = iframe.release_datastack_frame() { + vm.datastack_pop_frame(base, size); + } + } + result + } + /// Fast path for calling a simple function with exact positional args. /// Skips FuncArgs allocation, prepend_arg, and fill_locals_from_args. /// Only valid when: CO_OPTIMIZED, no VARARGS, no VARKEYWORDS, no kwonlyargs, /// and nargs == co_argcount. pub fn invoke_exact_args(&self, args: Vec, vm: &VirtualMachine) -> PyResult { - let code: PyRef = (*self.code).to_owned(); - - debug_assert_eq!(args.len(), code.arg_count as usize); - debug_assert!(code.flags.contains(bytecode::CodeFlags::OPTIMIZED)); - debug_assert!( - !code - .flags - .intersects(bytecode::CodeFlags::VARARGS | bytecode::CodeFlags::VARKEYWORDS) - ); - debug_assert_eq!(code.kwonlyarg_count, 0); + debug_assert_eq!(args.len(), self.code.arg_count as usize); // Generator/coroutine code objects are SIMPLE_FUNCTION in call // specialization classification, but their call path must still // go through invoke() to produce generator/coroutine objects. - if code.flags.intersects( - bytecode::CodeFlags::GENERATOR - | bytecode::CodeFlags::COROUTINE - | bytecode::CodeFlags::ASYNC_GENERATOR, - ) { + if self.is_generator_like() { return self.invoke(FuncArgs::from(args), vm); } - let frame = self.prepare_exact_args_frame(args, vm); + self.invoke_prepared_exact_args(args.into_iter(), vm) + } - let result = vm.run_frame(frame.clone()); - unsafe { - if let Some(base) = frame.materialize_localsplus() { - vm.datastack_pop(base); - } + /// Like `invoke_exact_args`, but moves the args out of caller-provided + /// slots (all filled with `Some`), so callers can stage them in a + /// fixed-size stack buffer instead of allocating a Vec per call. + pub(crate) fn invoke_exact_args_slots( + &self, + args: &mut [Option], + vm: &VirtualMachine, + ) -> PyResult { + debug_assert_eq!(args.len(), self.code.arg_count as usize); + + let taken = args + .iter_mut() + .map(|slot| slot.take().expect("arg slot must be filled")); + // Generator/coroutine code objects are SIMPLE_FUNCTION in call + // specialization classification, but their call path must still + // go through invoke() to produce generator/coroutine objects. + if self.is_generator_like() { + let args: Vec = taken.collect(); + return self.invoke(FuncArgs::from(args), vm); } - result + self.invoke_prepared_exact_args(taken, vm) } } @@ -765,8 +893,10 @@ pub(crate) fn datastack_frame_size_bytes_for_code(code: &Py) -> Option()) + Some(crate::frame::datastack_iframe_total_bytes( + nlocalsplus, + code.max_stackdepth as usize, + )) } impl PyPayload for PyFunction { @@ -1394,7 +1524,7 @@ impl Representable for PyBoundMethod { } } -#[pyclass(module = false, name = "cell", traverse)] +#[pyclass(module = false, name = "cell", unhashable = true, traverse)] #[derive(Debug, Default)] pub(crate) struct PyCell { contents: PyMutex>, @@ -1417,8 +1547,26 @@ impl Constructor for PyCell { } } -#[pyclass(with(Constructor))] +#[pyclass(with(Constructor, Representable))] impl PyCell { + #[pyslot] + fn slot_richcompare( + zelf: &PyObject, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult> { + let (Some(zelf), Some(other)) = (zelf.downcast_ref::(), other.downcast_ref::()) + else { + return Ok(Either::B(PyComparisonValue::NotImplemented)); + }; + // compare cells by contents; empty cells come before anything else + match (zelf.get(), other.get()) { + (Some(a), Some(b)) => a.rich_compare(b, op, vm).map(Either::A), + (a, b) => Ok(Either::B(op.eval_ord(b.is_none().cmp(&a.is_none())).into())), + } + } + pub(crate) const fn new(contents: Option) -> Self { Self { contents: PyMutex::new(contents), @@ -1448,6 +1596,30 @@ impl PyCell { } } +impl Representable for PyCell { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let id = zelf.get_id(); + Ok(match zelf.get() { + Some(value) => { + let type_name = value.class().slot_name(); + // CPython renders the type name with "%.80s", which reads at + // most 80 bytes and drops a character left incomplete by the cut. + let mut end = type_name.len().min(80); + while !type_name.is_char_boundary(end) { + end -= 1; + } + format!( + "", + &type_name[..end], + value.get_id() + ) + } + None => format!(""), + }) + } +} + /// Vectorcall implementation for PyFunction (PEP 590). /// Takes owned args to avoid cloning when filling fastlocals. pub(crate) fn vectorcall_function( @@ -1461,6 +1633,16 @@ pub(crate) fn vectorcall_function( let code: &Py = &zelf.code; let has_kwargs = kwnames.is_some_and(|kw| !kw.is_empty()); + if zelf.is_jitted() { + let func_args = if has_kwargs { + FuncArgs::from_vectorcall_owned(args, nargs, kwnames) + } else { + args.truncate(nargs); + FuncArgs::from(args) + }; + return zelf.invoke(func_args, vm); + } + let is_simple = !has_kwargs && code.flags.contains(bytecode::CodeFlags::OPTIMIZED) && !code.flags.contains(bytecode::CodeFlags::VARARGS) @@ -1476,20 +1658,16 @@ pub(crate) fn vectorcall_function( // FAST PATH: simple positional-only call, exact arg count. // Move owned args directly into fastlocals — no clone needed. args.truncate(nargs); - let frame = zelf.prepare_exact_args_frame(args, vm); + let frame = zelf.prepare_exact_args_frame(args.into_iter(), vm); let result = vm.run_frame(frame.clone()); - unsafe { - if let Some(base) = frame.materialize_localsplus() { - vm.datastack_pop(base); - } - } + crate::frame::release_datastack_frame(&frame, vm); return result; } // SLOW PATH: construct FuncArgs from owned Vec and delegate to invoke() let func_args = if has_kwargs { - FuncArgs::from_vectorcall(&args, nargs, kwnames) + FuncArgs::from_vectorcall_owned(args, nargs, kwnames) } else { args.truncate(nargs); FuncArgs::from(args) diff --git a/crates/vm/src/builtins/function/jit.rs b/crates/vm/src/builtins/function/jit.rs index 8432bb5369a..96c1465d4f1 100644 --- a/crates/vm/src/builtins/function/jit.rs +++ b/crates/vm/src/builtins/function/jit.rs @@ -184,6 +184,9 @@ pub(crate) fn get_jit_args<'a>( for (name, value) in &func_args.kwargs { let arg_pos = |args: &[&PyStrInterned], name: &str| args.iter().position(|arg| arg.as_str() == name); + // Parameter names are plain identifiers, so a non-UTF-8 (surrogate) key + // can never match one. + let name = name.as_str().map_err(|_| ArgsError::NotAKeywordArg)?; if let Some(arg_idx) = arg_pos(arg_names.args, name) { if jit_args.is_set(arg_idx) { return Err(ArgsError::ArgPassedMultipleTimes); diff --git a/crates/vm/src/builtins/generator.rs b/crates/vm/src/builtins/generator.rs index 6b16917e390..b06a3a45ea7 100644 --- a/crates/vm/src/builtins/generator.rs +++ b/crates/vm/src/builtins/generator.rs @@ -7,7 +7,7 @@ use crate::{ AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, coroutine::{Coro, warn_deprecated_throw_signature}, - frame::FrameRef, + frame::FrameObjectRef, function::OptionalArg, object::{Traverse, TraverseFn}, protocol::PyIterReturn, @@ -43,7 +43,7 @@ impl PyGenerator { } #[must_use] - pub fn new(frame: FrameRef, name: PyStrRef, qualname: PyStrRef) -> Self { + pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { Self { inner: Coro::new(frame, name, qualname), } @@ -70,7 +70,7 @@ impl PyGenerator { } #[pygetset] - fn gi_frame(&self, _vm: &VirtualMachine) -> Option { + fn gi_frame(&self, _vm: &VirtualMachine) -> Option { if self.inner.closed() { None } else { @@ -85,7 +85,7 @@ impl PyGenerator { #[pygetset] fn gi_code(&self, _vm: &VirtualMachine) -> PyRef { - self.inner.frame().code.clone() + self.inner.frame().iframe().code().to_owned() } #[pygetset] @@ -99,7 +99,11 @@ impl PyGenerator { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/builtins/genericalias.rs b/crates/vm/src/builtins/genericalias.rs index b6f6012fd43..8004bd535be 100644 --- a/crates/vm/src/builtins/genericalias.rs +++ b/crates/vm/src/builtins/genericalias.rs @@ -68,7 +68,7 @@ impl Constructor for PyGenericAlias { } else { PyTuple::new_ref(vec![arguments], &vm.ctx) }; - Ok(Self::new(origin, args, false, vm)) + Self::new(origin, args, false, vm) } } @@ -92,14 +92,14 @@ impl PyGenericAlias { args: PyTupleRef, starred: bool, vm: &VirtualMachine, - ) -> Self { - let parameters = make_parameters(&args, vm); - Self { + ) -> PyResult { + let parameters = make_parameters(&args, vm)?; + Ok(Self { origin: origin.into(), args, parameters, starred, - } + }) } /// Create a GenericAlias from an origin and PyObjectRef arguments (helper for compatibility) @@ -107,7 +107,7 @@ impl PyGenericAlias { origin: impl Into, args: PyObjectRef, vm: &VirtualMachine, - ) -> Self { + ) -> PyResult { let args = if let Ok(tuple) = args.try_to_ref::(vm) { tuple.to_owned() } else { @@ -228,7 +228,7 @@ impl PyGenericAlias { vm, )?; - Ok(Self::new(zelf.origin.clone(), new_args, false, vm).into_pyobject(vm)) + Ok(Self::new(zelf.origin.clone(), new_args, false, vm)?.into_pyobject(vm)) } #[pymethod] @@ -247,7 +247,7 @@ impl PyGenericAlias { if zelf.starred { // (next, (iter(GenericAlias(origin, args)),)) let next_fn = vm.builtins.get_attr("next", vm)?; - let non_starred = Self::new(zelf.origin.clone(), zelf.args.clone(), false, vm); + let non_starred = Self::new(zelf.origin.clone(), zelf.args.clone(), false, vm)?; let iter_obj = PyGenericAliasIterator { obj: crate::common::lock::PyMutex::new(Some(non_starred.into_pyobject(vm))), } @@ -292,11 +292,11 @@ impl PyGenericAlias { } } -pub(crate) fn make_parameters(args: &Py, vm: &VirtualMachine) -> PyTupleRef { +pub(crate) fn make_parameters(args: &Py, vm: &VirtualMachine) -> PyResult { make_parameters_from_slice(args.as_slice(), vm) } -fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyTupleRef { +fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyResult { let mut parameters: Vec = Vec::with_capacity(args.len()); for arg in args { @@ -326,7 +326,9 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyTu let list = arg.downcast_ref::().unwrap(); list.borrow_vec().to_vec() }; - let sub = make_parameters_from_slice(&items, vm); + let sub = vm.with_recursion("while computing __parameters__", || { + make_parameters_from_slice(&items, vm) + })?; for sub_param in sub.iter() { if tuple_index(¶meters, sub_param).is_none() { parameters.push(sub_param.clone()); @@ -335,7 +337,7 @@ fn make_parameters_from_slice(args: &[PyObjectRef], vm: &VirtualMachine) -> PyTu } } - PyTuple::new_ref(parameters, &vm.ctx) + Ok(PyTuple::new_ref(parameters, &vm.ctx)) } #[inline] @@ -716,7 +718,7 @@ impl crate::types::IterNext for PyGenericAliasIterator { let alias = obj .downcast_ref::() .ok_or_else(|| vm.new_type_error("generic_alias_iterator expected GenericAlias"))?; - let starred = PyGenericAlias::new(alias.origin.clone(), alias.args.clone(), true, vm); + let starred = PyGenericAlias::new(alias.origin.clone(), alias.args.clone(), true, vm)?; Ok(PyIterReturn::Return(starred.into_pyobject(vm))) } } diff --git a/crates/vm/src/builtins/int.rs b/crates/vm/src/builtins/int.rs index 198c2765cdc..60463ed0d58 100644 --- a/crates/vm/src/builtins/int.rs +++ b/crates/vm/src/builtins/int.rs @@ -3,7 +3,7 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, TryFromBorrowedObject, VirtualMachine, builtins::PyUtf8StrRef, - bytes_inner::PyBytesInner, + byte::bytes_from_object, class::PyClassImpl, common::{ format::FormatSpec, @@ -305,12 +305,28 @@ impl PyInt { &self.value } + /// Extract the inline magnitude without the generic primitive-conversion path. + #[inline(always)] + pub(crate) fn try_to_i64_fast(&self) -> Option { + let bits = self.value.bits(); + if bits > i64::BITS as u64 { + return None; + } + let magnitude = self.value.iter_u64_digits().next().unwrap_or(0); + let signed_magnitude = i64::try_from(magnitude).ok(); + match self.value.sign() { + Sign::Minus if magnitude == 1u64 << 63 => Some(i64::MIN), + Sign::Minus => signed_magnitude.map(|value| -value), + Sign::NoSign | Sign::Plus => signed_magnitude, + } + } + /// Fast decimal string conversion, using i64 path when possible. #[inline] #[must_use] pub fn to_str_radix_10(&self) -> String { match self.value.to_i64() { - Some(i) => i.to_string(), + Some(i) => itoa::Buffer::new().format(i).to_owned(), None => self.value.to_string(), } } @@ -319,31 +335,41 @@ impl PyInt { #[must_use] pub fn as_u32_mask(&self) -> u32 { let v = self.as_bigint(); - v.to_u32() - .or_else(|| v.to_i32().map(|i| i as u32)) - .unwrap_or_else(|| { - let mut out = 0u32; - for digit in v.iter_u32_digits() { - out = out.wrapping_shl(32) | digit; - } - match v.sign() { - Sign::Minus => out * -1i32 as u32, - _ => out, - } - }) + let out = v.iter_u32_digits().next().unwrap_or(0); + match v.sign() { + Sign::Minus => out.wrapping_neg(), + _ => out, + } + } + + // _PyLong_AsUnsignedLongLongMask + #[must_use] + pub fn as_u64_mask(&self) -> u64 { + let v = self.as_bigint(); + let mut digits = v.iter_u32_digits(); + let out = + u64::from(digits.next().unwrap_or(0)) | (u64::from(digits.next().unwrap_or(0)) << 32); + match v.sign() { + Sign::Minus => out.wrapping_neg(), + _ => out, + } } pub fn try_to_primitive<'a, I>(&'a self, vm: &VirtualMachine) -> PyResult where I: PrimInt + TryFrom<&'a BigInt>, { - // TODO: Python 3.14+: ValueError for negative int to unsigned type - // See stdlib_socket.py socket.htonl(-1) - // - // if I::min_value() == I::zero() && self.as_bigint().sign() == Sign::Minus { - // return Err(vm.new_value_error("Cannot convert negative int".to_owned())); - // } + if I::min_value() == I::zero() && self.as_bigint().sign() == Sign::Minus { + return Err(vm.new_value_error("can't convert negative number to unsigned")); + } + + self.try_to_primitive_raw(vm) + } + pub fn try_to_primitive_raw<'a, I>(&'a self, vm: &VirtualMachine) -> PyResult + where + I: PrimInt + TryFrom<&'a BigInt>, + { I::try_from(self.as_bigint()).map_err(|_| { vm.new_overflow_error(format!( "Python int too large to convert to Rust {}", @@ -477,7 +503,9 @@ impl PyInt { return vm.ctx.new_int(rounded); } } - zelf + // No rounding to do, but an int subclass must still be normalized to an + // exact int, the way CPython's long_long() does. + zelf.__int__(vm).into_pyref() } #[pymethod] @@ -544,13 +572,13 @@ impl PyInt { vm: &VirtualMachine, ) -> PyResult> { let signed = args.signed.map_or(false, Into::into); + // PyObject_Bytes, so an iterable of ints is as good as a buffer + let bytes = bytes_from_object(vm, &args.bytes)?; let value = match (args.byteorder, signed) { - (ArgByteOrder::Big, true) => BigInt::from_signed_bytes_be(args.bytes.as_bytes()), - (ArgByteOrder::Big, false) => BigInt::from_bytes_be(Sign::Plus, args.bytes.as_bytes()), - (ArgByteOrder::Little, true) => BigInt::from_signed_bytes_le(args.bytes.as_bytes()), - (ArgByteOrder::Little, false) => { - BigInt::from_bytes_le(Sign::Plus, args.bytes.as_bytes()) - } + (ArgByteOrder::Big, true) => BigInt::from_signed_bytes_be(&bytes), + (ArgByteOrder::Big, false) => BigInt::from_bytes_be(Sign::Plus, &bytes), + (ArgByteOrder::Little, true) => BigInt::from_signed_bytes_le(&bytes), + (ArgByteOrder::Little, false) => BigInt::from_bytes_le(Sign::Plus, &bytes), }; Self::with_value(cls, value, vm) } @@ -774,7 +802,7 @@ pub(crate) struct IntOptions { #[derive(FromArgs)] struct IntFromByteArgs { - bytes: PyBytesInner, + bytes: PyObjectRef, #[pyarg(any, default = ArgByteOrder::Big)] byteorder: ArgByteOrder, #[pyarg(named, optional)] diff --git a/crates/vm/src/builtins/interpolation.rs b/crates/vm/src/builtins/interpolation.rs index a865ff390de..5d5f3774640 100644 --- a/crates/vm/src/builtins/interpolation.rs +++ b/crates/vm/src/builtins/interpolation.rs @@ -68,8 +68,7 @@ impl Constructor for PyInterpolation { .as_bytes() .iter() .exactly_one() - .ok() - .is_some_and(|s| matches!(*s, b's' | b'r' | b'a')); + .is_ok_and(|s| matches!(*s, b's' | b'r' | b'a')); if !has_flag { return Err(vm.new_value_error( "Interpolation() argument 'conversion' must be one of 's', 'a' or 'r'", @@ -145,7 +144,11 @@ impl PyInterpolation { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } diff --git a/crates/vm/src/builtins/iter.rs b/crates/vm/src/builtins/iter.rs index e9f1516b5bf..4e29df583a8 100644 --- a/crates/vm/src/builtins/iter.rs +++ b/crates/vm/src/builtins/iter.rs @@ -184,17 +184,27 @@ impl PySequenceIterator { } #[pymethod] - fn __length_hint__(&self, vm: &VirtualMachine) -> PyObjectRef { - let internal = self.internal.lock(); - if let IterStatus::Active(obj) = &internal.status { - let seq = obj.sequence_unchecked(); - seq.length(vm).map_or_else( - |_| vm.ctx.not_implemented(), - |x| PyInt::from(x).into_pyobject(vm), - ) - } else { - PyInt::from(0).into_pyobject(vm) - } + fn __length_hint__(&self, vm: &VirtualMachine) -> PyResult { + vm.with_recursion("in __length_hint__", || { + let (obj, position) = { + let internal = self.internal.lock(); + match &internal.status { + IterStatus::Active(obj) => (Some(obj.clone()), internal.position), + IterStatus::Exhausted => (None, 0), + } + }; + if let Some(obj) = obj { + let seq = obj.sequence_unchecked(); + match seq.length_opt(vm) { + Some(len) => { + len.map(|len| PyInt::from(len.saturating_sub(position)).into_pyobject(vm)) + } + None => Ok(vm.ctx.not_implemented()), + } + } else { + Ok(PyInt::from(0).into_pyobject(vm)) + } + }) } #[pymethod] diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 2bdbaf63cde..fe674a45821 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -9,18 +9,19 @@ use crate::common::lock::{ use crate::object::{Traverse, TraverseFn}; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, - builtins::PyStr, + builtins::{PyFloat, PyInt, PyStr, PyTuple}, class::PyClassImpl, convert::ToPyObject, - function::{ArgSize, FuncArgs, OptionalArg, PyComparisonValue}, + function::{ArgSize, Either, FuncArgs, OptionalArg, PyComparisonValue}, iter::PyExactSizeIterator, protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods}, recursion::ReprGuard, sequence::{MutObjectSequenceOp, OptionalRangeArgs, SequenceExt, SequenceMutExt}, sliceable::{SequenceIndex, SliceableSequenceMutOp, SliceableSequenceOp}, + sorting::timsort, types::{ AsMapping, AsSequence, Comparable, Constructor, Initializer, IterNext, Iterable, - PyComparisonOp, Representable, SelfIter, + PyComparisonOp, Representable, RichCompareFunc, SelfIter, }, vm::VirtualMachine, }; @@ -371,12 +372,15 @@ impl PyList { if let Some(index) = index.into() { // defer delete out of borrow - let is_inside_range = index < self.borrow_vec().len(); - Ok(is_inside_range.then(|| self.borrow_vec_mut().remove(index))) + let removed = { + let mut elements = self.borrow_vec_mut(); + (index < elements.len()).then(|| elements.remove(index)) + }; + drop(removed); + Ok(()) } else { Err(vm.new_value_error(format!("'{}' is not in list", needle.str(vm)?))) } - .map(drop) } fn _delitem(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult<()> { @@ -417,7 +421,11 @@ impl PyList { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -634,33 +642,226 @@ impl Representable for PyList { } } +enum Elem { + Str, + Int, + Float, + Object(RichCompareFunc), + Generic, +} + +enum PreSort { + Str, + Int, + Float, + Object(RichCompareFunc), + Tuple(Elem), + Generic, +} + +impl From for PreSort { + fn from(e: Elem) -> Self { + match e { + Elem::Str => Self::Str, + Elem::Int => Self::Int, + Elem::Float => Self::Float, + Elem::Object(f) => Self::Object(f), + Elem::Generic => Self::Generic, + } + } +} + +fn classify(class: &Py, vm: &VirtualMachine) -> Elem { + if class.is(vm.ctx.types.str_type) { + Elem::Str + } else if class.is(vm.ctx.types.int_type) { + Elem::Int + } else if class.is(vm.ctx.types.float_type) { + Elem::Float + } else if let Some(f) = class.slots.richcompare.load() { + Elem::Object(f) + } else { + Elem::Generic + } +} + +fn pre_sort_check<'a>( + mut keys: impl Iterator, + vm: &VirtualMachine, +) -> PreSort { + let Some(first) = keys.next() else { + return PreSort::Generic; + }; + + if let Some(t) = first + .downcast_ref_if_exact::(vm) + .filter(|t| !t.as_slice().is_empty()) + { + pre_sort_check_tuples(&t.as_slice()[0], keys, vm) + } else { + let class = first.class(); + if keys.all(|k| k.class().is(class)) { + classify(class, vm).into() + } else { + PreSort::Generic + } + } +} + +fn pre_sort_check_tuples<'a>( + first_elem: &PyObjectRef, + keys: impl Iterator, + vm: &VirtualMachine, +) -> PreSort { + let class = first_elem.class(); + let mut all_same_type = true; + + for k in keys { + let Some(t) = k + .downcast_ref_if_exact::(vm) + .filter(|t| !t.as_slice().is_empty()) + else { + return PreSort::Generic; + }; + if all_same_type && !t.as_slice()[0].class().is(class) { + all_same_type = false; + } + } + + let elem = if !all_same_type || class.is(vm.ctx.types.tuple_type) { + Elem::Generic + } else { + classify(class, vm) + }; + PreSort::Tuple(elem) +} + +fn str_lt(a: &PyObjectRef, b: &PyObjectRef) -> bool { + a.downcast_ref::().unwrap().as_bytes() < b.downcast_ref::().unwrap().as_bytes() +} + +fn int_lt(a: &PyObjectRef, b: &PyObjectRef) -> bool { + a.downcast_ref::().unwrap().as_bigint() < b.downcast_ref::().unwrap().as_bigint() +} + +fn float_lt(a: &PyObjectRef, b: &PyObjectRef) -> bool { + a.downcast_ref::().unwrap().to_f64() < b.downcast_ref::().unwrap().to_f64() +} + +fn object_lt( + cmp: RichCompareFunc, + a: &PyObjectRef, + b: &PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + #[allow(unpredictable_function_pointer_comparisons)] + if a.class().slots.richcompare.load() != Some(cmp) { + return a.rich_compare_bool(b, PyComparisonOp::Lt, vm); + } + match cmp(a, b, PyComparisonOp::Lt, vm)? { + Either::B(PyComparisonValue::Implemented(v)) => Ok(v), + Either::B(PyComparisonValue::NotImplemented) => { + a.rich_compare_bool(b, PyComparisonOp::Lt, vm) + } + Either::A(obj) => { + if obj.is(&vm.ctx.not_implemented) { + a.rich_compare_bool(b, PyComparisonOp::Lt, vm) + } else { + obj.try_to_bool(vm) + } + } + } +} + +fn elem_lt(elem: &Elem, a: &PyObjectRef, b: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + match elem { + Elem::Str => Ok(str_lt(a, b)), + Elem::Int => Ok(int_lt(a, b)), + Elem::Float => Ok(float_lt(a, b)), + Elem::Object(f) => object_lt(*f, a, b, vm), + Elem::Generic => a.rich_compare_bool(b, PyComparisonOp::Lt, vm), + } +} + +fn tuple_lt(elem: &Elem, a: &PyObjectRef, b: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + let a = a.downcast_ref::().unwrap().as_slice(); + let b = b.downcast_ref::().unwrap().as_slice(); + + let mut i = 0; + while i < a.len() && i < b.len() { + if !a[i].rich_compare_bool(&b[i], PyComparisonOp::Eq, vm)? { + break; + } + i += 1; + } + if i >= a.len() || i >= b.len() { + return Ok(a.len() < b.len()); + } + if i == 0 { + elem_lt(elem, &a[0], &b[0], vm) + } else { + a[i].rich_compare_bool(&b[i], PyComparisonOp::Lt, vm) + } +} + +fn timsort_by(items: &mut [T], reverse: bool, key: &K, mut lt: L) -> PyResult<()> +where + T: Clone, + K: Fn(&T) -> &PyObjectRef, + L: FnMut(&PyObjectRef, &PyObjectRef) -> PyResult, +{ + timsort(items, &mut |a, b| { + let (a, b) = if reverse { + (key(b), key(a)) + } else { + (key(a), key(b)) + }; + lt(a, b) + }) +} + +fn timsort_specialized( + vm: &VirtualMachine, + items: &mut [T], + reverse: bool, + key: K, +) -> PyResult<()> +where + T: Clone, + K: Fn(&T) -> &PyObjectRef, +{ + match pre_sort_check(items.iter().map(&key), vm) { + PreSort::Str => timsort_by(items, reverse, &key, |a, b| Ok(str_lt(a, b))), + PreSort::Int => timsort_by(items, reverse, &key, |a, b| Ok(int_lt(a, b))), + PreSort::Float => timsort_by(items, reverse, &key, |a, b| Ok(float_lt(a, b))), + PreSort::Object(cmp) => timsort_by(items, reverse, &key, |a, b| object_lt(cmp, a, b, vm)), + PreSort::Tuple(elem) => timsort_by(items, reverse, &key, |a, b| tuple_lt(&elem, a, b, vm)), + PreSort::Generic => timsort_by(items, reverse, &key, |a, b| { + a.rich_compare_bool(b, PyComparisonOp::Lt, vm) + }), + } +} + fn do_sort( vm: &VirtualMachine, values: &mut Vec, key_func: Option, reverse: bool, ) -> PyResult<()> { - // CPython uses __lt__ for all comparisons in sort. - // try_sort_by_gt expects is_gt(a, b) = true when a should come AFTER b. - let cmp = |a: &PyObjectRef, b: &PyObjectRef| { - if reverse { - // Descending: a comes after b when a < b - a.rich_compare_bool(b, PyComparisonOp::Lt, vm) - } else { - // Ascending: a comes after b when b < a - b.rich_compare_bool(a, PyComparisonOp::Lt, vm) - } - }; - if let Some(ref key_func) = key_func { let mut items = values .iter() .map(|x| Ok((x.clone(), key_func.call((x.clone(),), vm)?))) .collect::, _>>()?; - timsort::try_sort_by_gt(&mut items, |a, b| cmp(&a.1, &b.1))?; + timsort_specialized( + vm, + &mut items, + reverse, + |item: &(PyObjectRef, PyObjectRef)| &item.1, + )?; *values = items.into_iter().map(|(val, _)| val).collect(); } else { - timsort::try_sort_by_gt(values, cmp)?; + timsort_specialized(vm, values, reverse, |x: &PyObjectRef| x)? } Ok(()) diff --git a/crates/vm/src/builtins/map.rs b/crates/vm/src/builtins/map.rs index 4dda9caf211..cb8db23e640 100644 --- a/crates/vm/src/builtins/map.rs +++ b/crates/vm/src/builtins/map.rs @@ -37,9 +37,12 @@ impl Constructor for PyMap { fn py_new( _cls: &Py, (mapper, iterators, args): Self::Args, - _vm: &VirtualMachine, + vm: &VirtualMachine, ) -> PyResult { let iterators = iterators.into_vec(); + if iterators.is_empty() { + return Err(vm.new_type_error("map() must have at least two arguments.")); + } let strict = Radium::new(args.strict.unwrap_or(false)); Ok(Self { mapper, diff --git a/crates/vm/src/builtins/mappingproxy.rs b/crates/vm/src/builtins/mappingproxy.rs index c8b891f7972..dd8c689facb 100644 --- a/crates/vm/src/builtins/mappingproxy.rs +++ b/crates/vm/src/builtins/mappingproxy.rs @@ -177,7 +177,11 @@ impl PyMappingProxy { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index ec622896555..a4c29fe443c 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -7,8 +7,8 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, atomic_func, buffer::FormatSpec, - bytes_inner::bytes_to_hex, - class::PyClassImpl, + bytes_inner::{ByteInnerHexOptions, bytes_to_hex}, + class::{PyClassImpl, StaticType}, common::{ borrow::{BorrowedValue, BorrowedValueMut}, hash::PyHash, @@ -16,9 +16,9 @@ use crate::{ }, convert::ToPyObject, function::Either, - function::{FuncArgs, OptionalArg, PyComparisonValue}, + function::{ArgIndex, FuncArgs, OptionalArg, PyComparisonValue}, protocol::{ - BufferDescriptor, BufferMethods, PyBuffer, PyIterReturn, PyMappingMethods, + BufferDescriptor, BufferFlags, BufferMethods, PyBuffer, PyIterReturn, PyMappingMethods, PySequenceMethods, VecBuffer, }, sliceable::SequenceIndexOp, @@ -27,7 +27,7 @@ use crate::{ PyComparisonOp, Representable, SelfIter, }, }; -use core::{cmp::Ordering, fmt::Debug, mem::ManuallyDrop, ops::Range}; +use core::{cmp::Ordering, fmt::Debug, ops::Range}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; use rustpython_common::lock::PyMutex; @@ -37,18 +37,24 @@ pub struct PyMemoryViewNewArgs { object: PyObjectRef, } +#[derive(FromArgs)] +struct PyMemoryViewFromFlagsArgs { + object: PyObjectRef, + flags: ArgIndex, +} + #[pyclass(module = false, name = "memoryview")] #[derive(Debug)] pub struct PyMemoryView { - // avoid double release when memoryview had released the buffer before drop - buffer: ManuallyDrop, + /// One share of the acquisition this view is looking at, given up when the + /// view is released or dropped. + buffer: PyBuffer, // the released memoryview does not mean the buffer is destroyed // because the possible another memoryview is viewing from it released: AtomicCell, - // start does NOT mean the bytes before start will not be visited, - // it means the point we starting to get the absolute position via - // the needle - start: usize, + /// Forbids handing out anything that outlives this view, for the window + /// passed to `__release_buffer__`. + restricted: AtomicCell, format_spec: FormatSpec, // memoryview's options could be different from buffer's options desc: BufferDescriptor, @@ -71,13 +77,54 @@ impl PyMemoryView { FormatSpec::parse(format.as_bytes(), vm) } + /// The single native format character a cast is allowed to name, with an + /// optional `@` in front of it. get_native_fmtchar + fn native_fmtchar(format: &str) -> Option { + let format = format.strip_prefix('@').unwrap_or(format); + let [c] = *format.as_bytes() else { + return None; + }; + matches!( + c, + b'c' | b'b' + | b'B' + | b'h' + | b'H' + | b'i' + | b'I' + | b'l' + | b'L' + | b'q' + | b'Q' + | b'n' + | b'N' + | b'f' + | b'd' + | b'e' + | b'?' + | b'P' + ) + .then_some(c) + } + /// this should be the main entrance to create the memoryview /// to avoid the chained memoryview pub fn from_object(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + Self::from_object_with_flags(obj, BufferFlags::FULL_RO, vm) + } + + // PyMemoryView_FromObjectAndFlags + pub fn from_object_with_flags( + obj: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { if let Some(other) = obj.downcast_ref::() { + other.try_not_released(vm)?; + other.try_not_restricted(vm)?; Ok(other.new_view()) } else { - let buffer = PyBuffer::try_from_borrowed_object(vm, obj)?; + let buffer = PyBuffer::from_object(vm, obj, flags)?; Self::from_buffer(buffer, vm) } } @@ -93,9 +140,9 @@ impl PyMemoryView { let desc = buffer.desc.clone(); Ok(Self { - buffer: ManuallyDrop::new(buffer), + buffer, released: AtomicCell::new(false), - start: 0, + restricted: AtomicCell::new(false), format_spec, desc, hash: OnceCell::new(), @@ -120,16 +167,34 @@ impl PyMemoryView { /// this should be the only way to create a memoryview from another memoryview. #[must_use] pub fn new_view(&self) -> Self { - let zelf = Self { + Self { buffer: self.buffer.clone(), released: AtomicCell::new(false), - start: self.start, + restricted: AtomicCell::new(false), format_spec: self.format_spec.clone(), desc: self.desc.clone(), hash: OnceCell::new(), - }; - zelf.buffer.retain(); - zelf + } + } + + /// A view for a temporary that never reaches Python. It counts as no export, + /// so the exporter stays exactly as resizable as it already was, the way a + /// `Py_buffer dest = *view` copy does. + #[must_use] + fn borrowed_view(&self) -> Self { + Self { + buffer: self.buffer.detached(), + released: AtomicCell::new(false), + restricted: AtomicCell::new(false), + format_spec: self.format_spec.clone(), + desc: self.desc.clone(), + hash: OnceCell::new(), + } + } + + /// The object this view looks at, whose storage it borrows. + pub fn viewed_object(&self) -> &PyObject { + &self.buffer.obj } fn try_not_released(&self, vm: &VirtualMachine) -> PyResult<()> { @@ -140,22 +205,83 @@ impl PyMemoryView { } } + fn try_not_restricted(&self, vm: &VirtualMachine) -> PyResult<()> { + if self.restricted.load() { + Err(vm.new_value_error("cannot create new view on restricted memoryview")) + } else { + Ok(()) + } + } + + fn try_usable(&self, vm: &VirtualMachine) -> PyResult<()> { + self.try_not_released(vm)?; + self.try_not_restricted(vm) + } + + /// Reject a request this view cannot serve. memory_getbuf + fn check_buffer_request(&self, flags: BufferFlags, vm: &VirtualMachine) -> PyResult<()> { + let c_contiguous = self.desc.is_contiguous(); + flags.check_writable( + self.desc.readonly, + "memoryview: underlying buffer is not writable", + vm, + )?; + if flags.contains(BufferFlags::C_CONTIGUOUS) && !c_contiguous { + return Err(vm.new_buffer_error("memoryview: underlying buffer is not C-contiguous")); + } + if flags.contains(BufferFlags::F_CONTIGUOUS) && !self.desc.is_fortran_contiguous() { + return Err( + vm.new_buffer_error("memoryview: underlying buffer is not Fortran contiguous") + ); + } + if flags.contains(BufferFlags::ANY_CONTIGUOUS) + && !c_contiguous + && !self.desc.is_fortran_contiguous() + { + return Err(vm.new_buffer_error("memoryview: underlying buffer is not contiguous")); + } + // No exporter here produces a suboffset, so this is a guard rather than a + // reachable rejection. + if !flags.contains(BufferFlags::INDIRECT) && self.desc.has_suboffsets() { + return Err(vm.new_buffer_error("memoryview: underlying buffer requires suboffsets")); + } + if !flags.contains(BufferFlags::STRIDES) && !c_contiguous { + return Err(vm.new_buffer_error("memoryview: underlying buffer is not C-contiguous")); + } + if !flags.contains(BufferFlags::ND) && flags.intersects(BufferFlags::FORMAT) { + return Err(vm.new_buffer_error( + "memoryview: cannot cast to unsigned bytes if the format flag is present", + )); + } + Ok(()) + } + + /// The descriptor this view exports for `flags`, or an error if it cannot + /// serve the request. memory_getbuf + fn requested_desc( + &self, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + self.check_buffer_request(flags, vm)?; + Ok(self.desc.projected(flags)) + } + fn getitem_by_idx(&self, i: isize, vm: &VirtualMachine) -> PyResult { if self.desc.ndim() != 1 { return Err( vm.new_not_implemented_error("multi-dimensional sub-views are not implemented") ); } - let (shape, stride, suboffset) = self.desc.dim_desc[0]; + let (shape, _, _) = self.desc.dim_desc[0]; let index = i .wrapped_at(shape) .ok_or_else(|| vm.new_index_error("index out of range"))?; - let index = index as isize * stride + suboffset; - let pos = (index + self.start as isize) as usize; - self.unpack_single(pos, vm) + self.unpack_single(self.desc.fast_position(&[index]) as usize, vm) } fn getitem_by_slice(&self, slice: &PySlice, vm: &VirtualMachine) -> PyResult { + self.try_not_restricted(vm)?; let mut other = self.new_view(); other.init_slice(slice, 0, vm)?; other.init_len(); @@ -166,20 +292,22 @@ impl PyMemoryView { fn getitem_by_multi_idx(&self, indexes: &[isize], vm: &VirtualMachine) -> PyResult { let pos = self.pos_from_multi_index(indexes, vm)?; let bytes = self.buffer.obj_bytes(); - format_unpack(&self.format_spec, &bytes[pos..pos + self.desc.itemsize], vm) + format_unpack( + &self.format_spec, + &bytes[pos..pos + self.format_spec.size()], + vm, + ) } fn setitem_by_idx(&self, i: isize, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { if self.desc.ndim() != 1 { return Err(vm.new_not_implemented_error("sub-views are not implemented")); } - let (shape, stride, suboffset) = self.desc.dim_desc[0]; + let (shape, _, _) = self.desc.dim_desc[0]; let index = i .wrapped_at(shape) .ok_or_else(|| vm.new_index_error("index out of range"))?; - let index = index as isize * stride + suboffset; - let pos = (index + self.start as isize) as usize; - self.pack_single(pos, value, vm) + self.pack_single(self.desc.fast_position(&[index]) as usize, value, vm) } fn setitem_by_multi_idx( @@ -193,23 +321,33 @@ impl PyMemoryView { } fn pack_single(&self, pos: usize, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let mut bytes = self.buffer.obj_bytes_mut(); + // The value is converted before the destination is borrowed, because the + // conversion runs `__index__` or `__float__`, which can read or write the + // same buffer. // TODO: Optimize let data = self.format_spec.pack(vec![value], vm).map_err(|_| { vm.new_type_error(format!( "memoryview: invalid type for format '{}'", - &self.desc.format + self.desc.format )) })?; - bytes[pos..pos + self.desc.itemsize].copy_from_slice(&data); + // The conversion, and the index that produced `pos`, could have released + // the view; `pos` addresses a buffer that is no longer there. + // CHECK_RELEASED_INT_AGAIN + self.try_not_released(vm)?; + let mut bytes = self.buffer.obj_bytes_mut(); + bytes[pos..pos + self.format_spec.size()].copy_from_slice(&data); Ok(()) } fn unpack_single(&self, pos: usize, vm: &VirtualMachine) -> PyResult { + // The index that produced `pos` could have released the view. + // CHECK_RELEASED_AGAIN + self.try_not_released(vm)?; let bytes = self.buffer.obj_bytes(); // TODO: Optimize self.format_spec - .unpack(&bytes[pos..pos + self.desc.itemsize], vm) + .unpack(&bytes[pos..pos + self.format_spec.size()], vm) .map(|x| { if x.len() == 1 { x[0].to_owned() @@ -234,9 +372,7 @@ impl PyMemoryView { Ordering::Equal => (), } - let pos = self.desc.position(indexes, vm)?; - let pos = (pos + self.start as isize) as usize; - Ok(pos) + Ok(self.desc.position(indexes, vm)? as usize) } fn init_len(&mut self) { @@ -244,50 +380,38 @@ impl PyMemoryView { self.desc.len = product * self.desc.itemsize; } + /// Move this view by `delta` bytes. The offset moves, unless a dimension + /// outside `dim` is reached through a pointer, in which case its suboffset + /// does. + fn adjust_position(&mut self, dim: usize, delta: isize) { + match self.desc.dim_desc[..dim] + .iter() + .rposition(|&(_, _, suboffset)| suboffset != 0) + { + Some(n) => self.desc.dim_desc[n].2 += delta, + None => self.desc.offset += delta, + } + } + fn init_range(&mut self, range: Range, dim: usize) { let (shape, stride, _) = self.desc.dim_desc[dim]; debug_assert!(shape >= range.len()); - let mut is_adjusted = false; - for (_, _, suboffset) in self.desc.dim_desc.iter_mut().rev() { - if *suboffset != 0 { - *suboffset += stride * range.start as isize; - is_adjusted = true; - break; - } - } - if !is_adjusted { - // no suboffset set, stride must be positive - self.start += stride as usize * range.start; - } - let new_len = range.len(); - self.desc.dim_desc[dim].0 = new_len; + self.adjust_position(dim, stride * range.start as isize); + self.desc.dim_desc[dim].0 = range.len(); } + // init_slice fn init_slice(&mut self, slice: &PySlice, dim: usize, vm: &VirtualMachine) -> PyResult<()> { let (shape, stride, _) = self.desc.dim_desc[dim]; let slice = slice.to_saturated(vm)?; - let (range, step, slice_len) = slice.adjust_indices(shape); - - let mut is_adjusted_suboffset = false; - for (_, _, suboffset) in self.desc.dim_desc.iter_mut().rev() { - if *suboffset != 0 { - *suboffset += stride * range.start as isize; - is_adjusted_suboffset = true; - break; - } - } - if !is_adjusted_suboffset { - // no suboffset set, stride must be positive - self.start += stride as usize - * if step.is_negative() { - range.end - 1 - } else { - range.start - }; - } + let (start, slice_len) = slice.adjust_indices_start(shape); + + // Repeated slicing multiplies the stride by the step every time, which + // overflows after about twenty rounds; C wraps there and so does this. + self.adjust_position(dim, stride.wrapping_mul(start)); self.desc.dim_desc[dim].0 = slice_len; - self.desc.dim_desc[dim].1 *= step; + self.desc.dim_desc[dim].1 = stride.wrapping_mul(slice.step()); Ok(()) } @@ -303,10 +427,12 @@ impl PyMemoryView { if dim + 1 == self.desc.ndim() { let mut v = Vec::with_capacity(shape); for _ in 0..shape { - let pos = index + suboffset; - let pos = (pos + self.start as isize) as usize; - let obj = - format_unpack(&self.format_spec, &bytes[pos..pos + self.desc.itemsize], vm)?; + let pos = (index + suboffset) as usize; + let obj = format_unpack( + &self.format_spec, + &bytes[pos..pos + self.format_spec.size()], + vm, + )?; v.push(obj); index += stride; } @@ -330,29 +456,42 @@ impl PyMemoryView { return Ok(false); } - if let Some(other) = other.downcast_ref::() - && other.released.load() - { - return Ok(false); - } - - let other = match PyBuffer::try_from_borrowed_object(vm, other) { - Ok(buf) => buf, - Err(_) => return Ok(false), + let other = if let Some(mv) = other.downcast_ref::() { + if mv.released.load() { + return Ok(false); + } + // Another view's buffer is read where it lies rather than acquired, + // so that a restricted view still compares. memory_richcompare + let mut view = mv.buffer.detached(); + view.desc = mv.desc.clone(); + view + } else { + match PyBuffer::try_from_borrowed_object(vm, other) { + Ok(buf) => buf, + Err(_) => return Ok(false), + } }; if !is_equiv_shape(&zelf.desc, &other.desc) { return Ok(false); } - let a_itemsize = zelf.desc.itemsize; - let b_itemsize = other.desc.itemsize; let a_format_spec = &zelf.format_spec; let b_format_spec = &Self::parse_format(&other.desc.format, vm)?; + // An element is as wide as its format, which a projected descriptor can + // make narrower than the item size it steps by. + let a_itemsize = a_format_spec.size(); + let b_itemsize = b_format_spec.size(); if zelf.desc.ndim() == 0 { - let a_val = format_unpack(a_format_spec, &zelf.buffer.obj_bytes()[..a_itemsize], vm)?; - let b_val = format_unpack(b_format_spec, &other.obj_bytes()[..b_itemsize], vm)?; + let a_pos = zelf.desc.offset as usize; + let b_pos = other.desc.offset as usize; + let a_bytes = zelf.buffer.obj_bytes(); + let a_val = format_unpack(a_format_spec, &a_bytes[a_pos..a_pos + a_itemsize], vm)?; + drop(a_bytes); + let b_bytes = other.obj_bytes(); + let b_val = format_unpack(b_format_spec, &b_bytes[b_pos..b_pos + b_itemsize], vm)?; + drop(b_bytes); return vm.bool_eq(&a_val, &b_val); } @@ -361,9 +500,8 @@ impl PyMemoryView { let a_bytes = zelf.buffer.obj_bytes(); let b_bytes = other.obj_bytes(); zelf.desc.zip_eq(&other.desc, false, |a_range, b_range| { - let a_range = (a_range.start + zelf.start as isize) as usize - ..(a_range.end + zelf.start as isize) as usize; - let b_range = b_range.start as usize..b_range.end as usize; + let a_range = a_range.start as usize..a_range.start as usize + a_itemsize; + let b_range = b_range.start as usize..b_range.start as usize + b_itemsize; let a_val = match format_unpack(a_format_spec, &a_bytes[a_range], vm) { Ok(val) => val, Err(e) => { @@ -384,39 +522,17 @@ impl PyMemoryView { ret } - fn obj_bytes(&self) -> BorrowedValue<'_, [u8]> { - if self.desc.is_contiguous() { - BorrowedValue::map(self.buffer.obj_bytes(), |x| { - &x[self.start..self.start + self.desc.len] - }) - } else { - BorrowedValue::map(self.buffer.obj_bytes(), |x| &x[self.start..]) - } - } - - fn obj_bytes_mut(&self) -> BorrowedValueMut<'_, [u8]> { - if self.desc.is_contiguous() { - BorrowedValueMut::map(self.buffer.obj_bytes_mut(), |x| { - &mut x[self.start..self.start + self.desc.len] - }) - } else { - BorrowedValueMut::map(self.buffer.obj_bytes_mut(), |x| &mut x[self.start..]) - } - } - fn as_contiguous(&self) -> Option> { self.desc.is_contiguous().then(|| { - BorrowedValue::map(self.buffer.obj_bytes(), |x| { - &x[self.start..self.start + self.desc.len] - }) + let range = self.desc.contiguous_range(); + BorrowedValue::map(self.buffer.obj_bytes(), |x| &x[range]) }) } fn _as_contiguous_mut(&self) -> Option> { self.desc.is_contiguous().then(|| { - BorrowedValueMut::map(self.buffer.obj_bytes_mut(), |x| { - &mut x[self.start..self.start + self.desc.len] - }) + let range = self.desc.contiguous_range(); + BorrowedValueMut::map(self.buffer.obj_bytes_mut(), |x| &mut x[range]) }) } @@ -427,9 +543,7 @@ impl PyMemoryView { buf.reserve(self.desc.len); let bytes = &*self.buffer.obj_bytes(); self.desc.for_each_segment(true, |range| { - let start = (range.start + self.start as isize) as usize; - let end = (range.end + self.start as isize) as usize; - buf.extend_from_slice(&bytes[start..end]); + buf.extend_from_slice(&bytes[range.start as usize..range.end as usize]); }) } } @@ -454,27 +568,7 @@ impl PyMemoryView { let mut data = vec![]; self.append_to(&mut data); - if self.desc.ndim() == 0 { - return VecBuffer::from(data) - .into_ref(&vm.ctx) - .into_pybuffer_with_descriptor(self.desc.clone()); - } - - let mut dim_desc = self.desc.dim_desc.clone(); - dim_desc.last_mut().unwrap().1 = self.desc.itemsize as isize; - dim_desc.last_mut().unwrap().2 = 0; - for i in (0..dim_desc.len() - 1).rev() { - dim_desc[i].1 = dim_desc[i + 1].1 * dim_desc[i + 1].0 as isize; - dim_desc[i].2 = 0; - } - - let desc = BufferDescriptor { - len: self.desc.len, - readonly: self.desc.readonly, - itemsize: self.desc.itemsize, - format: self.desc.format.clone(), - dim_desc, - }; + let desc = self.desc.contiguous(); VecBuffer::from(data) .into_ref(&vm.ctx) @@ -493,7 +587,7 @@ impl Py { return Err(vm.new_not_implemented_error("sub-view are not implemented")); } - let mut dest = self.new_view(); + let mut dest = self.borrowed_view(); dest.init_slice(slice, 0, vm)?; dest.init_len(); @@ -508,15 +602,11 @@ impl Py { }; }; - let src = if let Some(src) = src.downcast_ref::() { - if self.buffer.obj.is(&src.buffer.obj) { - src.to_contiguous(vm) - } else { - AsBuffer::as_buffer(src, vm)? - } - } else { - PyBuffer::try_from_object(vm, src)? - }; + // PyObject_GetBuffer(value, &src, PyBUF_FULL_RO) + let src = PyBuffer::try_from_object(vm, src)?; + // Acquiring the source ran `__buffer__`, which can release this view. + // copy_single: CHECK_RELEASED_INT_AGAIN + self.try_not_released(vm)?; if !is_equiv_structure(&src.desc, &dest.desc) { return Err(vm.new_value_error( @@ -524,11 +614,21 @@ impl Py { )); } + // copy_buffer reads the source as it stood before the copy began, which an + // overlapping assignment depends on and which also keeps the two borrows + // below off the same storage. + let src = if root_exporter(&src).is(&root_exporter(&dest.buffer)) { + let owned = src.to_contiguous(vm); + drop(src); + owned + } else { + src + }; + let mut bytes_mut = dest.buffer.obj_bytes_mut(); let src_bytes = src.obj_bytes(); dest.desc.zip_eq(&src.desc, true, |a_range, b_range| { - let a_range = (a_range.start + dest.start as isize) as usize - ..(a_range.end + dest.start as isize) as usize; + let a_range = a_range.start as usize..a_range.end as usize; let b_range = b_range.start as usize..b_range.end as usize; bytes_mut[a_range].copy_from_slice(&src_bytes[b_range]); false @@ -554,10 +654,25 @@ impl Py { )] impl PyMemoryView { #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } + #[pyclassmethod] + fn _from_flags( + _cls: PyTypeRef, + args: PyMemoryViewFromFlagsArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let flags = + BufferFlags::from_bits_retain(args.flags.as_ref().try_to_primitive::(vm)? as u32); + Self::from_object_with_flags(&args.object, flags, vm).map(|mv| mv.into_ref(&vm.ctx)) + } + #[pymethod] pub fn release(&self) { if self.released.compare_exchange(false, true).is_ok() { @@ -567,7 +682,14 @@ impl PyMemoryView { #[pygetset] fn obj(&self, vm: &VirtualMachine) -> PyResult { - self.try_not_released(vm).map(|_| self.buffer.obj.clone()) + self.try_not_released(vm)?; + // A window over a buffer being released exposes no exporter, like a + // Py_buffer whose obj is NULL. + Ok(if self.buffer.obj.downcastable::() { + vm.ctx.none() + } else { + self.buffer.obj.clone() + }) } #[pygetset] @@ -643,7 +765,8 @@ impl PyMemoryView { #[pygetset] fn contiguous(&self, vm: &VirtualMachine) -> PyResult { - self.try_not_released(vm).map(|_| self.desc.is_contiguous()) + self.try_not_released(vm) + .map(|_| self.desc.is_contiguous() || self.desc.is_fortran_contiguous()) } #[pygetset] @@ -653,9 +776,8 @@ impl PyMemoryView { #[pygetset] fn f_contiguous(&self, vm: &VirtualMachine) -> PyResult { - // TODO: column-major order self.try_not_released(vm) - .map(|_| self.desc.ndim() <= 1 && self.desc.is_contiguous()) + .map(|_| self.desc.is_fortran_contiguous()) } #[pymethod] @@ -678,7 +800,7 @@ impl PyMemoryView { if let Some(tuple) = needle.downcast_ref::() && tuple.is_empty() { - return zelf.unpack_single(0, vm); + return zelf.unpack_single(zelf.desc.offset as usize, vm); } return Err(vm.new_type_error("invalid indexing of 0-dim memory")); } @@ -709,42 +831,64 @@ impl PyMemoryView { } #[pymethod] - fn tobytes(&self, vm: &VirtualMachine) -> PyResult { + fn tobytes(&self, args: ToBytesArgs, vm: &VirtualMachine) -> PyResult { self.try_not_released(vm)?; + let order = match &args.order { + None => Order::C, + Some(order) => match order.to_str() { + Some("C") => Order::C, + Some("F") => Order::Fortran, + Some("A") => Order::Any, + _ => return Err(vm.new_value_error("order must be 'C', 'F' or 'A'")), + }, + }; + let mut v = vec![]; - self.append_to(&mut v); + // 'A' asks for the memory as it is laid out, which is what appending a + // contiguous view does. Only a Fortran walk of a view that is not + // already Fortran-contiguous reorders anything, and a view of fewer + // than two dimensions has one layout under either name. + if order == Order::Fortran && self.desc.ndim() > 1 { + v.reserve(self.desc.len); + let bytes = &*self.buffer.obj_bytes(); + self.desc.for_each_segment_fortran(|range| { + v.extend_from_slice(&bytes[range.start as usize..range.end as usize]); + }); + } else { + self.append_to(&mut v); + } Ok(PyBytes::from(v).into_ref(&vm.ctx)) } #[pymethod] - fn tolist(&self, vm: &VirtualMachine) -> PyResult { + // memory_tolist + fn tolist(&self, vm: &VirtualMachine) -> PyResult { self.try_not_released(vm)?; let bytes = self.buffer.obj_bytes(); if self.desc.ndim() == 0 { - return Ok(vm.ctx.new_list(vec![format_unpack( + // A 0-dim view holds one element, which is what it unpacks to. + let pos = self.desc.offset as usize; + return format_unpack( &self.format_spec, - &bytes[..self.desc.itemsize], + &bytes[pos..pos + self.format_spec.size()], vm, - )?])); + ); } - self._to_list(&bytes, 0, 0, vm) + self._to_list(&bytes, self.desc.offset, 0, vm) + .map(Into::into) } #[pymethod] fn toreadonly(&self, vm: &VirtualMachine) -> PyResult> { - self.try_not_released(vm)?; + self.try_usable(vm)?; let mut other = self.new_view(); other.desc.readonly = true; Ok(other.into_ref(&vm.ctx)) } #[pymethod] - fn hex( - &self, - sep: OptionalArg>, - bytes_per_sep: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { + fn hex(&self, options: ByteInnerHexOptions, vm: &VirtualMachine) -> PyResult { + let ByteInnerHexOptions { sep, bytes_per_sep } = options; self.try_not_released(vm)?; self.contiguous_or_collect(|x| bytes_to_hex(x, sep, bytes_per_sep, vm)) } @@ -808,31 +952,45 @@ impl PyMemoryView { fn cast_to_1d(&self, format: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { let format_str = format.as_str(); + let Some(dest_char) = Self::native_fmtchar(format_str) else { + return Err(vm.new_value_error( + "memoryview: destination format must be a native single character format prefixed with an optional '@'", + )); + }; + // One side has to be bytes. Casting between two item types would + // reinterpret the items rather than re-divide the memory, and the + // source items were written by something that chose their type. + let source_is_bytes = Self::native_fmtchar(&self.desc.format).is_some_and(is_byte_fmtchar); + if !source_is_bytes && !is_byte_fmtchar(dest_char) { + return Err(vm.new_type_error("memoryview: cannot cast between two non-byte formats")); + } let format_spec = Self::parse_format(format_str, vm)?; let itemsize = format_spec.size(); if !self.desc.len.is_multiple_of(itemsize) { return Err(vm.new_type_error("memoryview: length is not a multiple of itemsize")); } - Ok(Self { + let zelf = Self { buffer: self.buffer.clone(), released: AtomicCell::new(false), - start: self.start, + restricted: AtomicCell::new(false), format_spec, desc: BufferDescriptor { len: self.desc.len, + offset: self.desc.offset, readonly: self.desc.readonly, itemsize, format: format_str.to_owned().into(), dim_desc: vec![(self.desc.len / itemsize, itemsize as isize, 0)], }, hash: OnceCell::new(), - }) + }; + Ok(zelf) } #[pymethod] fn cast(&self, args: CastArgs, vm: &VirtualMachine) -> PyResult> { - self.try_not_released(vm)?; + self.try_usable(vm)?; if !self.desc.is_contiguous() { return Err(vm.new_type_error("memoryview: casts are restricted to C-contiguous views")); } @@ -870,10 +1028,14 @@ impl PyMemoryView { let mut other = self.cast_to_1d(format, vm)?; let itemsize = other.desc.itemsize; - // 0 ndim is single item + // 0 ndim is single item, so the buffer has to be that one item if shape_ndim == 0 { + if itemsize != other.desc.len { + return Err( + vm.new_type_error("memoryview: product(shape) * itemsize != buffer size") + ); + } other.desc.dim_desc = vec![]; - other.desc.len = itemsize; return Ok(other.into_ref(&vm.ctx)); } @@ -881,7 +1043,19 @@ impl PyMemoryView { let mut dim_descriptor = Vec::with_capacity(shape_ndim); for x in shape { - let x = usize::try_from_borrowed_object(vm, x)?; + let x = x + .downcast_ref::() + .ok_or_else(|| { + vm.new_type_error("memoryview.cast(): elements of shape must be integers") + })? + .try_to_primitive::(vm) + .ok() + .filter(|x| *x > 0) + .ok_or_else(|| { + vm.new_value_error( + "memoryview.cast(): elements of shape must be integers > 0", + ) + })?; if x > isize::MAX as usize / product_shape { return Err(vm.new_value_error("memoryview.cast(): product(shape) > SSIZE_MAX")); @@ -929,11 +1103,11 @@ impl Py { if self.desc.ndim() == 0 { // TODO: merge branches when we got conditional if let if needle.is(&vm.ctx.ellipsis) { - return self.pack_single(0, value, vm); + return self.pack_single(self.desc.offset as usize, value, vm); } else if let Some(tuple) = needle.downcast_ref::() && tuple.is_empty() { - return self.pack_single(0, value, vm); + return self.pack_single(self.desc.offset as usize, value, vm); } return Err(vm.new_type_error("invalid indexing of 0-dim memory")); } @@ -955,6 +1129,20 @@ impl Py { } } +#[derive(FromArgs)] +struct ToBytesArgs { + #[pyarg(any, default)] + order: Option, +} + +/// The layout a copy of a view is written in. +#[derive(PartialEq, Eq)] +enum Order { + C, + Fortran, + Any, +} + #[derive(FromArgs)] struct CastArgs { #[pyarg(any)] @@ -1002,33 +1190,43 @@ impl TryFromObject for SubscriptNeedle { } static BUFFER_METHODS: BufferMethods = BufferMethods { - obj_bytes: |buffer| buffer.obj_as::().obj_bytes(), - obj_bytes_mut: |buffer| buffer.obj_as::().obj_bytes_mut(), - release: |buffer| buffer.obj_as::().buffer.release(), - retain: |buffer| buffer.obj_as::().buffer.retain(), + obj_bytes: |buffer| buffer.obj_as::().buffer.obj_bytes(), + obj_bytes_mut: |buffer| buffer.obj_as::().buffer.obj_bytes_mut(), + // memory_releasebuf / memory_getbuf: a consumer's export of this view is a + // share of the acquisition the view is looking at. + release: |buffer| buffer.obj_as::().buffer.release_share(), + retain: |buffer| buffer.obj_as::().buffer.retain_share(), }; impl AsBuffer for PyMemoryView { - fn as_buffer(zelf: &Py, vm: &VirtualMachine) -> PyResult { - if zelf.released.load() { - Err(vm.new_value_error("operation forbidden on released memoryview object")) - } else { - Ok(PyBuffer::new( - zelf.to_owned().into(), - zelf.desc.clone(), - &BUFFER_METHODS, - )) - } + const RELEASE_BUFFER: bool = true; + + // memory_getbuf + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { + let zelf = zelf + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("unexpected payload for as_buffer"))?; + zelf.try_usable(vm)?; + Ok(PyBuffer::new( + zelf.to_owned().into(), + zelf.requested_desc(flags, vm)?, + &BUFFER_METHODS, + )) } -} -impl Drop for PyMemoryView { - fn drop(&mut self) { - if self.released.load() { - unsafe { self.buffer.drop_without_release() }; - } else { - unsafe { ManuallyDrop::drop(&mut self.buffer) }; - } + fn as_buffer(zelf: &Py, vm: &VirtualMachine) -> PyResult { + zelf.try_usable(vm)?; + // memory_getbuf: *view = *base — the descriptor already says where the + // view starts. + Ok(PyBuffer::new( + zelf.to_owned().into(), + zelf.desc.clone(), + &BUFFER_METHODS, + )) } } @@ -1103,6 +1301,13 @@ impl Hashable for PyMemoryView { if !zelf.desc.readonly { return Err(vm.new_value_error("cannot hash writable memoryview object")); } + // The hash is over the bytes, so it agrees with the hash of the same + // bytes only where an item is a byte. + if !Self::native_fmtchar(&zelf.desc.format).is_some_and(is_byte_fmtchar) { + return Err( + vm.new_value_error("memoryview: hashing is restricted to formats 'B', 'b' or 'c'") + ); + } let val = zelf.contiguous_or_collect(|bytes| vm.state.hash_secret.hash_bytes(bytes)); let _ = zelf.hash.set(val); Ok(*zelf.hash.get().unwrap()) @@ -1131,6 +1336,211 @@ impl Representable for PyMemoryView { pub(crate) fn init(ctx: &'static Context) { PyMemoryView::extend_class(ctx, ctx.types.memoryview_type); PyMemoryViewIterator::extend_class(ctx, ctx.types.memoryviewiterator_type); + let wrapper_type = PyBufferWrapper::init_builtin_type(); + // bufferwrapper_as_buffer: bf_releasebuffer and no bf_getbuffer, so the type + // has `__release_buffer__` but no `__buffer__`. + wrapper_type.slots.has_release_buffer.store(true); + PyBufferWrapper::extend_class(ctx, wrapper_type); + PyBufferWindow::extend_class(ctx, PyBufferWindow::init_builtin_type()); +} + +#[pyclass(module = false, name = "_buffer_wrapper")] +#[derive(Debug)] +struct PyBufferWrapper { + // bw->obj: the object whose `__buffer__` produced the view + exporter: PyObjectRef, + // bw->mv: the memoryview `__buffer__` returned, dropped with the last export + returned_mv: PyMutex>>, + /// Memory of `returned_mv`, held on behalf of every live export. The wrapper + /// forwards shares of it rather than owning one. + view: PyBuffer, + /// Exports handed out for this wrapper; the wrapper is spent at zero. + exports: AtomicCell, +} + +impl PyPayload for PyBufferWrapper { + fn class(_ctx: &Context) -> &'static Py { + Self::static_type() + } +} + +#[pyclass(flags(DISALLOW_INSTANTIATION))] +impl PyBufferWrapper {} + +static BUFFER_WRAPPER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| buffer.obj_as::().view.obj_bytes(), + obj_bytes_mut: |buffer| buffer.obj_as::().view.obj_bytes_mut(), + retain: |buffer| { + let wrapper = buffer.obj_as::(); + wrapper.exports.fetch_add(1); + wrapper.view.retain_share(); + }, + // bufferwrapper_releasebuf + release: |buffer| { + let wrapper = buffer.obj_as::(); + wrapper.view.release_share(); + if wrapper.exports.fetch_sub(1) != 1 { + return; + } + let Some(mv) = wrapper.returned_mv.lock().take() else { + return; + }; + // A native release runs when the memoryview itself is torn down; only a + // Python-level hook on a foreign exporter has to be called here. + if !mv.buffer.obj.is(&wrapper.exporter) + && wrapper.exporter.class().slots.python_release_buffer.load() + { + call_python_release_buffer(&wrapper.exporter, mv.clone()); + } + // Py_CLEAR(bw->mv): the view outlives this only if user code kept it. + drop(mv); + }, +}; + +// Read-only window over an exporter, handed to `__release_buffer__`. It owns no +// export, like a `Py_buffer` whose `obj` is NULL, so releasing it is inert and +// cannot recurse back into the hook. +#[pyclass(module = false, name = "_buffer_window")] +#[derive(Debug)] +struct PyBufferWindow { + source: PyBuffer, +} + +impl PyPayload for PyBufferWindow { + fn class(_ctx: &Context) -> &'static Py { + Self::static_type() + } +} + +#[pyclass(flags(DISALLOW_INSTANTIATION))] +impl PyBufferWindow {} + +static BUFFER_WINDOW_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| buffer.obj_as::().source.obj_bytes(), + obj_bytes_mut: |buffer| buffer.obj_as::().source.obj_bytes_mut(), + retain: |_buffer| {}, + release: |_buffer| {}, +}; + +/// The object that ultimately owns the bytes a buffer reads, seen through the +/// payloads that only forward to another export: a view, the wrapper holding what +/// a `__buffer__` returned, and the window handed to `__release_buffer__`. +/// +/// Two buffers that resolve to the same object address the same storage, so +/// borrowing one for writing while the other is borrowed for reading would +/// deadlock on it. +fn root_exporter(buffer: &PyBuffer) -> PyObjectRef { + let mut obj = buffer.obj.clone(); + loop { + let next = if let Some(view) = obj.downcast_ref::() { + view.buffer.obj.clone() + } else if let Some(wrapper) = obj.downcast_ref::() { + wrapper.view.obj.clone() + } else if let Some(window) = obj.downcast_ref::() { + window.source.obj.clone() + } else { + return obj; + }; + obj = next; + } +} + +// slot_bf_getbuffer +pub(crate) fn buffer_from_python_getbuffer( + obj: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, +) -> PyResult { + let flags_obj = vm.ctx.new_int(flags.bits() as i32); + let ret = vm.call_special_method(obj, identifier!(vm, __buffer__), (flags_obj,))?; + let mv = ret + .downcast::() + .map_err(|_| vm.new_type_error("__buffer__ returned non-memoryview object"))?; + + // PyObject_GetBuffer(ret, buffer, flags): the returned view has to satisfy + // the request in its own right. + mv.try_usable(vm)?; + let desc = mv.requested_desc(flags, vm)?; + let wrapper = PyBufferWrapper { + exporter: obj.to_owned(), + view: mv.buffer.detached(), + returned_mv: PyMutex::new(Some(mv)), + exports: AtomicCell::new(0), + } + .into_pyobject(vm); + + // PyBuffer::new retains once through BUFFER_WRAPPER_METHODS. + Ok(PyBuffer::new(wrapper, desc, &BUFFER_WRAPPER_METHODS)) +} + +// wrap_releasebuffer +pub(crate) fn release_buffer_from_python( + obj: &PyObject, + mv: PyRef, + vm: &VirtualMachine, +) -> PyResult<()> { + let view_obj = &mv.buffer.obj; + if view_obj.downcastable::() { + // A window exports nothing, so there is nothing left to release, as for + // a `Py_buffer` whose `obj` is NULL. + return Ok(()); + } + let exports_obj = view_obj.is(obj) + || view_obj + .downcast_ref::() + .is_some_and(|wrapper| wrapper.exporter.is(obj)); + if !exports_obj { + return Err(vm.new_value_error("memoryview's buffer is not this object")); + } + if mv.released.load() { + return Err(vm.new_value_error("memoryview's buffer has already been released")); + } + mv.release(); + Ok(()) +} + +// releasebuffer_call_python, for a buffer acquired from a native exporter +pub(crate) fn release_buffer_call_python(buffer: &PyBuffer) { + crate::vm::thread::try_with_current_vm(|vm| { + let exporter = buffer.obj.clone(); + let window = PyBufferWindow { + source: buffer.detached(), + } + .into_pyobject(vm); + let window = PyBuffer::new(window, buffer.desc.clone(), &BUFFER_WINDOW_METHODS); + let mv = match PyMemoryView::from_buffer(window, vm) { + Ok(mv) => mv, + Err(exc) => { + let msg = format!( + "Exception ignored in bf_releasebuffer of {}", + exporter.class().name() + ); + return vm.run_unraisable(exc, Some(msg), vm.ctx.none()); + } + }; + // Restricted, so user code cannot keep anything addressing the memory + // that is about to go away. + mv.restricted.store(true); + let mv = mv.into_ref(&vm.ctx); + call_python_release_buffer(&exporter, mv.clone()); + // The window does not outlive the release it was made for. + mv.release(); + }); +} + +fn call_python_release_buffer(exporter: &PyObject, mv: PyRef) { + crate::vm::thread::try_with_current_vm(|vm| { + let method = vm.get_special_method(exporter, identifier!(vm, __release_buffer__)); + if let Ok(Some(method)) = method + && let Err(exc) = method.invoke((mv,), vm) + { + let msg = format!( + "Exception ignored in __release_buffer__ of {}", + exporter.class().name() + ); + vm.run_unraisable(exc, Some(msg), vm.ctx.none()); + } + }); } fn format_unpack( @@ -1147,6 +1557,10 @@ fn format_unpack( }) } +/// Whether `ch` names a format whose items are single bytes. +const fn is_byte_fmtchar(ch: u8) -> bool { + matches!(ch, b'c' | b'b' | b'B') +} fn is_equiv_shape(a: &BufferDescriptor, b: &BufferDescriptor) -> bool { if a.ndim() != b.ndim() { return false; diff --git a/crates/vm/src/builtins/mod.rs b/crates/vm/src/builtins/mod.rs index 27b52dacb15..f08a2b46721 100644 --- a/crates/vm/src/builtins/mod.rs +++ b/crates/vm/src/builtins/mod.rs @@ -28,6 +28,8 @@ pub use filter::PyFilter; pub(crate) mod float; pub use float::PyFloat; pub(crate) mod frame; +pub(crate) mod frame_locals_proxy; +pub use frame_locals_proxy::FrameLocalsProxy; pub(crate) mod function; pub use function::{PyBoundMethod, PyFunction}; pub(crate) mod generator; @@ -97,7 +99,12 @@ pub use zip::PyZip; pub(crate) mod union_; pub use union_::{PyUnion, make_union}; pub(crate) mod descriptor; +pub use descriptor::{ + MemberGetter, MemberKind, MemberSetter, PyDescriptorOwned, PyMemberDef as DescriptorMemberDef, + PyMemberDescriptor, +}; +pub use float::float_from_string as parse_float_from_string; pub use float::try_to_bigint as try_f64_to_bigint; pub use int::try_to_float as try_bigint_to_f64; diff --git a/crates/vm/src/builtins/module.rs b/crates/vm/src/builtins/module.rs index 90410907276..8fa5259f705 100644 --- a/crates/vm/src/builtins/module.rs +++ b/crates/vm/src/builtins/module.rs @@ -231,7 +231,7 @@ impl Py { } else { // Check for uninitialized submodule let submodule_initializing = - is_uninitialized_submodule(mod_name_str.as_ref(), name, vm); + is_uninitialized_submodule(mod_name_str.as_deref(), name, vm); if submodule_initializing { Err(vm.new_attribute_error(format!( "cannot access submodule '{name}' of module '{mod_display}' \ @@ -303,6 +303,10 @@ impl PyModule { let dict = dict_attr .downcast::() .map_err(|_| vm.new_type_error(".__dict__ is not a dictionary"))?; + // PEP 562: honor a module-level __dir__ if one is defined + if let Some(dir_func) = dict.get_item_opt(identifier!(vm, __dir__), vm)? { + return dir_func.call((), vm)?.try_to_value(vm); + } let attrs = dict.into_iter().map(|(k, _v)| k).collect(); Ok(attrs) } @@ -461,29 +465,29 @@ pub(crate) fn init(context: &'static Context) { /// Check if {module_name}.{name} is an uninitialized submodule in sys.modules. fn is_uninitialized_submodule( - module_name: Option<&String>, + module_name: Option<&str>, name: &Py, vm: &VirtualMachine, ) -> bool { - let mod_name = match module_name { - Some(n) => n.as_str(), - None => return false, + let Some(mod_name) = module_name else { + return false; }; - let full_name = format!("{mod_name}.{name}"); - let sys_modules = match vm.sys_module.get_attr("modules", vm).ok() { - Some(m) => m, - None => return false, + + let Ok(sys_modules) = vm.sys_module.get_attr("modules", vm) else { + return false; }; - let sub_mod = match sys_modules.get_item(&full_name, vm).ok() { - Some(m) => m, - None => return false, + + let full_name = format!("{mod_name}.{name}"); + let Ok(sub_mod) = sys_modules.get_item(&full_name, vm) else { + return false; }; - let spec = match sub_mod.get_attr("__spec__", vm).ok() { - Some(s) if !vm.is_none(&s) => s, + + let spec = match sub_mod.get_attr("__spec__", vm) { + Ok(s) if !vm.is_none(&s) => s, _ => return false, }; + spec.get_attr("_initializing", vm) - .ok() - .and_then(|v| v.try_to_bool(vm).ok()) + .and_then(|v| v.try_to_bool(vm)) .unwrap_or(false) } diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 147e215a0cb..151b753b2a0 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -65,34 +65,24 @@ impl Constructor for PyBaseObject { } // Ensure that all abstract methods are implemented before instantiating instance. - if let Some(abs_methods) = cls.get_attr(identifier!(vm, __abstractmethods__)) - && let Some(unimplemented_abstract_method_count) = abs_methods.length_opt(vm) - { + if let Some(abs_methods) = cls.get_attr(identifier!(vm, __abstractmethods__)) { let methods: Vec = abs_methods.try_to_value(vm)?; - let methods: String = Itertools::intersperse( - methods.iter().map(|name| name.as_str().to_owned()), - "', '".to_owned(), - ) - .collect(); - - let unimplemented_abstract_method_count = unimplemented_abstract_method_count?; - let name = cls.name().to_string(); - - match unimplemented_abstract_method_count { - 0 => {} - 1 => { - return Err(vm.new_type_error(format!( - "class {name} without an implementation for abstract method '{methods}'" - ))); - } - 2.. => { - return Err(vm.new_type_error(format!( - "class {name} without an implementation for abstract methods '{methods}'" - ))); - } - // TODO: remove `allow` when redox build doesn't complain about it - #[allow(unreachable_patterns)] - _ => unreachable!(), + let unimplemented_abstract_method_count = methods.len(); + if unimplemented_abstract_method_count > 0 { + let methods: String = Itertools::intersperse( + methods.iter().map(|name| name.as_str().to_owned()), + "', '".to_owned(), + ) + .collect(); + let name = cls.name().to_string(); + let noun = if unimplemented_abstract_method_count == 1 { + "method" + } else { + "methods" + }; + return Err(vm.new_type_error(format!( + "class {name} without an implementation for abstract {noun} '{methods}'" + ))); } } @@ -132,8 +122,12 @@ impl Initializer for PyBaseObject { let typ = zelf.class(); let object_type = &vm.ctx.types.object_type; - let typ_init = typ.slots.init.load().map(|f| f as usize); - let object_init = object_type.slots.init.load().map(|f| f as usize); + let typ_init = typ.slots.init.load().map(|f| crate::types::fn_addr(f)); + let object_init = object_type + .slots + .init + .load() + .map(|f| crate::types::fn_addr(f)); // if (type->tp_init != object_init) → first error if typ_init != object_init { @@ -346,23 +340,7 @@ impl PyBaseObject { Ok(res) } - /// Implement setattr(self, name, value). - #[pymethod] - fn __setattr__( - obj: PyObjectRef, - name: PyStrRef, - value: PyObjectRef, - vm: &VirtualMachine, - ) -> PyResult<()> { - obj.generic_setattr(&name, PySetterValue::Assign(value), vm) - } - - /// Implement delattr(self, name). - #[pymethod] - fn __delattr__(obj: PyObjectRef, name: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { - obj.generic_setattr(&name, PySetterValue::Delete, vm) - } - + // __setattr__ and __delattr__ are added as slot wrappers by add_operators. #[pyslot] pub(crate) fn slot_setattro( obj: &PyObject, @@ -461,39 +439,7 @@ impl PyBaseObject { && !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE); // FIXME(#1979) cls instances might have a payload if both_mutable || both_module { - let has_dict = - |typ: &Py| typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT); - let has_weakref = - |typ: &Py| typ.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF); - // Compare slots tuples - let slots_equal = match ( - current_cls - .heaptype_ext - .as_ref() - .and_then(|e| e.slots.as_ref()), - cls.heaptype_ext.as_ref().and_then(|e| e.slots.as_ref()), - ) { - (Some(a), Some(b)) => { - a.len() == b.len() - && a.iter() - .zip(b.iter()) - .all(|(x, y)| x.as_wtf8() == y.as_wtf8()) - } - (None, None) => true, - _ => false, - }; - if current_cls.slots.basicsize != cls.slots.basicsize - || !slots_equal - || has_dict(current_cls) != has_dict(&cls) - || has_weakref(current_cls) != has_weakref(&cls) - || current_cls.slots.member_count != cls.slots.member_count - { - return Err(vm.new_type_error(format!( - "__class__ assignment: '{}' object layout differs from '{}'", - cls.name(), - current_cls.name() - ))); - } + super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?; instance.set_class(cls, vm); Ok(()) } else { @@ -513,17 +459,14 @@ impl PyBaseObject { } /// Return getattr(self, name). + /// + /// __getattribute__ is added as a slot wrapper by add_operators. #[pyslot] pub(crate) fn getattro(obj: &PyObject, name: &Py, vm: &VirtualMachine) -> PyResult { vm_trace!("object.__getattribute__({:?}, {:?})", obj, name); obj.as_object().generic_getattr(name, vm) } - #[pymethod] - fn __getattribute__(obj: PyObjectRef, name: PyStrRef, vm: &VirtualMachine) -> PyResult { - Self::getattro(&obj, &name, vm) - } - #[pymethod] fn __reduce__(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { common_reduce(obj, 0, vm) diff --git a/crates/vm/src/builtins/property.rs b/crates/vm/src/builtins/property.rs index cff5a8a60d0..65ae48222fa 100644 --- a/crates/vm/src/builtins/property.rs +++ b/crates/vm/src/builtins/property.rs @@ -1,7 +1,7 @@ /*! Python `property` descriptor class. */ -use super::{PyStrRef, PyType}; +use super::PyType; use crate::common::lock::PyRwLock; use crate::function::{IntoFuncArgs, PosArgs}; use crate::{ @@ -41,8 +41,6 @@ pub struct PropertyArgs { fdel: Option, #[pyarg(any, default)] doc: Option, - #[pyarg(any, default)] - name: Option, } impl GetDescriptor for PyProperty { @@ -221,7 +219,6 @@ impl PyProperty { fset: new_setter.or_else(|| zelf.fset()), fdel: new_deleter.or_else(|| zelf.fdel()), doc, - name: None, }; // Create new property using py_new and init @@ -401,7 +398,6 @@ impl Initializer for PyProperty { *zelf.getter.write() = args.fget; *zelf.setter.write() = args.fset; *zelf.deleter.write() = args.fdel; - *zelf.name.write() = args.name.map(|a| a.as_object().to_owned()); zelf.getter_doc.store(getter_doc, Ordering::Relaxed); Ok(()) diff --git a/crates/vm/src/builtins/range.rs b/crates/vm/src/builtins/range.rs index 415d34fdb05..5962f90e521 100644 --- a/crates/vm/src/builtins/range.rs +++ b/crates/vm/src/builtins/range.rs @@ -364,7 +364,11 @@ impl PyRange { // TODO: Uncomment when Python adds __class_getitem__ to range // #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/builtins/set.rs b/crates/vm/src/builtins/set.rs index 8b38223fce1..d737612b158 100644 --- a/crates/vm/src/builtins/set.rs +++ b/crates/vm/src/builtins/set.rs @@ -195,6 +195,26 @@ impl PySetInner { Ok(set) } + /// Build a set from an arbitrary object, reusing stored hashes when the + /// source is a set/frozenset/dict. + fn from_object(iterable: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let set = Self::default(); + set.update_internal(iterable, vm)?; + Ok(set) + } + + /// Elements of `obj` with their stored hashes, or `None` if `obj` keeps + /// none and must be iterated generically. Mirrors the `PyAnySet_Check` / + /// `PyDict_CheckExact` fast paths in CPython's `set_update_internal`. + fn cached_hashes(obj: &PyObject, vm: &VirtualMachine) -> Option> { + if let Some(set) = extract_set(obj) { + Some(set.content.keys_with_hashes()) + } else { + obj.downcast_ref_if_exact::(vm) + .map(|dict| dict._as_dict_inner().keys_with_hashes()) + } + } + fn fold_op( &self, others: impl core::iter::Iterator, @@ -223,7 +243,20 @@ impl PySetInner { } fn contains(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { - self.retry_op_with_frozenset(needle, vm, |needle, vm| self.content.contains(vm, needle)) + let result = self + .retry_op_with_frozenset(needle, vm, |needle, vm| self.content.contains(vm, needle)); + Self::wrap_unhashable_error(result, needle, vm) + } + + /// [`Self::contains`] with a known hash. Such a needle came out of a + /// set/dict, so it is hashable and needs no frozenset retry. + fn contains_known_hash( + &self, + needle: &PyObject, + hash: PyHash, + vm: &VirtualMachine, + ) -> PyResult { + self.content.contains_known_hash(vm, needle, hash) } fn compare(&self, other: &Self, op: PyComparisonOp, vm: &VirtualMachine) -> PyResult { @@ -249,6 +282,12 @@ impl PySetInner { pub(super) fn union(&self, other: ArgIterable, vm: &VirtualMachine) -> PyResult { let set = self.clone(); + if let Some(elements) = Self::cached_hashes(other.as_object(), vm) { + for (item, hash) in elements { + set.add_known_hash(item, hash, vm)?; + } + return Ok(set); + } for item in other.iter(vm)? { set.add(item?, vm)?; } @@ -258,6 +297,14 @@ impl PySetInner { pub(super) fn intersection(&self, other: ArgIterable, vm: &VirtualMachine) -> PyResult { let set = Self::default(); + if let Some(elements) = Self::cached_hashes(other.as_object(), vm) { + for (obj, hash) in elements { + if self.contains_known_hash(&obj, hash, vm)? { + set.add_known_hash(obj, hash, vm)?; + } + } + return Ok(set); + } for item in other.iter(vm)? { let obj = item?; if self.contains(&obj, vm)? { @@ -269,6 +316,12 @@ impl PySetInner { pub(super) fn difference(&self, other: ArgIterable, vm: &VirtualMachine) -> PyResult { let set = self.copy(); + if let Some(elements) = Self::cached_hashes(other.as_object(), vm) { + for (item, hash) in elements { + set.content.delete_if_exists_known_hash(vm, &*item, hash)?; + } + return Ok(set); + } for item in other.iter(vm)? { set.content.delete_if_exists(vm, &*item?)?; } @@ -282,6 +335,16 @@ impl PySetInner { ) -> PyResult { let new_inner = self.clone(); + if let Some(elements) = Self::cached_hashes(other.as_object(), vm) { + // the source is already duplicate-free + for (item, hash) in elements { + new_inner + .content + .delete_or_insert_known_hash(vm, &item, hash, ())?; + } + return Ok(new_inner); + } + // We want to remove duplicates in other let other_set = Self::from_iter(other.iter(vm)?, vm)?; @@ -323,19 +386,31 @@ impl PySetInner { } fn repr(&self, class_name: Option<&str>, vm: &VirtualMachine) -> PyResult { - collection_repr(class_name, "{", "}", self.elements().iter(), vm) + let empty = format!("{}()", class_name.unwrap_or("set")); + collection_repr(class_name, "{", "}", &empty, self.elements().iter(), vm) } fn add(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - self.content.insert(vm, &*item, ()) + let result = self.content.insert(vm, &*item, ()); + Self::wrap_unhashable_error(result, &item, vm) + } + + /// [`Self::add`] with a known hash. + fn add_known_hash(&self, item: PyObjectRef, hash: PyHash, vm: &VirtualMachine) -> PyResult<()> { + let result = self.content.insert_known_hash(vm, &*item, hash, ()); + Self::wrap_unhashable_error(result, &item, vm) } fn remove(&self, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - self.retry_op_with_frozenset(&item, vm, |item, vm| self.content.delete(vm, item)) + let result = + self.retry_op_with_frozenset(&item, vm, |item, vm| self.content.delete(vm, item)); + Self::wrap_unhashable_error(result, &item, vm) } fn discard(&self, item: &PyObject, vm: &VirtualMachine) -> PyResult { - self.retry_op_with_frozenset(item, vm, |item, vm| self.content.delete_if_exists(vm, item)) + let result = self + .retry_op_with_frozenset(item, vm, |item, vm| self.content.delete_if_exists(vm, item)); + Self::wrap_unhashable_error(result, item, vm) } fn clear(&self) { @@ -386,15 +461,15 @@ impl PySetInner { } fn merge_set(&self, any_set: AnySet, vm: &VirtualMachine) -> PyResult<()> { - for item in any_set.as_inner().elements() { - self.add(item, vm)?; + for (item, hash) in any_set.as_inner().content.keys_with_hashes() { + self.add_known_hash(item, hash, vm)?; } Ok(()) } fn merge_dict(&self, dict: PyDictRef, vm: &VirtualMachine) -> PyResult<()> { - for (key, _value) in dict { - self.add(key, vm)?; + for (key, hash) in dict._as_dict_inner().keys_with_hashes() { + self.add_known_hash(key, hash, vm)?; } Ok(()) } @@ -406,8 +481,8 @@ impl PySetInner { ) -> PyResult<()> { let temp_inner = self.fold_op(others, Self::intersection, vm)?; self.clear(); - for obj in temp_inner.elements() { - self.add(obj, vm)?; + for (obj, hash) in temp_inner.content.keys_with_hashes() { + self.add_known_hash(obj, hash, vm)?; } Ok(()) } @@ -418,6 +493,12 @@ impl PySetInner { vm: &VirtualMachine, ) -> PyResult<()> { for iterable in others { + if let Some(elements) = Self::cached_hashes(iterable.as_object(), vm) { + for (item, hash) in elements { + self.content.delete_if_exists_known_hash(vm, &*item, hash)?; + } + continue; + } let items = iterable.iter(vm)?.collect::, _>>()?; for item in items { self.content.delete_if_exists(vm, &*item)?; @@ -432,6 +513,14 @@ impl PySetInner { vm: &VirtualMachine, ) -> PyResult<()> { for iterable in others { + if let Some(elements) = Self::cached_hashes(iterable.as_object(), vm) { + // the source is already duplicate-free + for (item, hash) in elements { + self.content + .delete_or_insert_known_hash(vm, &item, hash, ())?; + } + continue; + } // We want to remove duplicates in iterable let iterable_set = Self::from_iter(iterable.iter(vm)?, vm)?; for item in iterable_set.elements() { @@ -442,28 +531,14 @@ impl PySetInner { } fn hash(&self, vm: &VirtualMachine) -> PyResult { - // Work to increase the bit dispersion for closely spaced hash values. - // This is important because some use cases have many combinations of a - // small number of elements with nearby hashes so that many distinct - // combinations collapse to only a handful of distinct hash values. - const fn _shuffle_bits(h: u64) -> u64 { - ((h ^ 89869747) ^ (h.wrapping_shl(16))).wrapping_mul(3644798167) - } - // Factor in the number of active entries - let mut hash: u64 = (self.len() as u64 + 1).wrapping_mul(1927868237); - // Xor-in shuffled bits from every entry's hash field because xor is - // commutative and a frozenset hash should be independent of order. - hash = self.content.try_fold_keys(hash, |h, element| { - Ok(h ^ _shuffle_bits(element.hash(vm)? as u64)) - })?; - // Disperse patterns arising in nested frozen-sets - hash ^= (hash >> 11) ^ (hash >> 25); - hash = hash.wrapping_mul(69069).wrapping_add(907133923); - // -1 is reserved as an error code - if hash == u64::MAX { - hash = 590923713; - } - Ok(hash as PyHash) + let hasher = self.content.try_fold_keys( + hash::FrozenSetHash::new(self.len()), + |mut hasher, element| { + hasher.add(element.hash(vm)?); + Ok(hasher) + }, + )?; + Ok(hasher.finish()) } // Run operation, on failure, if item is a set/set subclass, convert it @@ -502,6 +577,25 @@ impl PySetInner { }) }) } + + fn wrap_unhashable_error( + result: PyResult, + item: &PyObject, + vm: &VirtualMachine, + ) -> PyResult { + match result { + Err(cause) if cause.fast_isinstance(vm.ctx.exceptions.type_error) => { + let message = cause.as_object().str(vm)?; + let err = vm.new_type_error(format!( + "cannot use '{}' as a set element ({message})", + item.class().name() + )); + err.set___cause__(Some(cause)); + Err(err) + } + result => result, + } + } } fn extract_set(obj: &PyObject) -> Option<&PySetInner> { @@ -512,6 +606,23 @@ fn extract_set(obj: &PyObject) -> Option<&PySetInner> { }) } +/// Elements of `obj` with their stored hashes, or `None` unless `obj` is exactly +/// a `set` or `frozenset` — `PyAnySet_CheckExact`, where [`extract_set`] is the +/// subclass-inclusive `PyAnySet_Check`. +pub(super) fn exact_set_keys_with_hashes( + obj: &PyObject, + vm: &VirtualMachine, +) -> Option> { + let inner = obj + .downcast_ref_if_exact::(vm) + .map(|set| &set.inner) + .or_else(|| { + obj.downcast_ref_if_exact::(vm) + .map(|frozen| &frozen.inner) + })?; + Some(inner.content.keys_with_hashes()) +} + fn reduce_set(zelf: &PyObject, vm: &VirtualMachine) -> (PyTypeRef, PyTupleRef, Option) { ( zelf.class().to_owned(), @@ -771,7 +882,11 @@ impl PySet { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -943,43 +1058,72 @@ impl Representable for PySet { } impl Constructor for PyFrozenSet { - type Args = Vec; + type Args = OptionalArg; fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let iterable: OptionalArg = args.bind(vm)?; + let is_exact_frozenset = cls.is(vm.ctx.types.frozenset_type); + let is_frozenset_init = { + let cls_init = cls + .slots + .init + .load() + .map(|init| crate::types::fn_addr(init)); + let frozenset_init = vm + .ctx + .types + .frozenset_type + .slots + .init + .load() + .map(|init| crate::types::fn_addr(init)); + cls_init == frozenset_init + }; // Optimizations for exact frozenset type - if cls.is(vm.ctx.types.frozenset_type) { + let iterable_opt = if is_exact_frozenset || is_frozenset_init { + let iterable: OptionalArg = args.bind(vm)?; + // Return exact frozenset as-is - if let OptionalArg::Present(ref input) = iterable - && let Ok(fs) = input.clone().downcast_exact::(vm) + if is_exact_frozenset + && let OptionalArg::Present(input) = &iterable + && input.class().is(vm.ctx.types.frozenset_type) { - return Ok(fs.into_pyref().into()); + return Ok(input.clone()); } - // Return empty frozenset singleton - if iterable.is_missing() { - return Ok(vm.ctx.empty_frozenset.clone().into()); - } - } - - let elements: Vec = if let OptionalArg::Present(iterable) = iterable { - iterable.try_to_value(vm)? + iterable } else { - vec![] + match &args.args[..] { + [] => OptionalArg::Missing, + [iterable] => OptionalArg::Present(iterable.clone()), + slice => { + return Err(vm.new_type_error(format!( + "frozenset expected at most 1 argument, got {}", + slice.len() + ))); + } + } }; - // Return empty frozenset singleton for exact frozenset types (when iterable was empty) - if elements.is_empty() && cls.is(vm.ctx.types.frozenset_type) { + let payload = Self::py_new(&cls, iterable_opt, vm)?; + + // Return empty frozenset singleton + if is_exact_frozenset && payload.inner.len() == 0 { return Ok(vm.ctx.empty_frozenset.clone().into()); } - let payload = Self::py_new(&cls, elements, vm)?; payload.into_ref_with_type(vm, cls).map(Into::into) } - fn py_new(_cls: &Py, elements: Self::Args, vm: &VirtualMachine) -> PyResult { - Self::from_iter(vm, elements) + fn py_new(_cls: &Py, iterable: Self::Args, vm: &VirtualMachine) -> PyResult { + let inner = match iterable { + OptionalArg::Present(iterable) => PySetInner::from_object(iterable, vm)?, + OptionalArg::Missing => PySetInner::default(), + }; + Ok(Self { + inner, + ..Default::default() + }) } } @@ -1147,7 +1291,11 @@ impl PyFrozenSet { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -1385,16 +1533,16 @@ impl IterNext for PySetIterator { fn next(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { let mut internal = zelf.internal.lock(); let next = if let IterStatus::Active(dict) = &internal.status { - if dict.has_changed_size(&zelf.size) { - internal.status = IterStatus::Exhausted; - return Err(vm.new_runtime_error("set changed size during iteration")); - } - match dict.next_entry(internal.position) { - Some((position, key, _)) => { + match dict.next_entry_checked(internal.position, &zelf.size, |key, ()| key.clone()) { + Err(crate::dict_inner::DictChanged) => { + internal.status = IterStatus::Exhausted; + return Err(vm.new_runtime_error("set changed size during iteration")); + } + Ok(Some((position, key))) => { internal.position = position; PyIterReturn::Return(key) } - None => { + Ok(None) => { internal.status = IterStatus::Exhausted; PyIterReturn::StopIteration(None) } diff --git a/crates/vm/src/builtins/slice.rs b/crates/vm/src/builtins/slice.rs index 3c5f13b382d..026b976b65e 100644 --- a/crates/vm/src/builtins/slice.rs +++ b/crates/vm/src/builtins/slice.rs @@ -260,7 +260,11 @@ impl PySlice { // TODO: Uncomment when Python adds __class_getitem__ to slice // #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/builtins/staticmethod.rs b/crates/vm/src/builtins/staticmethod.rs index 3a8d451b3dc..8ae31b67b5c 100644 --- a/crates/vm/src/builtins/staticmethod.rs +++ b/crates/vm/src/builtins/staticmethod.rs @@ -68,6 +68,7 @@ impl PyStaticMethod { callable: PyMutex::new(callable), } } + #[deprecated(note = "use PyStaticMethod::new(...).into_ref() instead")] pub fn new_ref(callable: PyObjectRef, ctx: &Context) -> PyRef { Self::new(callable).into_ref(ctx) @@ -162,7 +163,11 @@ impl PyStaticMethod { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -178,7 +183,7 @@ impl Callable for PyStaticMethod { impl Representable for PyStaticMethod { fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let callable = zelf.callable.lock().repr(vm).unwrap(); + let callable = zelf.callable.lock().repr(vm)?; let class = Self::class(&vm.ctx); match ( diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index 69109c943e8..6e774f7e652 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -9,7 +9,7 @@ use super::{ use crate::{ AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, TryFromBorrowedObject, VirtualMachine, - anystr::{self, AnyStr, AnyStrContainer, AnyStrWrapper, adjust_indices}, + anystr::{self, AnyStr, AnyStrContainer, AnyStrWrapper, StringRange, adjust_indices}, atomic_func, bytes_inner::{swapcase_ascii, title_ascii}, cformat::cformat_string, @@ -23,20 +23,23 @@ use crate::{ function::{ArgIterable, ArgSize, FuncArgs, OptionalArg, OptionalOption, PyComparisonValue}, intern::PyInterned, object::{MaybeTraverse, Traverse, TraverseFn}, - protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, + protocol::{ + BufferFlags, PyBuffer, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods, + }, sequence::SequenceExt, sliceable::{SequenceIndex, SliceableSequenceOp}, types::{ AsMapping, AsNumber, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable, PyComparisonOp, Representable, SelfIter, }, - utils::VecFmtWriter, }; use alloc::{borrow::Cow, fmt}; use ascii::{AsciiChar, AsciiStr, AsciiString}; use bstr::ByteSlice; +use core::ffi::CStr; use core::{char, mem, ops::Range}; use itertools::Itertools; +use memchr::memchr; use num_traits::ToPrimitive; use rustpython_common::{ ascii, @@ -48,13 +51,7 @@ use rustpython_common::{ wtf8::{CodePoint, Wtf8, Wtf8Buf, Wtf8Concat}, }; -use icu_casemap::{CaseMapper, TitlecaseMapper}; -use icu_locale::LanguageIdentifier; -use icu_properties::props::{ - BidiClass, BinaryProperty, CaseIgnorable, Cased, EnumeratedProperty, GeneralCategory, - GeneralCategoryGroup, Lowercase, NumericType, Uppercase, XidContinue, XidStart, -}; -use writeable::Writeable; +use rustpython_unicode::{self as unicode, case}; impl<'a> TryFromBorrowedObject<'a> for String { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { @@ -446,15 +443,24 @@ impl Constructor for PyStr { if input.fast_isinstance(vm.ctx.types.str_type) { return Err(vm.new_type_error("decoding str is not supported")); } - if !input.fast_isinstance(vm.ctx.types.bytes_type) - && !input.fast_isinstance(vm.ctx.types.bytearray_type) - && crate::protocol::PyBuffer::try_from_borrowed_object(vm, &input).is_err() + let input = if input.fast_isinstance(vm.ctx.types.bytes_type) + || input.fast_isinstance(vm.ctx.types.bytearray_type) { - return Err(vm.new_type_error(format!( - "decoding to str: need a bytes-like object, {} found", - input.class().name() - ))); - } + input + } else { + // PyUnicode_FromEncodedObject: whatever an exporter + // complains about, the argument is simply not bytes-like. + let buffer = PyBuffer::from_object(vm, &input, BufferFlags::SIMPLE) + .map_err(|_| { + vm.new_type_error(format!( + "decoding to str: need a bytes-like object, {} found", + input.class().name() + )) + })?; + vm.ctx + .new_bytes(buffer.contiguous_or_collect(<[u8]>::to_vec)) + .into() + }; let enc_str = encoding.as_ref().map_or("utf-8", |e| e.as_str()); let s = vm .state @@ -551,6 +557,13 @@ impl PyStr { } } + /// Check string bytes for interior NULs. + #[inline] + #[must_use] + pub fn contains_nuls(&self) -> bool { + memchr(b'\0', self.as_bytes()).is_some() + } + pub fn to_string_lossy(&self) -> Cow<'_, str> { self.to_str() .map_or_else(|| self.as_wtf8().to_string_lossy(), Cow::Borrowed) @@ -710,6 +723,20 @@ impl PyStr { self.data.char_len() } + /// The byte offset the `index`-th character starts at, or the string's byte + /// length if `index` is at or past its end. + #[inline] + pub fn char_index_to_byte(&self, index: usize) -> usize { + self.data.char_index_to_byte(index) + } + + /// The character index of the character starting at byte offset `bytepos`, + /// which must be a character boundary at or before the end. + #[inline] + pub fn byte_to_char_index(&self, bytepos: usize) -> usize { + self.data.byte_to_char_index(bytepos) + } + #[pymethod] #[inline(always)] pub const fn isascii(&self) -> bool { @@ -752,22 +779,8 @@ impl PyStr { fn casefold(&self) -> Self { match self.as_str_kind() { PyKindStr::Ascii(s) => s.to_ascii_lowercase().into(), - PyKindStr::Utf8(s) => CaseMapper::new().fold_string(s).to_string().into(), - PyKindStr::Wtf8(w) => { - let mut out = VecFmtWriter(Vec::with_capacity(w.len())); - let mapper = CaseMapper::new(); - for chunk in w.as_bytes().utf8_chunks() { - mapper - .fold(chunk.valid()) - .write_to(&mut out) - .expect("Writing to an in-memory buffer cannot fail."); - out.0.extend(chunk.invalid()); - } - // SAFETY: - // * CaseMapper only produces valid UTF-8 - // * Surrogates are appended as-is - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) }.into() - } + PyKindStr::Utf8(s) => unicode::case::casefold_str(s).into(), + PyKindStr::Wtf8(w) => unicode::case::casefold_wtf8(w).into(), } } @@ -791,40 +804,8 @@ impl PyStr { } s.into() } - PyKindStr::Utf8(s) => { - let mut chars = s.char_indices(); - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - titlecase_first(s, &mut chars, &mut out); - for (i, ch) in chars { - lowercase_or_sigma(ch, s, i, &mut out); - } - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } - PyKindStr::Wtf8(s) => { - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - let mut chunks = s.as_bytes().utf8_chunks(); - - if let Some(first) = chunks.next() { - let s = first.valid(); - let mut chars = s.char_indices(); - titlecase_first(s, &mut chars, &mut out); - for (i, ch) in chars { - lowercase_or_sigma(ch, s, i, &mut out); - } - out.0.extend(first.invalid()); - } - // This loop is only hit if the WTF-8 buffer contains invalid Unicode. Otherwise, - // everything is handled above without chunking. - for chunk in chunks { - let s = chunk.valid(); - for (i, ch) in s.char_indices() { - lowercase_or_sigma(ch, s, i, &mut out); - } - out.0.extend(chunk.invalid()); - } - - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } + PyKindStr::Utf8(s) => case::capitalize_str(s).into(), + PyKindStr::Wtf8(s) => case::capitalize_wtf8(s), } } @@ -961,11 +942,12 @@ impl PyStr { #[pymethod] fn endswith(&self, options: anystr::StartsEndsWithArgs, vm: &VirtualMachine) -> PyResult { - let (affix, substr) = - match options.prepare(self.as_wtf8(), self.len(), |s, r| s.get_chars(r)) { - Some(x) => x, - None => return Ok(false), - }; + let (affix, substr) = match options.prepare(self.as_wtf8(), self.len(), |s, r| { + &s[self.data.char_range_to_bytes(r)] + }) { + Some(x) => x, + None => return Ok(false), + }; substr.py_starts_ends_with( &affix, "endswith", @@ -981,11 +963,12 @@ impl PyStr { options: anystr::StartsEndsWithArgs, vm: &VirtualMachine, ) -> PyResult { - let (affix, substr) = - match options.prepare(self.as_wtf8(), self.len(), |s, r| s.get_chars(r)) { - Some(x) => x, - None => return Ok(false), - }; + let (affix, substr) = match options.prepare(self.as_wtf8(), self.len(), |s, r| { + &s[self.data.char_range_to_bytes(r)] + }) { + Some(x) => x, + None => return Ok(false), + }; substr.py_starts_ends_with( &affix, "startswith", @@ -1011,44 +994,25 @@ impl PyStr { #[pymethod] fn isalnum(&self) -> bool { - !self.data.is_empty() - && self.char_all(|c| { - GeneralCategoryGroup::Letter - .union(GeneralCategoryGroup::Number) - .contains(GeneralCategory::for_char(c)) - }) + !self.data.is_empty() && self.char_all(unicode::classify::is_alnum) } #[pymethod] fn isnumeric(&self) -> bool { - !self.data.is_empty() - && self.char_all(|c| { - [ - NumericType::Decimal, - NumericType::Digit, - NumericType::Numeric, - ] - .contains(&NumericType::for_char(c)) - }) + !self.data.is_empty() && self.char_all(unicode::classify::is_numeric) } #[pymethod] fn isdigit(&self) -> bool { - !self.data.is_empty() - && self.char_all(|c| { - [NumericType::Digit, NumericType::Decimal].contains(&NumericType::for_char(c)) - }) + !self.data.is_empty() && self.char_all(unicode::classify::is_digit) } #[pymethod] fn isdecimal(&self) -> bool { - !self.data.is_empty() - && self.char_all(|c| { - matches!(GeneralCategory::for_char(c), GeneralCategory::DecimalNumber) - }) + !self.data.is_empty() && self.char_all(unicode::classify::is_decimal) } - fn __mod__(&self, values: PyObjectRef, vm: &VirtualMachine) -> PyResult { + pub fn __mod__(&self, values: PyObjectRef, vm: &VirtualMachine) -> PyResult { cformat_string(vm, self.as_wtf8(), values) } @@ -1094,23 +1058,8 @@ impl PyStr { PyKindStr::Ascii(_) => unsafe { Wtf8Buf::from_bytes_unchecked(title_ascii(self.as_bytes())) }, - PyKindStr::Utf8(s) => { - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - titlecase_string(s, &mut out); - // SAFETY: `s` is valid UTF-8 and titlecase_string only works on Unicode. - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } - PyKindStr::Wtf8(s) => { - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - for chunk in s.as_bytes().utf8_chunks() { - titlecase_string(chunk.valid(), &mut out); - out.0.extend(chunk.invalid()); - } - // SAFETY: - // * `s` is valid WTF-8; surrogate bytes were appended without processing. - // * TitlecaseMapper produces valid UTF-8. - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } + PyKindStr::Utf8(s) => case::title_str(s).into(), + PyKindStr::Wtf8(s) => case::title_wtf8(s), } } @@ -1121,31 +1070,14 @@ impl PyStr { // SAFETY: ASCII is valid Unicode and swapcase_ascii does not produce non-ASCII. Wtf8Buf::from_bytes_unchecked(swapcase_ascii(s.as_bytes())) }, - PyKindStr::Utf8(s) => { - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - swapcase_utf8(s, &mut out); - // SAFETY: `s` is valid UTF-8 and swapcase_utf8 only works on Unicode. - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } - PyKindStr::Wtf8(s) => { - let mut out = VecFmtWriter(Vec::with_capacity(s.len())); - for chunk in s.as_bytes().utf8_chunks() { - swapcase_utf8(chunk.valid(), &mut out); - out.0.extend(chunk.invalid()); - } - // SAFETY: - // * `s` is valid WTF-8; surrogate bytes were appended without processing. - // * swapcase_utf8 produces valid UTF-8. - unsafe { Wtf8Buf::from_bytes_unchecked(out.0) } - } + PyKindStr::Utf8(s) => case::swapcase_str(s).into(), + PyKindStr::Wtf8(s) => case::swapcase_wtf8(s), } } #[pymethod] fn isalpha(&self) -> bool { - !self.data.is_empty() - && self - .char_all(|c| GeneralCategoryGroup::Letter.contains(GeneralCategory::for_char(c))) + !self.data.is_empty() && self.char_all(unicode::classify::is_alpha) } #[pymethod] @@ -1175,23 +1107,12 @@ impl PyStr { #[pymethod] fn isprintable(&self) -> bool { - self.char_all(|c| c == '\u{0020}' || rustpython_literal::char::is_printable(c)) + self.char_all(unicode::classify::is_printable) } #[pymethod] fn isspace(&self) -> bool { - !self.data.is_empty() - && self.char_all(|c| { - matches!( - GeneralCategory::for_char(c), - GeneralCategory::SpaceSeparator - ) || matches!( - BidiClass::for_char(c), - BidiClass::WhiteSpace - | BidiClass::ParagraphSeparator - | BidiClass::SegmentSeparator - ) - }) + !self.data.is_empty() && self.char_all(unicode::classify::is_space) } // Return true if all cased characters in the string are lowercase and there is at least one cased character, false otherwise. @@ -1266,47 +1187,57 @@ impl PyStr { Ok(vm.ctx.new_str(joined)) } - // FIXME: two traversals of str is expensive + /// The bytes the character range `range` spans and the byte offset it + /// starts at, or `None` if the range is inverted. + /// + /// The bounds go through the string's character index, so reaching a range + /// deep in the subject costs a lookup rather than a walk to it. #[inline] - fn _to_char_idx(r: &Wtf8, byte_idx: usize) -> usize { - r[..byte_idx].code_points().count() + fn char_range_bytes(&self, range: Range) -> Option<(usize, &Wtf8)> { + if !range.is_normal() { + return None; + } + let bytes = self.data.char_range_to_bytes(range); + Some((bytes.start, &self.as_wtf8()[bytes])) } + /// Searches the character range `range` with `find`, which answers in bytes + /// relative to the range, and reports the hit as a character index. #[inline] fn _find(&self, args: FindArgs, find: F) -> Option where F: Fn(&Wtf8, &Wtf8) -> Option, { let (sub, range) = args.get_value(self.len()); - self.as_wtf8().py_find(sub.as_wtf8(), range, find) + let (start, haystack) = self.char_range_bytes(range)?; + let found = find(haystack, sub.as_wtf8())?; + Some(self.byte_to_char_index(start + found)) } #[pymethod] fn find(&self, args: FindArgs) -> isize { - self._find(args, |r, s| Some(Self::_to_char_idx(r, r.find(s)?))) - .map_or(-1, |v| v as isize) + self._find(args, Wtf8::find).map_or(-1, |v| v as isize) } #[pymethod] fn rfind(&self, args: FindArgs) -> isize { - self._find(args, |r, s| Some(Self::_to_char_idx(r, r.rfind(s)?))) - .map_or(-1, |v| v as isize) + self._find(args, Wtf8::rfind).map_or(-1, |v| v as isize) } #[pymethod] fn index(&self, args: FindArgs, vm: &VirtualMachine) -> PyResult { - self._find(args, |r, s| Some(Self::_to_char_idx(r, r.find(s)?))) + self._find(args, Wtf8::find) .ok_or_else(|| vm.new_value_error("substring not found")) } #[pymethod] fn rindex(&self, args: FindArgs, vm: &VirtualMachine) -> PyResult { - self._find(args, |r, s| Some(Self::_to_char_idx(r, r.rfind(s)?))) + self._find(args, Wtf8::rfind) .ok_or_else(|| vm.new_value_error("substring not found")) } #[pymethod] - fn partition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult { + pub fn partition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult { let (front, has_mid, back) = self.as_wtf8().py_partition( sep.as_wtf8(), || self.as_wtf8().splitn(2, sep.as_wtf8()), @@ -1325,7 +1256,7 @@ impl PyStr { } #[pymethod] - fn rpartition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult { + pub fn rpartition(&self, sep: PyStrRef, vm: &VirtualMachine) -> PyResult { let (back, has_mid, front) = self.as_wtf8().py_partition( sep.as_wtf8(), || self.as_wtf8().rsplitn(2, sep.as_wtf8()), @@ -1352,9 +1283,7 @@ impl PyStr { let mut cased = false; let mut previous_is_cased = false; for c in self.as_wtf8().code_points().map(CodePoint::to_char_lossy) { - if c.is_uppercase() - || GeneralCategoryGroup::TitlecaseLetter.contains(GeneralCategory::for_char(c)) - { + if c.is_uppercase() || case::is_titlecase(c) { if previous_is_cased { return false; } @@ -1376,16 +1305,28 @@ impl PyStr { #[pymethod] fn count(&self, args: FindArgs) -> usize { let (needle, range) = args.get_value(self.len()); - self.as_wtf8() - .py_count(needle.as_wtf8(), range, |h, n| h.find_iter(n).count()) + let chars = range.len(); + self.char_range_bytes(range).map_or(0, |(_, haystack)| { + if needle.is_empty() { + // An empty needle sits between every pair of characters and at + // both ends, so it occurs once more than the range holds + // characters. Counting it in the bytes would answer in encoded + // positions instead. + chars + 1 + } else { + haystack.find_iter(needle.as_wtf8()).count() + } + }) } #[pymethod] - fn zfill(&self, width: isize) -> Wtf8Buf { - unsafe { - // SAFETY: this is safe-guaranteed because the original self.as_wtf8() is valid wtf8 - Wtf8Buf::from_bytes_unchecked(self.as_wtf8().py_zfill(width)) - } + fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult { + let filled = self + .as_wtf8() + .py_zfill(width) + .ok_or_else(|| vm.new_memory_error(""))?; + // SAFETY: this is safe-guaranteed because the original self.as_wtf8() is valid wtf8 + Ok(unsafe { Wtf8Buf::from_bytes_unchecked(filled) }) } #[inline] @@ -1393,7 +1334,7 @@ impl PyStr { &self, width: isize, fillchar: OptionalArg, - pad: fn(&Wtf8, usize, CodePoint, usize) -> Wtf8Buf, + pad: fn(&Wtf8, usize, CodePoint, usize) -> Option, vm: &VirtualMachine, ) -> PyResult { let fillchar = fillchar.map_or(Ok(' '.into()), |ref s| { @@ -1401,11 +1342,11 @@ impl PyStr { vm.new_type_error("The fill character must be exactly one character long") }) })?; - Ok(if self.len() as isize >= width { - self.as_wtf8().to_owned() - } else { - pad(self.as_wtf8(), width as usize, fillchar, self.len()) - }) + if self.len() as isize >= width { + return Ok(self.as_wtf8().to_owned()); + } + pad(self.as_wtf8(), width as usize, fillchar, self.len()) + .ok_or_else(|| vm.new_memory_error("")) } #[pymethod] @@ -1452,17 +1393,15 @@ impl PyStr { let Some(s) = self.to_str() else { return false }; let mut chars = s.chars(); - let is_identifier_start = chars - .next() - .is_some_and(|c| c == '_' || XidStart::for_char(c)); + let is_identifier_start = chars.next().is_some_and(unicode::identifier::is_start); // a string is not an identifier if it has whitespace or starts with a number - is_identifier_start && chars.all(XidContinue::for_char) + is_identifier_start && chars.all(unicode::identifier::is_continue) } // https://docs.python.org/3/library/stdtypes.html#str.translate #[pymethod] - fn translate(&self, table: PyObjectRef, vm: &VirtualMachine) -> PyResult { + pub fn translate(&self, table: PyObjectRef, vm: &VirtualMachine) -> PyResult { vm.get_method_or_type_error(table.clone(), identifier!(vm, __getitem__), || { format!("'{}' object is not subscriptable", table.class().name()) })?; @@ -1593,102 +1532,6 @@ impl PyStr { } } -/// Title case first char if it is cased or write as is. -/// -/// This matches CPython's behavior: -/// "123abc" -> "123abc" -/// "abc" -> "Abc" -fn titlecase_first(s: &str, chars: &mut core::str::CharIndices<'_>, out: &mut VecFmtWriter) { - if let Some((first_pos, first_ch)) = chars.next() { - let first = &s[..first_pos + first_ch.len_utf8()]; - let tm = TitlecaseMapper::new(); - tm.titlecase_segment(first, &LanguageIdentifier::UNKNOWN, Default::default()) - .write_to(out) - .expect("Writing to an in-memory buffer cannot fail."); - } -} - -/// Title case a string following CPython conventions. -/// -/// CPython title cases each char in a segment. A "segment" is split by case ignorable characters -/// rather than whitespace. -/// "123abc" -> "123Abc" -/// "123abc456def" -> "123Abc456Def" -/// "123 abc" -> "123 Abc" -fn titlecase_string(s: &str, out: &mut VecFmtWriter) { - let mut previous_is_cased = false; - let mapper = TitlecaseMapper::new(); - for (i, ch) in s.char_indices() { - if previous_is_cased { - lowercase_or_sigma(ch, s, i, out); - } else { - let s = &s[i..i + ch.len_utf8()]; - mapper - .titlecase_segment(s, &LanguageIdentifier::UNKNOWN, Default::default()) - .write_to(out) - .expect("Writing to an in-memory buffer cannot fail."); - } - - previous_is_cased = Cased::for_char(ch); - } -} - -fn lowercase_or_sigma(ch: char, s: &str, i: usize, out: &mut VecFmtWriter) { - let sigma = 'Σ'; - if ch == sigma { - let sigma_cased = handle_capital_sigma(s, i); - let mut buf = [0u8; 4]; - let s = sigma_cased.encode_utf8(&mut buf); - out.0.extend(s.as_bytes()); - } else { - for ch in ch.to_lowercase() { - let mut buf = [0u8; 4]; - let s = ch.encode_utf8(&mut buf); - out.0.extend(s.as_bytes()); - } - } -} - -// Handle context-sensitive sigma. -// -// CPython handles sigma as a special case. This is more efficient than using icu4x to scan the -// entire string with CaseMapper because CaseMapper would allocate to produce a new string. The -// icu4x crates are robust but CPython's capitalize() is NOT so we can skip the extra allocs. -fn handle_capital_sigma(s: &str, i: usize) -> char { - let (left, rest) = s.split_at(i); - let right = &rest['Σ'.len_utf8()..]; - - // Check if any chars before or after sigma are cased. - let before = left - .chars() - .rev() - .find(|&ch| !CaseIgnorable::for_char(ch)) - .is_some_and(Cased::for_char); - let after = right - .chars() - .find(|&ch| !CaseIgnorable::for_char(ch)) - .is_some_and(Cased::for_char); - if before && !after { 'ς' } else { 'σ' } -} - -fn swapcase_utf8(s: &str, out: &mut VecFmtWriter) { - for (i, ch) in s.char_indices() { - if ch.is_uppercase() { - lowercase_or_sigma(ch, s, i, out); - } else if ch.is_lowercase() { - for ch in ch.to_uppercase() { - let mut buf = [0u8; 4]; - let s = ch.encode_utf8(&mut buf); - out.0.extend(s.as_bytes()); - } - } else { - let mut buf = [0u8; 4]; - let s = ch.encode_utf8(&mut buf); - out.0.extend(s.as_bytes()); - } - } -} - impl PyRef { #[must_use] pub fn is_empty(&self) -> bool { @@ -1762,6 +1605,11 @@ impl Comparable for PyStr { return Ok(res.into()); } let other = class_or_notimplemented!(Self, other); + // Equality does not need the ordering, and answers two strings of + // different length without reading either. + if let Some(res) = op.eval_eq(|| zelf.as_wtf8() == other.as_wtf8()) { + return Ok(res.into()); + } Ok(op.eval_ord(zelf.as_wtf8().cmp(other.as_wtf8())).into()) } } @@ -1915,6 +1763,13 @@ impl ToPyObject for &String { } } +impl ToPyObject for &CStr { + fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { + let s = self.to_str().expect("ToPyObject expects utf-8 CStr"); + vm.ctx.new_str(s).into() + } +} + impl ToPyObject for &Wtf8 { fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.new_str(self).into() @@ -1999,6 +1854,31 @@ pub(crate) fn init(ctx: &'static Context) { PyStrIterator::extend_class(ctx, ctx.types.str_iterator_type); } +impl PyStr { + /// The code points at `indices`, in that order, as a new string. + /// + /// Each index is resolved through the string's own index table, so the + /// cost is one lookup per collected character rather than a walk to the + /// furthest one. The iterator's length is the result's character count, + /// which is why it has to be exact. + fn gather_chars(&self, indices: impl ExactSizeIterator) -> Self { + let char_len = indices.len(); + // Not ascii, so the code points are at least two bytes each. + let mut out = Wtf8Buf::with_capacity(2 * char_len); + let s = self.as_wtf8(); + for index in indices { + out.push( + s[self.data.char_index_to_byte(index)..] + .code_points() + .next() + .expect("index is below the character count"), + ); + } + // SAFETY: char_len is accurate + unsafe { Self::new_with_char_len(out, char_len) } + } +} + impl SliceableSequenceOp for PyStr { type Item = CodePoint; type Sliced = Self; @@ -2008,125 +1888,56 @@ impl SliceableSequenceOp for PyStr { } fn do_slice(&self, range: Range) -> Self::Sliced { - match self.as_str_kind() { - PyKindStr::Ascii(s) => s[range].into(), - PyKindStr::Utf8(s) => { - let char_len = range.len(); - let out = rustpython_common::str::get_chars(s, range); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } - PyKindStr::Wtf8(w) => { - let char_len = range.len(); - let out = rustpython_common::str::get_codepoints(w, range); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } + if let PyKindStr::Ascii(s) = self.as_str_kind() { + return s[range].into(); } + // Both ends resolve through the string's own index, so the slice is a + // byte reslice rather than a walk to `range.start` and another to + // `range.end`. + let char_len = range.len(); + let bytes = self.data.char_range_to_bytes(range); + let out = &self.as_wtf8()[bytes]; + // SAFETY: char_len is accurate + unsafe { Self::new_with_char_len(out.to_owned(), char_len) } } fn do_slice_reverse(&self, range: Range) -> Self::Sliced { - match self.as_str_kind() { - PyKindStr::Ascii(s) => { - let mut out = s[range].to_owned(); - out.as_mut_slice().reverse(); - out.into() - } - PyKindStr::Utf8(s) => { - let char_len = range.len(); - let mut out = String::with_capacity(2 * char_len); - out.extend( - s.chars() - .rev() - .skip(self.char_len() - range.end) - .take(range.len()), - ); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, range.len()) } - } - PyKindStr::Wtf8(w) => { - let char_len = range.len(); - let mut out = Wtf8Buf::with_capacity(2 * char_len); - out.extend( - w.code_points() - .rev() - .skip(self.char_len() - range.end) - .take(range.len()), - ); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } + if let PyKindStr::Ascii(s) = self.as_str_kind() { + let mut out = s[range].to_owned(); + out.as_mut_slice().reverse(); + return out.into(); } + let char_len = range.len(); + let bytes = self.data.char_range_to_bytes(range); + let mut out = Wtf8Buf::with_capacity(bytes.len()); + out.extend(self.as_wtf8()[bytes].code_points().rev()); + // SAFETY: char_len is accurate + unsafe { Self::new_with_char_len(out, char_len) } } fn do_stepped_slice(&self, range: Range, step: usize) -> Self::Sliced { - match self.as_str_kind() { - PyKindStr::Ascii(s) => s[range] + if let PyKindStr::Ascii(s) = self.as_str_kind() { + return s[range] .as_slice() .iter() .copied() .step_by(step) .collect::() - .into(), - PyKindStr::Utf8(s) => { - let char_len = (range.len() / step) + 1; - let mut out = String::with_capacity(2 * char_len); - out.extend(s.chars().skip(range.start).take(range.len()).step_by(step)); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } - PyKindStr::Wtf8(w) => { - let char_len = (range.len() / step) + 1; - let mut out = Wtf8Buf::with_capacity(2 * char_len); - out.extend( - w.code_points() - .skip(range.start) - .take(range.len()) - .step_by(step), - ); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } + .into(); } + self.gather_chars(range.step_by(step)) } fn do_stepped_slice_reverse(&self, range: Range, step: usize) -> Self::Sliced { - match self.as_str_kind() { - PyKindStr::Ascii(s) => s[range] + if let PyKindStr::Ascii(s) = self.as_str_kind() { + return s[range] .chars() .rev() .step_by(step) .collect::() - .into(), - PyKindStr::Utf8(s) => { - let char_len = (range.len() / step) + 1; - // not ascii, so the codepoints have to be at least 2 bytes each - let mut out = String::with_capacity(2 * char_len); - out.extend( - s.chars() - .rev() - .skip(self.char_len() - range.end) - .take(range.len()) - .step_by(step), - ); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } - PyKindStr::Wtf8(w) => { - let char_len = (range.len() / step) + 1; - // not ascii, so the codepoints have to be at least 2 bytes each - let mut out = Wtf8Buf::with_capacity(2 * char_len); - out.extend( - w.code_points() - .rev() - .skip(self.char_len() - range.end) - .take(range.len()) - .step_by(step), - ); - // SAFETY: char_len is accurate - unsafe { Self::new_with_char_len(out, char_len) } - } + .into(); } + self.gather_chars(range.rev().step_by(step)) } fn empty() -> Self::Sliced { @@ -2357,7 +2168,7 @@ impl PyUtf8Str { impl Py { /// Upcast to PyStr. - pub fn as_pystr(&self) -> &Py { + pub const fn as_pystr(&self) -> &Py { unsafe { // Safety: PyUtf8Str is a wrapper around PyStr, so this cast is safe. &*(self as *const Self as *const Py) @@ -2405,6 +2216,12 @@ impl AnyStrContainer for String { Self::with_capacity(capacity) } + fn try_with_capacity(capacity: usize) -> Option { + let mut s = Self::new(); + s.try_reserve_exact(capacity).ok()?; + Some(s) + } + fn push_str(&mut self, other: &str) { Self::push_str(self, other) } @@ -2501,11 +2318,11 @@ impl AnyStr for str { } fn py_islower(&self) -> bool { - self.is_cased::() + self.is_cased(case::is_lowercase, case::is_uppercase) } fn py_isupper(&self) -> bool { - self.is_cased::() + self.is_cased(case::is_uppercase, case::is_lowercase) } } @@ -2518,6 +2335,12 @@ impl AnyStrContainer for Wtf8Buf { Self::with_capacity(capacity) } + fn try_with_capacity(capacity: usize) -> Option { + let mut s = Self::new(); + s.try_reserve_exact(capacity).ok()?; + Some(s) + } + fn push_str(&mut self, other: &Wtf8) { self.push_wtf8(other) } @@ -2621,11 +2444,11 @@ impl AnyStr for Wtf8 { } fn py_islower(&self) -> bool { - self.is_cased::() + self.is_cased(case::is_lowercase, case::is_uppercase) } fn py_isupper(&self) -> bool { - self.is_cased::() + self.is_cased(case::is_uppercase, case::is_lowercase) } } @@ -2638,6 +2461,12 @@ impl AnyStrContainer for AsciiString { Self::with_capacity(capacity) } + fn try_with_capacity(capacity: usize) -> Option { + let mut v = Vec::new(); + v.try_reserve_exact(capacity).ok()?; + Some(Self::from(v)) + } + fn push_str(&mut self, other: &AsciiStr) { Self::push_str(self, other) } diff --git a/crates/vm/src/builtins/super.rs b/crates/vm/src/builtins/super.rs index c44b61d71e9..f71bf51a6e1 100644 --- a/crates/vm/src/builtins/super.rs +++ b/crates/vm/src/builtins/super.rs @@ -79,22 +79,27 @@ impl Initializer for PySuper { let (typ, obj) = if let OptionalArg::Present(ty) = py_type { (ty, py_obj.unwrap_or_none(vm)) } else { - let frame = vm - .current_frame() - .ok_or_else(|| vm.new_runtime_error("super(): no current frame"))?; + // Access the InterpreterFrame directly — no need to materialize + // a FrameObject just to read code/locals. + let iframe_ptr = crate::vm::thread::get_current_frame(); + if iframe_ptr.is_null() { + return Err(vm.new_runtime_error("super(): no current frame")); + } + let iframe = unsafe { &*iframe_ptr }; + let code = iframe.code(); - if frame.code.arg_count == 0 { + if code.arg_count == 0 { return Err(vm.new_runtime_error("super(): no arguments")); } - // SAFETY: Frame is current and not concurrently mutated. + // SAFETY: InterpreterFrame is current and not concurrently mutated. use rustpython_compiler_core::bytecode::CO_FAST_CELL; - let obj = unsafe { frame.fastlocals() }[0] + let fastlocals = iframe.localsplus.fastlocals(); + let obj = fastlocals[0] .clone() .and_then(|val| { // If slot 0 is a merged cell (LOCAL|CELL), extract value from cell - if frame - .code + if code .localspluskinds .first() .is_some_and(|&k| k & CO_FAST_CELL != 0) @@ -108,13 +113,15 @@ impl Initializer for PySuper { let mut typ = None; // Search for __class__ in freevars using localspluskinds - let nlocalsplus = frame.code.localspluskinds.len(); - let nfrees = frame.code.freevars.len(); + let nlocalsplus = code.localspluskinds.len(); + let nfrees = code.freevars.len(); let free_start = nlocalsplus - nfrees; - for (i, var) in frame.code.freevars.iter().enumerate() { + for (i, var) in code.freevars.iter().enumerate() { if var.as_bytes() == b"__class__" { - let class = frame - .get_cell_contents(free_start + i) + let class = fastlocals[free_start + i] + .as_ref() + .and_then(|v| v.downcast_ref::()) + .and_then(|c| c.get()) .ok_or_else(|| vm.new_runtime_error("super(): empty __class__ cell"))?; typ = Some(class.downcast().map_err(|o| { vm.new_type_error(format!( @@ -237,7 +244,7 @@ impl Representable for PySuper { let obj = zelf.inner.read().obj.clone(); let repr = match obj { Some((_, ref ty)) => { - format!(", <{} object>>", &type_name, ty.name()) + format!(", <{} object>>", type_name, ty.name()) } None => format!(", NULL>"), }; diff --git a/crates/vm/src/builtins/template.rs b/crates/vm/src/builtins/template.rs index 30812b4f171..94c4d653df3 100644 --- a/crates/vm/src/builtins/template.rs +++ b/crates/vm/src/builtins/template.rs @@ -186,7 +186,11 @@ impl PyTemplate { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } diff --git a/crates/vm/src/builtins/traceback.rs b/crates/vm/src/builtins/traceback.rs index b31de874b73..0e93768dca9 100644 --- a/crates/vm/src/builtins/traceback.rs +++ b/crates/vm/src/builtins/traceback.rs @@ -1,7 +1,7 @@ use super::{PyList, PyType}; use crate::{ AsObject, Context, Py, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, - frame::FrameRef, function::PySetterValue, types::Constructor, + frame::FrameObjectRef, function::PySetterValue, types::Constructor, }; use rustpython_common::lock::PyMutex; use rustpython_compiler_core::OneIndexed; @@ -10,7 +10,7 @@ use rustpython_compiler_core::OneIndexed; #[derive(Debug)] pub struct PyTraceback { pub next: PyMutex>, - pub frame: FrameRef, + pub frame: FrameObjectRef, #[pytraverse(skip)] pub lasti: u32, #[pytraverse(skip)] @@ -31,7 +31,7 @@ impl PyTraceback { #[must_use] pub const fn new( next: Option>, - frame: FrameRef, + frame: FrameObjectRef, lasti: u32, lineno: OneIndexed, ) -> Self { @@ -44,7 +44,7 @@ impl PyTraceback { } #[pygetset] - fn tb_frame(&self) -> FrameRef { + fn tb_frame(&self) -> FrameObjectRef { self.frame.clone() } @@ -104,7 +104,7 @@ impl PyTraceback { } impl Constructor for PyTraceback { - type Args = (Option>, FrameRef, u32, usize); + type Args = (Option>, FrameObjectRef, u32, usize); fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { let (next, frame, lasti, lineno) = args; @@ -130,9 +130,12 @@ impl serde::Serialize for PyTraceback { use serde::ser::SerializeStruct; let mut struc = s.serialize_struct("PyTraceback", 3)?; - struc.serialize_field("name", self.frame.code.obj_name.as_str())?; + struc.serialize_field("name", self.frame.iframe().code().obj_name.as_str())?; struc.serialize_field("lineno", &self.lineno.get())?; - struc.serialize_field("filename", self.frame.code.source_path().as_str())?; + struc.serialize_field( + "filename", + self.frame.iframe().code().source_path().as_str(), + )?; struc.end() } } diff --git a/crates/vm/src/builtins/tuple.rs b/crates/vm/src/builtins/tuple.rs index 4606509fd19..d510e35326f 100644 --- a/crates/vm/src/builtins/tuple.rs +++ b/crates/vm/src/builtins/tuple.rs @@ -1,14 +1,8 @@ -// cspell:ignore pyhash - use super::{ PositionIterInternal, PyGenericAlias, PyStrRef, PyType, PyTypeRef, iter::builtins_iter, }; use crate::common::lock::LazyLock; -use crate::common::{ - hash::{PyHash, PyUHash}, - lock::PyMutex, - wtf8::wtf8_concat, -}; +use crate::common::{hash, hash::PyHash, lock::PyMutex, wtf8::wtf8_concat}; use crate::object::{Traverse, TraverseFn}; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, @@ -29,74 +23,90 @@ use crate::{ vm::VirtualMachine, }; use alloc::fmt; -use core::cell::Cell; +use core::cell::{Cell, UnsafeCell}; use core::ptr::NonNull; #[pyclass(module = false, name = "tuple", traverse = "manual")] pub struct PyTuple { - elements: Box<[R]>, + elements: TupleElements, } -impl fmt::Debug for PyTuple { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: implement more informational, non-recursive Debug formatter - f.write_str("tuple") +/// Tuple storage is immutable after publication, but marshal must publish a +/// tuple in its reference table before recursively reading its children. +/// This mirrors CPython's `PyTuple_New` followed by `PyTuple_SET_ITEM`. +struct TupleElements(UnsafeCell>); + +unsafe impl Send for TupleElements {} +unsafe impl Sync for TupleElements {} + +impl TupleElements { + const fn new(elements: Box<[R]>) -> Self { + Self(UnsafeCell::new(elements)) } -} -// SAFETY: Traverse properly visits all owned PyObjectRefs -// Note: Only impl for PyTuple (the default) -unsafe impl Traverse for PyTuple { - fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { - self.elements.traverse(traverse_fn); + fn as_slice(&self) -> &[R] { + // SAFETY: initialization writes happen only while the tuple is owned by + // the synchronous marshal decoder; afterwards the storage is immutable. + unsafe { &*self.0.get() } } - fn clear(&mut self, out: &mut Vec) { - let elements = core::mem::take(&mut self.elements); - out.extend(elements.into_vec()); + fn get_mut(&mut self) -> &mut Box<[R]> { + self.0.get_mut() + } + + /// # Safety + /// The tuple must still be in its private initialization phase, and each + /// placeholder index must be replaced at most once before it is observable. + unsafe fn set_initializing(&self, index: usize, value: R) { + unsafe { (*self.0.get())[index] = value }; } } -// spell-checker:ignore MAXSAVESIZE -/// Per-size freelist storage for tuples, matching tuples[PyTuple_MAXSAVESIZE]. -/// Each bucket caches tuples of a specific element count (index = len - 1). -struct TupleFreeList { - buckets: [Vec>; Self::MAX_SAVE_SIZE], +impl core::ops::Deref for TupleElements { + type Target = [R]; + + fn deref(&self) -> &Self::Target { + self.as_slice() + } } -impl TupleFreeList { - /// Largest tuple size to cache on the freelist (sizes 1..=20). - const MAX_SAVE_SIZE: usize = 20; - const fn new() -> Self { - Self { - buckets: [const { Vec::new() }; Self::MAX_SAVE_SIZE], - } +impl<'a, R> IntoIterator for &'a TupleElements { + type Item = &'a R; + type IntoIter = core::slice::Iter<'a, R>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() } } -impl Default for TupleFreeList { - fn default() -> Self { - Self::new() +impl fmt::Debug for PyTuple { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: implement more informational, non-recursive Debug formatter + f.write_str("tuple") } } -impl Drop for TupleFreeList { - fn drop(&mut self) { - // Same safety pattern as FreeList::drop — free raw allocation - // without running payload destructors to avoid TLS-after-destruction panics. - let layout = crate::object::pyinner_layout::(); - for bucket in &mut self.buckets { - for ptr in bucket.drain(..) { - unsafe { - alloc::alloc::dealloc(ptr.as_ptr() as *mut u8, layout); - } - } - } +// SAFETY: Traverse properly visits all owned PyObjectRefs +// Note: Only impl for PyTuple (the default) +unsafe impl Traverse for PyTuple { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + self.elements.as_slice().traverse(traverse_fn); + } + + fn clear(&mut self, out: &mut Vec) { + let elements = core::mem::take(self.elements.get_mut()); + out.extend(elements.into_vec()); } } thread_local! { - static TUPLE_FREELIST: Cell = const { Cell::new(TupleFreeList::new()) }; + // A single freelist for all tuple sizes: `PyInner` is a + // fixed-size allocation (elements are a separate boxed slice that is + // dropped and replaced on reuse), so husks are interchangeable. + // freelist_push must not read the payload — it runs after tp_clear, + // which has already emptied `elements`. + static TUPLE_FREELIST: Cell> = + const { Cell::new(crate::object::FreeList::new()) }; } impl PyPayload for PyTuple { @@ -110,16 +120,11 @@ impl PyPayload for PyTuple { #[inline] unsafe fn freelist_push(obj: *mut PyObject) -> bool { - let len = unsafe { &*(obj as *const crate::Py) }.elements.len(); - if len == 0 || len > TupleFreeList::MAX_SAVE_SIZE { - return false; - } TUPLE_FREELIST .try_with(|fl| { let mut list = fl.take(); - let bucket = &mut list.buckets[len - 1]; - let stored = if bucket.len() < Self::MAX_FREELIST { - bucket.push(unsafe { NonNull::new_unchecked(obj) }); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); true } else { false @@ -131,15 +136,11 @@ impl PyPayload for PyTuple { } #[inline] - unsafe fn freelist_pop(payload: &Self) -> Option> { - let len = payload.elements.len(); - if len == 0 || len > TupleFreeList::MAX_SAVE_SIZE { - return None; - } + unsafe fn freelist_pop(_payload: &Self) -> Option> { TUPLE_FREELIST .try_with(|fl| { let mut list = fl.take(); - let result = list.buckets[len - 1].pop(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); fl.set(list); result }) @@ -253,7 +254,7 @@ impl Constructor for PyTuple { fn py_new(_cls: &Py, elements: Self::Args, _vm: &VirtualMachine) -> PyResult { Ok(Self { - elements: elements.into_boxed_slice(), + elements: TupleElements::new(elements.into_boxed_slice()), }) } } @@ -292,19 +293,19 @@ impl<'a, R> core::iter::IntoIterator for &'a Py> { impl PyTuple { #[must_use] - pub const fn as_slice(&self) -> &[R] { + pub fn as_slice(&self) -> &[R] { &self.elements } #[inline] #[must_use] - pub const fn len(&self) -> usize { + pub fn len(&self) -> usize { self.elements.len() } #[inline] #[must_use] - pub const fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.elements.is_empty() } @@ -321,7 +322,13 @@ impl PyTuple { ctx.empty_tuple.clone() } else { let elements = elements.into_boxed_slice(); - PyRef::new_ref(Self { elements }, ctx.types.tuple_type.to_owned(), None) + PyRef::new_ref( + Self { + elements: TupleElements::new(elements), + }, + ctx.types.tuple_type.to_owned(), + None, + ) } } @@ -330,7 +337,16 @@ impl PyTuple { /// Calling this function implies trying micro optimization for non-zero-sized tuple. #[must_use] pub const fn new_unchecked(elements: Box<[PyObjectRef]>) -> Self { - Self { elements } + Self { + elements: TupleElements::new(elements), + } + } + + /// # Safety + /// This tuple must be a marshal placeholder which has not escaped the + /// decoder, and `index` must not have been replaced previously. + pub(crate) unsafe fn set_marshal_item(&self, index: usize, value: PyObjectRef) { + unsafe { self.elements.set_initializing(index, value) }; } fn repeat(zelf: PyRef, value: isize, vm: &VirtualMachine) -> PyResult> { @@ -345,7 +361,10 @@ impl PyTuple { } else { let v = zelf.elements.mul(vm, value)?; let elements = v.into_boxed_slice(); - Self { elements }.into_ref(&vm.ctx) + Self { + elements: TupleElements::new(elements), + } + .into_ref(&vm.ctx) }) } @@ -388,7 +407,10 @@ impl PyTuple { .chain(other.as_slice()) .cloned() .collect::>(); - Self { elements }.into_ref(&vm.ctx) + Self { + elements: TupleElements::new(elements), + } + .into_ref(&vm.ctx) } }); PyArithmeticValue::from_option(added.ok()) @@ -407,7 +429,7 @@ impl PyTuple { #[inline] #[must_use] - pub const fn __len__(&self) -> usize { + pub fn __len__(&self) -> usize { self.elements.len() } @@ -472,13 +494,17 @@ impl PyTuple { let tup_arg = if zelf.class().is(vm.ctx.types.tuple_type) { zelf } else { - Self::new_ref(zelf.elements.clone().into_vec(), &vm.ctx) + Self::new_ref(zelf.elements.as_slice().to_vec(), &vm.ctx) }; (tup_arg,) } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -583,7 +609,7 @@ impl Representable for PyTuple { let s = if zelf.len() == 1 { wtf8_concat!("(", zelf.elements[0].repr(vm)?.as_wtf8(), ",)") } else { - collection_repr(None, "(", ")", zelf.elements.iter(), vm)? + collection_repr(None, "(", ")", "()", zelf.elements.iter(), vm)? }; vm.ctx.new_str(s) } else { @@ -727,46 +753,5 @@ pub(crate) fn init(context: &'static Context) { } pub(super) fn tuple_hash(elements: &[PyObjectRef], vm: &VirtualMachine) -> PyResult { - const PRIME1: PyUHash = cfg_select! { - target_pointer_width = "64" => 11400714785074694791, - target_pointer_width = "32" => 2654435761, - _ => unreachable!(), - }; - - const PRIME2: PyUHash = cfg_select! { - target_pointer_width = "64" => 14029467366897019727, - target_pointer_width = "32" => 2246822519, - _ => unreachable!(), - }; - - const PRIME5: PyUHash = cfg_select! { - target_pointer_width = "64" => 2870177450012600261, - target_pointer_width = "32" => 374761393, - _ => unreachable!(), - }; - - const ROTATE: u32 = cfg_select! { - target_pointer_width = "64" => 31, - target_pointer_width = "32" => 13, - _ => unreachable!(), - }; - - let mut acc = PRIME5; - let len = elements.len() as PyUHash; - - for val in elements { - let lane = val.hash(vm)? as PyUHash; - acc = acc.wrapping_add(lane.wrapping_mul(PRIME2)); - acc = acc.rotate_left(ROTATE); - acc = acc.wrapping_mul(PRIME1); - } - - acc = acc.wrapping_add(len ^ (PRIME5 ^ 3527539)); - - let acc_pyhash = acc as PyHash; - if acc_pyhash == -1 { - return Ok(1546275796); - } - - Ok(acc_pyhash) + hash::hash_tuple(elements.iter().map(|val| val.hash(vm))) } diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index d26c81a7b76..46e2e4ebfc6 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -18,7 +18,7 @@ use crate::{ common::{ ascii, borrow::BorrowedValue, - lock::{PyMutex, PyRwLock, PyRwLockReadGuard}, + lock::{PyRwLock, PyRwLockReadGuard}, }, function::{FuncArgs, KwArgs, OptionalArg, PyMethodDef, PySetterValue}, object::{Traverse, TraverseFn}, @@ -34,7 +34,7 @@ use core::{ ops::Deref, pin::Pin, ptr::NonNull, - sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, + sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering}, }; use indexmap::{IndexMap, map::Entry}; use itertools::Itertools; @@ -44,7 +44,8 @@ use std::collections::HashSet; #[pyclass(module = false, name = "type", traverse = "manual")] pub struct PyType { - pub base: Option, + /// tp_base. Written under the type lock (see `set_bases`); read lock-free. + pub base: PyAtomicRef>, pub bases: PyRwLock>, pub mro: PyRwLock>, pub subclasses: PyRwLock>>, @@ -53,10 +54,11 @@ pub struct PyType { pub heaptype_ext: Option>>, /// Type version tag for inline caching. 0 means unassigned/invalidated. pub tp_version_tag: AtomicU32, + pub abc_tpflags: AtomicU64, } /// Monotonic counter for type version tags. Once it reaches `u32::MAX`, -/// `assign_version_tag()` returns 0 permanently, disabling new inline-cache +/// version assignment returns 0 permanently, disabling new inline-cache /// entries but not invalidating correctness (cache misses fall back to the /// generic path). static NEXT_TYPE_VERSION: AtomicU32 = AtomicU32::new(1); @@ -216,9 +218,30 @@ pub(crate) fn type_cache_clear() { TYPE_CACHE_CLEARING.store(false, Ordering::Release); } +/// Repair type-cache SeqLock state in the post-fork child. +/// +/// If fork happens while a writer holds an entry SeqLock, the child inherits +/// the odd sequence value with no surviving writer to release it. Clear only +/// those in-progress entries, matching `_PyTypes_AfterFork()`. +#[cfg(all(feature = "host_env", unix))] +pub(crate) unsafe fn type_cache_after_fork() { + for entry in TYPE_CACHE.iter() { + let seq = entry.sequence.load(Ordering::Relaxed); + if (seq & 1) == 0 { + continue; + } + entry.value.store(core::ptr::null_mut(), Ordering::Relaxed); + entry.name.store(core::ptr::null_mut(), Ordering::Relaxed); + entry.version.store(0, Ordering::Relaxed); + entry.sequence.store(0, Ordering::Relaxed); + } +} + unsafe impl crate::object::Traverse for PyType { fn traverse(&self, tracer_fn: &mut crate::object::TraverseFn<'_>) { - self.base.traverse(tracer_fn); + if let Some(base) = self.base.deref() { + tracer_fn(base.as_object()); + } self.bases.traverse(tracer_fn); self.mro.traverse(tracer_fn); self.subclasses.traverse(tracer_fn); @@ -234,7 +257,8 @@ unsafe impl crate::object::Traverse for PyType { /// type_clear: break reference cycles in type objects fn clear(&mut self, out: &mut Vec) { - if let Some(base) = self.base.take() { + // SAFETY: tp_clear runs with exclusive access to the type object. + if let Some(base) = unsafe { self.base.swap(None) } { out.push(base.into()); } if let Some(mut guard) = self.bases.try_write() { @@ -270,69 +294,67 @@ pub struct HeapTypeExt { pub slots: Option>>, pub type_data: PyRwLock>, pub specialization_cache: TypeSpecializationCache, + /// The interpreter this type was created in, or `None` for the types the + /// shared context builds before any interpreter exists. + pub interpreter_id: Option, +} + +impl HeapTypeExt { + /// The interpreter a type created right now belongs to. + fn creating_interpreter_id() -> Option { + crate::vm::thread::try_with_current_vm(|vm| vm.state.interpreter_id) + } } pub struct TypeSpecializationCache { pub init: PyAtomicRef>, + pub init_version: AtomicU32, pub getitem: PyAtomicRef>, pub getitem_version: AtomicU32, - // Serialize cache writes/invalidation similar to CPython's BEGIN_TYPE_LOCK. - write_lock: PyMutex<()>, - retired: PyRwLock>, } impl TypeSpecializationCache { fn new() -> Self { Self { init: PyAtomicRef::from(None::>), + init_version: AtomicU32::new(0), getitem: PyAtomicRef::from(None::>), getitem_version: AtomicU32::new(0), - write_lock: PyMutex::new(()), - retired: PyRwLock::new(Vec::new()), } } #[inline] - fn retire_old_function(&self, old: Option>) { - if let Some(old) = old { - self.retired.write().push(old.into()); - } - } - - #[inline] - fn swap_init(&self, new_init: Option>, vm: Option<&VirtualMachine>) { - if let Some(vm) = vm { - // Keep replaced refs alive for the currently executing frame, matching - // CPython-style "old pointer remains valid during ongoing execution" - // without accumulating global retired refs. - self.init.swap_to_temporary_refs(new_init, vm); - return; + fn swap_init(&self, new_init: Option>) { + if let Some(new) = &new_init { + new.as_object().mark_cache_published(); } - // SAFETY: old value is moved to `retired`, so it stays alive while - // concurrent readers may still hold borrowed references. + // SAFETY: reclamation of published objects is deferred via QSBR; + // racing try_to_owned readers never touch freed memory. let old = unsafe { self.init.swap(new_init) }; - self.retire_old_function(old); + if let Some(old) = old { + // Dropping may run arbitrary Python; defer past the type lock. + rustpython_common::refcount::try_defer_drop(move || drop(old)); + } } #[inline] - fn swap_getitem(&self, new_getitem: Option>, vm: Option<&VirtualMachine>) { - if let Some(vm) = vm { - self.getitem.swap_to_temporary_refs(new_getitem, vm); - return; + fn swap_getitem(&self, new_getitem: Option>) { + if let Some(new) = &new_getitem { + new.as_object().mark_cache_published(); } - // SAFETY: old value is moved to `retired`, so it stays alive while - // concurrent readers may still hold borrowed references. + // SAFETY: as in swap_init. let old = unsafe { self.getitem.swap(new_getitem) }; - self.retire_old_function(old); + if let Some(old) = old { + rustpython_common::refcount::try_defer_drop(move || drop(old)); + } } #[inline] fn invalidate_for_type_modified(&self) { - let _guard = self.write_lock.lock(); - // _spec_cache contract: type modification invalidates all cached - // specialization functions. - self.swap_init(None, None); - self.swap_getitem(None, None); + self.swap_init(None); + self.init_version.store(0, Ordering::Release); + self.swap_getitem(None); + self.getitem_version.store(0, Ordering::Release); } fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { @@ -342,25 +364,19 @@ impl TypeSpecializationCache { if let Some(getitem) = self.getitem.deref() { tracer_fn(getitem.as_object()); } - self.retired - .read() - .iter() - .map(|obj| obj.traverse(tracer_fn)) - .count(); } fn clear_into(&self, out: &mut Vec) { - let _guard = self.write_lock.lock(); let old_init = unsafe { self.init.swap(None) }; if let Some(old_init) = old_init { out.push(old_init.into()); } + self.init_version.store(0, Ordering::Release); let old_getitem = unsafe { self.getitem.swap(None) }; if let Some(old_getitem) = old_getitem { out.push(old_getitem.into()); } self.getitem_version.store(0, Ordering::Release); - out.extend(self.retired.write().drain(..)); } } @@ -370,7 +386,7 @@ unsafe impl Sync for PointerSlot {} unsafe impl Send for PointerSlot {} impl PointerSlot { - pub(crate) const unsafe fn borrow_static(&self) -> &'static T { + pub(crate) const unsafe fn borrow_static(self) -> &'static T { unsafe { self.0.as_ref() } } } @@ -425,7 +441,7 @@ impl core::fmt::Display for PyType { impl core::fmt::Debug for PyType { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "[PyType {}]", &self.name()) + write!(f, "[PyType {}]", self.name()) } } @@ -459,9 +475,19 @@ fn is_subtype_with_mro(a_mro: &[PyTypeRef], a: &Py, b: &Py) -> b } impl PyType { + #[inline] + fn with_type_lock(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { + // Drops deferred via try_defer_drop inside the critical section run + // after the guard is released, outside the lock. + rustpython_common::refcount::with_deferred_drops(|| { + let _guard = vm.state.type_mutex.lock(); + f() + }) + } + /// Assign a fresh version tag. Returns 0 if the version counter has been /// exhausted, in which case no new cache entries can be created. - pub fn assign_version_tag(&self) -> u32 { + fn assign_version_tag_inner(&self) -> u32 { let v = self.tp_version_tag.load(Ordering::Acquire); if v != 0 { return v; @@ -469,7 +495,7 @@ impl PyType { // Assign versions to all direct bases first (MRO invariant). for base in self.bases.read().iter() { - if base.assign_version_tag() == 0 { + if base.assign_version_tag_inner() == 0 { return 0; } } @@ -489,27 +515,68 @@ impl PyType { } } - /// Invalidate this type's version tag and cascade to all subclasses. - pub fn modified(&self) { - if let Some(ext) = self.heaptype_ext.as_ref() { - ext.specialization_cache.invalidate_for_type_modified(); + pub(crate) fn version_for_specialization(&self, vm: &VirtualMachine) -> u32 { + let version = self.tp_version_tag.load(Ordering::Acquire); + if version != 0 { + return version; } - // If already invalidated, all subclasses must also be invalidated - // (guaranteed by the MRO invariant in assign_version_tag). + Self::with_type_lock(vm, || { + let version = self.tp_version_tag.load(Ordering::Acquire); + if version == 0 { + self.assign_version_tag_inner() + } else { + version + } + }) + } + + /// Invalidate this type's version tag and cascade to all subclasses. + fn modified_inner(&self) { let old_version = self.tp_version_tag.load(Ordering::Acquire); if old_version == 0 { return; } - self.tp_version_tag.store(0, Ordering::SeqCst); - // Nullify borrowed pointers in cache entries for this version - // so they don't dangle after the dict is modified. - type_cache_clear_version(old_version); let subclasses = self.subclasses.read(); for weak_ref in subclasses.iter() { if let Some(sub) = weak_ref.upgrade() { - sub.downcast_ref::().unwrap().modified(); + sub.downcast_ref::().unwrap().modified_inner(); } } + self.tp_version_tag.store(0, Ordering::SeqCst); + // Nullify borrowed pointers in cache entries for this version + // so they don't dangle after the dict is modified. + type_cache_clear_version(old_version); + if let Some(ext) = self.heaptype_ext.as_ref() { + ext.specialization_cache.invalidate_for_type_modified(); + } + } + + pub fn modified(&self) { + if self.tp_version_tag.load(Ordering::Acquire) == 0 { + return; + } + if let Some(()) = crate::vm::thread::try_with_current_vm(|vm| { + Self::with_type_lock(vm, || self.modified_inner()); + }) { + return; + } + self.modified_inner(); + } + + /// Whether the interpreter with `interpreter_id` can see this type. + /// + /// Interpreters share the context, so a subclass of a shared type is + /// recorded on an object every interpreter reaches. Only the interpreter + /// that created it can name it, so only that one lists it. + pub fn is_visible_to_interpreter(&self, interpreter_id: i64) -> bool { + match self + .heaptype_ext + .as_ref() + .and_then(|ext| ext.interpreter_id) + { + Some(owner) => owner == interpreter_id, + None => true, + } } pub fn new_simple_heap( @@ -548,6 +615,7 @@ impl PyType { slots: None, type_data: PyRwLock::new(None), specialization_cache: TypeSpecializationCache::new(), + interpreter_id: HeapTypeExt::creating_interpreter_id(), }; let base = bases[0].clone(); @@ -590,7 +658,9 @@ impl PyType { // Check each base in order and inherit the first collection flag found for base in bases { - let base_flags = base.slots.flags & COLLECTION_FLAGS; + let base_flags = (base.slots.flags + | PyTypeFlags::from_bits_truncate(base.abc_tpflags.load(Ordering::Acquire))) + & COLLECTION_FLAGS; if !base_flags.is_empty() { slots.flags |= base_flags; return; @@ -598,6 +668,58 @@ impl PyType { } } + fn inherited_abc_tpflags(bases: &[PyRef]) -> 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 + .try_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::() + { + subclass.set_abc_collection_flags_recursive(flags); + } + } + } + /// Check for __abc_tpflags__ and set the appropriate flags /// This checks in attrs and all base classes for __abc_tpflags__ fn check_abc_tpflags( @@ -626,21 +748,19 @@ impl PyType { .to_owned(), ); } - // Don't override flags already inherited from a base class. - if !slots.flags.intersects(COLLECTION_FLAGS) { - slots.flags |= masked; - } + slots.flags.remove(COLLECTION_FLAGS); + slots.flags |= masked; return Ok(()); } - // No __abc_tpflags__ on this class — inheritance already happened - // in inherit_patma_flags, so nothing more to do if those bits are set. + // No __abc_tpflags__ on this class. Inheritance already happened in + // inherit_patma_flags, using base order and including ABC markers. if slots.flags.intersects(COLLECTION_FLAGS) { return Ok(()); } - // Then check in base classes (legacy path for cases that bypass - // inherit_patma_flags). + // Then check in base classes for legacy paths that bypassed + // inherit_patma_flags. for base in bases { if let Some(abc_tpflags_obj) = base.find_name_in_mro(abc_tpflags_name) && let Some(int_obj) = abc_tpflags_obj.downcast_ref::() @@ -654,6 +774,7 @@ impl PyType { .to_owned(), ); } + slots.flags.remove(COLLECTION_FLAGS); slots.flags |= masked; return Ok(()); } @@ -700,8 +821,6 @@ impl PyType { slots.basicsize = base.slots.basicsize; } - Self::inherit_readonly_slots(&mut slots, &base); - // Normalize: any type with HAS_WEAKREF gets MANAGED_WEAKREF if slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF) { slots.flags |= PyTypeFlags::MANAGED_WEAKREF; @@ -716,9 +835,10 @@ impl PyType { )); } + let inherited_abc_tpflags = Self::inherited_abc_tpflags(&bases); let new_type = PyRef::new_ref( Self { - base: Some(base), + base: Some(base).into(), bases: PyRwLock::new(bases), mro: PyRwLock::new(mro), subclasses: PyRwLock::default(), @@ -726,6 +846,7 @@ impl PyType { slots, heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))), tp_version_tag: AtomicU32::new(0), + abc_tpflags: AtomicU64::new(inherited_abc_tpflags), }, metaclass, None, @@ -768,19 +889,18 @@ impl PyType { slots.basicsize = base.slots.basicsize; } - Self::inherit_readonly_slots(&mut slots, &base); - // Normalize: any type with HAS_WEAKREF gets MANAGED_WEAKREF if slots.flags.has_feature(PyTypeFlags::HAS_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 mro = base.mro_map_collect(|x| x.to_owned()); let new_type = PyRef::new_ref( Self { - base: Some(base), + base: Some(base).into(), bases, mro: PyRwLock::new(mro), subclasses: PyRwLock::default(), @@ -788,6 +908,7 @@ impl PyType { slots, heaptype_ext: None, tp_version_tag: AtomicU32::new(0), + abc_tpflags: AtomicU64::new(inherited_abc_tpflags), }, metaclass, None, @@ -806,8 +927,8 @@ impl PyType { // Note: inherit_slots is called in PyClassImpl::init_class after // slots are fully initialized by make_slots() - Self::set_new(&new_type.slots, new_type.base.as_ref()); - Self::set_alloc(&new_type.slots, new_type.base.as_ref()); + Self::set_new(&new_type.slots, new_type.base.deref()); + Self::set_alloc(&new_type.slots, new_type.base.deref()); let weakref_type = super::PyWeak::static_type(); for base in new_type.bases.read().iter() { @@ -853,11 +974,31 @@ impl PyType { self.update_slot::(attr_name, ctx); } - Self::set_new(&self.slots, self.base.as_ref()); - Self::set_alloc(&self.slots, self.base.as_ref()); + Self::set_new(&self.slots, self.base.deref()); + Self::set_alloc(&self.slots, self.base.deref()); + } + + /// Recompute every slot for this type and all its descendants. update_all_slots + /// + /// Unlike `init_slots`, which is additive and driven only by the dunder names + /// present in the current MRO, this iterates the full `SLOT_DEFS` name table so + /// a slot whose method left the MRO is reset instead of left stale. Must be + /// called under the type lock after MROs have been recomputed. + pub(crate) fn update_all_slots(&self, ctx: &Context) { + // Invalidate version tags first; cascades to subclasses. + self.modified_inner(); + // Distinct names only; update_slot fans out to every SLOT_DEFS entry + // sharing the name and recurses into subclasses on its own. + let mut seen = std::collections::HashSet::new(); + for def in SLOT_DEFS { + if seen.insert(def.name) { + let name = ctx.intern_str(def.name); + self.update_slot::(name, ctx); + } + } } - fn set_new(slots: &PyTypeSlots, base: Option<&PyTypeRef>) { + fn set_new(slots: &PyTypeSlots, base: Option<&Py>) { if slots.flags.contains(PyTypeFlags::DISALLOW_INSTANTIATION) { slots.new.store(None) } else if slots.new.load().is_none() { @@ -865,7 +1006,7 @@ impl PyType { } } - fn set_alloc(slots: &PyTypeSlots, base: Option<&PyTypeRef>) { + fn set_alloc(slots: &PyTypeSlots, base: Option<&Py>) { if slots.alloc.load().is_none() { slots .alloc @@ -873,18 +1014,9 @@ impl PyType { } } - /// Inherit readonly slots from base type at creation time. - /// These slots are not AtomicCell and must be set before the type is used. - fn inherit_readonly_slots(slots: &mut PyTypeSlots, base: &Self) { - if slots.as_buffer.is_none() { - slots.as_buffer = base.slots.as_buffer; - } - } - /// Inherit slots from base type. inherit_slots pub(crate) fn inherit_slots(&self, base: &Self) { // Use SLOT_DEFS to iterate all slots - // Note: as_buffer is handled in inherit_readonly_slots (not AtomicCell) for def in SLOT_DEFS { def.accessor.copyslot_if_none(self, base); } @@ -916,6 +1048,85 @@ impl PyType { self.find_name_in_mro(attr_name) } + /// `_PyType_LookupRefAndVersion` equivalent for interned names. + /// Returns the observed lookup result and the type version used for the lookup. + /// + /// Uses a lock-free SeqLock-style pattern: + /// Read: load sequence/version/name → load value + try_to_owned → + /// validate value pointer + sequence + /// Write: sequence(begin) → version=0 → swap value/name → version=assigned → sequence(end) + pub(crate) fn lookup_ref_and_version_interned( + &self, + name: &'static PyStrInterned, + vm: &VirtualMachine, + ) -> (Option, u32) { + #[cfg(all(feature = "threading", debug_assertions))] + crate::vm::thread::debug_assert_current_thread_attached(); + + let version = self.tp_version_tag.load(Ordering::Acquire); + if version != 0 { + let idx = type_cache_hash(version, name); + let entry = &TYPE_CACHE[idx]; + let name_ptr = name as *const _ as *mut _; + loop { + let seq1 = entry.begin_read(); + let entry_version = entry.version.load(Ordering::Acquire); + let type_version = self.tp_version_tag.load(Ordering::Acquire); + if entry_version != type_version + || !core::ptr::eq(entry.name.load(Ordering::Relaxed), name_ptr) + { + break; + } + let ptr = entry.value.load(Ordering::Acquire); + if ptr.is_null() { + if entry.end_read(seq1) { + return (None, entry_version); + } + continue; + } + if let Some(cloned) = unsafe { PyObject::try_to_owned_from_ptr(ptr) } { + let same_ptr = core::ptr::eq(entry.value.load(Ordering::Relaxed), ptr); + if same_ptr && entry.end_read(seq1) { + return (Some(cloned), entry_version); + } + drop(cloned); + continue; + } + break; + } + } + + Self::with_type_lock(vm, || { + let assigned = if self.tp_version_tag.load(Ordering::Acquire) == 0 { + self.assign_version_tag_inner() + } else { + self.tp_version_tag.load(Ordering::Acquire) + }; + let result = self.find_name_in_mro_uncached(name); + if assigned != 0 + && !TYPE_CACHE_CLEARING.load(Ordering::Acquire) + && self.tp_version_tag.load(Ordering::Acquire) == assigned + { + let idx = type_cache_hash(assigned, name); + let entry = &TYPE_CACHE[idx]; + let name_ptr = name as *const _ as *mut _; + entry.begin_write(); + entry.version.store(0, Ordering::Release); + let new_ptr = result.as_ref().map_or(core::ptr::null_mut(), |found| { + // Defer memory reclamation of cached values via QSBR so + // racing readers never try-incref freed memory. + found.mark_cache_published(); + &**found as *const PyObject as *mut _ + }); + entry.value.store(new_ptr, Ordering::Relaxed); + entry.name.store(name_ptr, Ordering::Relaxed); + entry.version.store(assigned, Ordering::Release); + entry.end_write(); + } + (result, assigned) + }) + } + /// Cache __init__ for CALL_ALLOC_AND_ENTER_INIT specialization. /// The cache is valid only when guarded by the type version check. pub(crate) fn cache_init_for_specialization( @@ -930,22 +1141,27 @@ impl PyType { if tp_version == 0 { return false; } - if self.tp_version_tag.load(Ordering::Acquire) != tp_version { - return false; - } - let _guard = ext.specialization_cache.write_lock.lock(); - if self.tp_version_tag.load(Ordering::Acquire) != tp_version { - return false; - } - ext.specialization_cache.swap_init(Some(init), Some(vm)); - true + Self::with_type_lock(vm, || { + if self.tp_version_tag.load(Ordering::Acquire) != tp_version { + return false; + } + let func_version = init.get_version_for_current_state(); + if func_version == 0 { + return false; + } + ext.specialization_cache.swap_init(Some(init)); + ext.specialization_cache + .init_version + .store(func_version, Ordering::Release); + true + }) } /// Read cached __init__ for CALL_ALLOC_AND_ENTER_INIT specialization. pub(crate) fn get_cached_init_for_specialization( &self, tp_version: u32, - ) -> Option> { + ) -> Option<(PyRef, u32)> { let ext = self.heaptype_ext.as_ref()?; if tp_version == 0 { return None; @@ -953,9 +1169,19 @@ impl PyType { if self.tp_version_tag.load(Ordering::Acquire) != tp_version { return None; } - ext.specialization_cache + // Check order: pointer (Acquire) then function version. + let init = ext + .specialization_cache .init - .to_owned_ordering(Ordering::Acquire) + .try_to_owned(Ordering::Acquire)?; + let cached_version = ext + .specialization_cache + .init_version + .load(Ordering::Acquire); + if cached_version == 0 { + return None; + } + Some((init, cached_version)) } /// Cache __getitem__ for BINARY_OP_SUBSCR_GETITEM specialization. @@ -972,34 +1198,34 @@ impl PyType { if tp_version == 0 { return false; } - let _guard = ext.specialization_cache.write_lock.lock(); - if self.tp_version_tag.load(Ordering::Acquire) != tp_version { - return false; - } - let func_version = getitem.get_version_for_current_state(); - if func_version == 0 { - return false; - } - ext.specialization_cache - .swap_getitem(Some(getitem), Some(vm)); - ext.specialization_cache - .getitem_version - .store(func_version, Ordering::Relaxed); - true + Self::with_type_lock(vm, || { + if self.tp_version_tag.load(Ordering::Acquire) != tp_version { + return false; + } + let func_version = getitem.get_version_for_current_state(); + if func_version == 0 { + return false; + } + ext.specialization_cache.swap_getitem(Some(getitem)); + ext.specialization_cache + .getitem_version + .store(func_version, Ordering::Release); + true + }) } /// Read cached __getitem__ for BINARY_OP_SUBSCR_GETITEM specialization. pub(crate) fn get_cached_getitem_for_specialization(&self) -> Option<(PyRef, u32)> { let ext = self.heaptype_ext.as_ref()?; - // Match CPython check order: pointer (Acquire) then function version. + // Check order: pointer (Acquire) then function version. let getitem = ext .specialization_cache .getitem - .to_owned_ordering(Ordering::Acquire)?; + .try_to_owned(Ordering::Acquire)?; let cached_version = ext .specialization_cache .getitem_version - .load(Ordering::Relaxed); + .load(Ordering::Acquire); if cached_version == 0 { return None; } @@ -1012,82 +1238,14 @@ impl PyType { /// find_name_in_mro with method cache (MCACHE). /// Looks in tp_dict of types in MRO, bypasses descriptors. - /// - /// Uses a lock-free SeqLock-style pattern: - /// Read: load sequence/version/name → load value + try_to_owned → - /// validate value pointer + sequence - /// Write: sequence(begin) → version=0 → swap value/name → version=assigned → sequence(end) fn find_name_in_mro(&self, name: &'static PyStrInterned) -> Option { - let version = self.tp_version_tag.load(Ordering::Acquire); - if version != 0 { - let idx = type_cache_hash(version, name); - let entry = &TYPE_CACHE[idx]; - let name_ptr = name as *const _ as *mut _; - loop { - let seq1 = entry.begin_read(); - let v1 = entry.version.load(Ordering::Acquire); - let type_version = self.tp_version_tag.load(Ordering::Acquire); - if v1 != type_version - || !core::ptr::eq(entry.name.load(Ordering::Relaxed), name_ptr) - { - break; - } - let ptr = entry.value.load(Ordering::Acquire); - if ptr.is_null() { - if entry.end_read(seq1) { - break; - } - continue; - } - // _Py_TryIncrefCompare-style validation: - // safe_inc via raw pointer, then ensure source is unchanged. - if let Some(cloned) = unsafe { PyObject::try_to_owned_from_ptr(ptr) } { - let same_ptr = core::ptr::eq(entry.value.load(Ordering::Relaxed), ptr); - if same_ptr && entry.end_read(seq1) { - return Some(cloned); - } - drop(cloned); - continue; - } - break; - } - } - - // Assign version BEFORE the MRO walk so that any concurrent - // modified() call during the walk invalidates this version. - let assigned = if version == 0 { - self.assign_version_tag() - } else { - version - }; - - // MRO walk - let result = self.find_name_in_mro_uncached(name); - - // Only cache positive results. Negative results are not cached to - // avoid stale entries from transient MRO walk failures during - // concurrent type modifications. - if let Some(ref found) = result - && assigned != 0 - && !TYPE_CACHE_CLEARING.load(Ordering::Acquire) - && self.tp_version_tag.load(Ordering::Acquire) == assigned - { - let idx = type_cache_hash(assigned, name); - let entry = &TYPE_CACHE[idx]; - let name_ptr = name as *const _ as *mut _; - entry.begin_write(); - // Invalidate first to prevent readers from seeing partial state - entry.version.store(0, Ordering::Release); - // Store borrowed pointer (no refcount increment). - let new_ptr = &**found as *const PyObject as *mut PyObject; - entry.value.store(new_ptr, Ordering::Relaxed); - entry.name.store(name_ptr, Ordering::Relaxed); - // Activate entry — Release ensures value/name writes are visible - entry.version.store(assigned, Ordering::Release); - entry.end_write(); - } - - result + crate::vm::thread::try_with_current_vm(|vm| { + self.lookup_ref_and_version_interned(name, vm).0 + }) + // No current VM: this thread is not registered for QSBR, so the + // lock-free cache read protocol is not sound here. Walk the MRO + // under the attributes locks instead (the dicts hold strong refs). + .unwrap_or_else(|| self.find_name_in_mro_uncached(name)) } /// Raw MRO walk without cache. @@ -1103,7 +1261,7 @@ impl PyType { /// _PyType_LookupRef: look up a name through the MRO without setting an exception. pub fn lookup_ref(&self, name: &Py, vm: &VirtualMachine) -> Option { let interned_name = vm.ctx.interned_str(name)?; - self.find_name_in_mro(interned_name) + self.lookup_ref_and_version_interned(interned_name, vm).0 } pub fn get_super_attr(&self, attr_name: &'static PyStrInterned) -> Option { @@ -1120,6 +1278,9 @@ impl PyType { /// Check if attribute exists in MRO, using method cache for fast check. /// Unlike find_name_in_mro, avoids cloning the value on cache hit. fn has_name_in_mro(&self, name: &'static PyStrInterned) -> bool { + #[cfg(all(feature = "threading", debug_assertions))] + crate::vm::thread::debug_assert_current_thread_attached(); + let version = self.tp_version_tag.load(Ordering::Acquire); if version != 0 { let idx = type_cache_hash(version, name); @@ -1292,7 +1453,7 @@ impl Py { } pub fn iter_base_chain(&self) -> impl Iterator { - core::iter::successors(Some(self), |cls| cls.base.as_deref()) + core::iter::successors(Some(self), |cls| cls.base.deref()) } pub fn extend_methods(&'static self, method_defs: &'static [PyMethodDef], ctx: &Context) { @@ -1339,58 +1500,149 @@ impl PyType { } if bases.is_empty() { return Err(vm.new_type_error(format!( - "can only assign non-empty tuple to %s.__bases__, not {}", + "can only assign non-empty tuple to {}.__bases__, not ()", zelf.name() ))); } // TODO: check for mro cycles - // TODO: Remove this class from all subclass lists - // for base in self.bases.read().iter() { - // let subclasses = base.subclasses.write(); - // // TODO: how to uniquely identify the subclasses to remove? - // } + // Compute the new solid base before committing anything. This also + // validates the new bases (BASETYPE flag, no instance layout + // conflict), the same checks type creation performs. + let new_base = best_base(&bases, vm)?.to_owned(); + + // Reject reparenting onto a base whose instances have an incompatible + // object layout. + let old_base = zelf.base.deref().unwrap_or(vm.ctx.types.object_type); + compatible_for_assignment(old_base, &new_base, "__bases__", vm)?; + + // References released inside the critical section are collected here + // and dropped after the lock: dropping them inside can run arbitrary + // code that re-acquires the non-reentrant type mutex. + let mut retired: Vec = Vec::new(); + + // A base swapped out of `zelf.base` may still be observed by + // concurrent lock-free readers; keep it alive in the frame's + // temporary refs so they never see a dangling pointer. + let keep_alive = |type_ref: PyTypeRef, retired: &mut Vec| { + if let Some(frame) = vm.current_frame() { + frame + .iframe() + .cold() + .temporary_refs + .lock() + .push(type_ref.into()); + } else { + retired.push(type_ref.into()); + } + }; - *zelf.bases.write() = bases; - // Recursively update the mros of this class and all subclasses - fn update_mro_recursively(cls: &PyType, vm: &VirtualMachine) -> PyResult<()> { - let mut mro = - PyType::resolve_mro(&cls.bases.read()).map_err(|msg| vm.new_type_error(msg))?; - // Preserve self (mro[0]) when updating MRO - mro.insert(0, cls.mro.read()[0].to_owned()); - *cls.mro.write() = mro; - for subclass in cls.subclasses.write().iter() { - let subclass = subclass.upgrade().unwrap(); - let subclass: &Py = subclass.downcast_ref().unwrap(); - update_mro_recursively(subclass, vm)?; + // Register this type as a subclass of the given bases + let register_subclasses = |bases: &[PyTypeRef]| { + let weakref_type = super::PyWeak::static_type(); + for base in bases { + base.subclasses.write().push( + zelf.as_object() + .downgrade_with_weakref_typ_opt(None, weakref_type.to_owned()) + .unwrap(), + ); } - Ok(()) - } - update_mro_recursively(zelf, vm)?; + }; - // Invalidate inline caches - zelf.modified(); + let result = Self::with_type_lock(vm, || { + // Remove this class from the old bases' subclass lists, pruning + // dead entries along the way. Upgraded refs are retired so the + // last strong reference is never dropped under the lock. + for base in zelf.bases.read().iter() { + let mut subclasses = base.subclasses.write(); + let mut kept = Vec::with_capacity(subclasses.len()); + for weak in subclasses.drain(..) { + match weak.upgrade() { + Some(obj) if obj.is(zelf.as_object()) => { + retired.push(obj); + retired.push(weak.into()); + } + Some(obj) => { + retired.push(obj); + kept.push(weak); + } + None => retired.push(weak.into()), + } + } + *subclasses = kept; + } - // TODO: do any old slots need to be cleaned up first? - zelf.init_slots(&vm.ctx); + let old_bases = core::mem::replace(&mut *zelf.bases.write(), bases); + let old_base = unsafe { zelf.base.swap(Some(new_base)) }; + + // Recursively update the mros of this class and all subclasses, + // recording the previous mros so a failure can be rolled back. + fn update_mro_recursively( + cls: &Py, + undo: &mut Vec<(PyTypeRef, Vec)>, + vm: &VirtualMachine, + ) -> PyResult<()> { + let mut mro = + PyType::resolve_mro(&cls.bases.read()).map_err(|msg| vm.new_type_error(msg))?; + // Preserve self (mro[0]) when updating MRO + mro.insert(0, cls.mro.read()[0].to_owned()); + let old_mro = core::mem::replace(&mut *cls.mro.write(), mro); + undo.push((cls.to_owned(), old_mro)); + for subclass in cls.subclasses.read().iter() { + // Dead entries are pruned elsewhere; skip them here. + let Some(subclass) = subclass.upgrade() else { + continue; + }; + let subclass: &Py = subclass.downcast_ref().unwrap(); + update_mro_recursively(subclass, undo, vm)?; + } + Ok(()) + } + let mut undo = Vec::new(); + if let Err(err) = update_mro_recursively(zelf, &mut undo, vm) { + // Roll back to the previous state. A class reachable through + // multiple bases is recorded once per visit, so restore in + // reverse to end with the first-recorded (original) mro. + for (cls, old_mro) in undo.into_iter().rev() { + let failed_mro = core::mem::replace(&mut *cls.mro.write(), old_mro); + retired.extend(failed_mro.into_iter().map(Into::into)); + retired.push(cls.into()); + } + let failed_bases = core::mem::replace(&mut *zelf.bases.write(), old_bases); + if let Some(failed_base) = unsafe { zelf.base.swap(old_base) } { + keep_alive(failed_base, &mut retired); + } + register_subclasses(&zelf.bases.read()); + retired.extend(failed_bases.into_iter().map(Into::into)); + zelf.modified_inner(); + return Err(err); + } + // Retire the replaced mros as well; dropping them here would + // release them while the lock is held. + for (cls, old_mro) in undo { + retired.extend(old_mro.into_iter().map(Into::into)); + retired.push(cls.into()); + } + retired.extend(old_bases.into_iter().map(Into::into)); + if let Some(old_base) = old_base { + keep_alive(old_base, &mut retired); + } - // Register this type as a subclass of its new bases - let weakref_type = super::PyWeak::static_type(); - for base in zelf.bases.read().iter() { - base.subclasses.write().push( - zelf.as_object() - .downgrade_with_weakref_typ_opt(None, weakref_type.to_owned()) - .unwrap(), - ); - } + // Invalidate inline caches and rebuild every slot for this type and + // all descendants so slots whose methods left the MRO are reset. + zelf.update_all_slots(&vm.ctx); - Ok(()) + register_subclasses(&zelf.bases.read()); + Ok(()) + }); + drop(retired); + result } #[pygetset] fn __base__(&self) -> Option { - self.base.clone() + self.base.to_owned() } #[pygetset] @@ -1475,20 +1727,31 @@ impl PyType { ))); } - let mut attrs = self.attributes.write(); - // First try __annotate__, in case that's been set explicitly - if let Some(annotate) = attrs.get(identifier!(vm, __annotate__)).cloned() { + let annotate_key = identifier!(vm, __annotate__); + let annotate_func_key = identifier!(vm, __annotate_func__); + let attrs = self.attributes.read(); + if let Some(annotate) = attrs.get(annotate_key).cloned() { return Ok(annotate); } - // Then try __annotate_func__ - if let Some(annotate) = attrs.get(identifier!(vm, __annotate_func__)).cloned() { - // TODO: Apply descriptor tp_descr_get if needed + if let Some(annotate) = attrs.get(annotate_func_key).cloned() { return Ok(annotate); } - // Set __annotate_func__ = None and return None + drop(attrs); + let none = vm.ctx.none(); - attrs.insert(identifier!(vm, __annotate_func__), none.clone()); - Ok(none) + let (result, _prev) = Self::with_type_lock(vm, || { + let mut attrs = self.attributes.write(); + if let Some(annotate) = attrs.get(annotate_key).cloned() { + return (annotate, None); + } + if let Some(annotate) = attrs.get(annotate_func_key).cloned() { + return (annotate, None); + } + self.modified_inner(); + let prev = attrs.insert(annotate_func_key, none.clone()); + (none, prev) + }); + Ok(result) } #[pygetset(setter)] @@ -1511,20 +1774,28 @@ impl PyType { return Err(vm.new_type_error("__annotate__ must be callable or None")); } - let mut attrs = self.attributes.write(); - // Clear cached annotations only when setting to a new callable - if !vm.is_none(&value) { - attrs.swap_remove(identifier!(vm, __annotations_cache__)); - } - attrs.insert(identifier!(vm, __annotate_func__), value); + let _prev_values = Self::with_type_lock(vm, || { + self.modified_inner(); + let mut attrs = self.attributes.write(); + // Clear cached annotations only when setting to a new callable + let removed = if !vm.is_none(&value) { + attrs.swap_remove(identifier!(vm, __annotations_cache__)) + } else { + None + }; + let prev = attrs.insert(identifier!(vm, __annotate_func__), value); + (removed, prev) + }); Ok(()) } #[pygetset] fn __annotations__(&self, vm: &VirtualMachine) -> PyResult { + let annotations_key = identifier!(vm, __annotations__); + let annotations_cache_key = identifier!(vm, __annotations_cache__); let attrs = self.attributes.read(); - if let Some(annotations) = attrs.get(identifier!(vm, __annotations__)).cloned() { + if let Some(annotations) = attrs.get(annotations_key).cloned() { // Ignore the __annotations__ descriptor stored on type itself. if !annotations.class().is(vm.ctx.types.getset_type) { if vm.is_none(&annotations) @@ -1539,8 +1810,7 @@ impl PyType { ))); } } - // Then try __annotations_cache__ - if let Some(annotations) = attrs.get(identifier!(vm, __annotations_cache__)).cloned() { + if let Some(annotations) = attrs.get(annotations_cache_key).cloned() { if vm.is_none(&annotations) || annotations.class().is(vm.ctx.types.dict_type) || self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) @@ -1577,11 +1847,21 @@ impl PyType { vm.ctx.new_dict().into() }; - // Cache the result in __annotations_cache__ - self.attributes - .write() - .insert(identifier!(vm, __annotations_cache__), annotations.clone()); - Ok(annotations) + let (result, _prev) = Self::with_type_lock(vm, || { + let mut attrs = self.attributes.write(); + if let Some(existing) = attrs.get(annotations_key).cloned() + && !existing.class().is(vm.ctx.types.getset_type) + { + return (existing, None); + } + if let Some(existing) = attrs.get(annotations_cache_key).cloned() { + return (existing, None); + } + self.modified_inner(); + let prev = attrs.insert(annotations_cache_key, annotations.clone()); + (annotations, prev) + }); + Ok(result) } #[pygetset(setter)] @@ -1597,43 +1877,43 @@ impl PyType { ))); } - let mut attrs = self.attributes.write(); - let has_annotations = attrs.contains_key(identifier!(vm, __annotations__)); - - match value { - crate::function::PySetterValue::Assign(value) => { - // SET path: store the value (including None) - let key = if has_annotations { - identifier!(vm, __annotations__) - } else { - identifier!(vm, __annotations_cache__) - }; - attrs.insert(key, value); - if has_annotations { - attrs.swap_remove(identifier!(vm, __annotations_cache__)); - } - } - crate::function::PySetterValue::Delete => { - // DELETE path: remove the key - let removed = if has_annotations { - attrs - .swap_remove(identifier!(vm, __annotations__)) - .is_some() - } else { - attrs - .swap_remove(identifier!(vm, __annotations_cache__)) - .is_some() - }; - if !removed { - return Err(vm.new_attribute_error("__annotations__")); + let _prev_values = Self::with_type_lock(vm, || { + self.modified_inner(); + let mut attrs = self.attributes.write(); + let has_annotations = attrs.contains_key(identifier!(vm, __annotations__)); + + let mut prev = Vec::new(); + match value { + crate::function::PySetterValue::Assign(value) => { + let key = if has_annotations { + identifier!(vm, __annotations__) + } else { + identifier!(vm, __annotations_cache__) + }; + prev.extend(attrs.insert(key, value)); + if has_annotations { + prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__))); + } } - if has_annotations { - attrs.swap_remove(identifier!(vm, __annotations_cache__)); + crate::function::PySetterValue::Delete => { + let removed = if has_annotations { + attrs.swap_remove(identifier!(vm, __annotations__)) + } else { + attrs.swap_remove(identifier!(vm, __annotations_cache__)) + }; + if removed.is_none() { + return Err(vm.new_attribute_error("__annotations__")); + } + prev.extend(removed); + if has_annotations { + prev.extend(attrs.swap_remove(identifier!(vm, __annotations_cache__))); + } } } - } - attrs.swap_remove(identifier!(vm, __annotate_func__)); - attrs.swap_remove(identifier!(vm, __annotate__)); + prev.extend(attrs.swap_remove(identifier!(vm, __annotate_func__))); + prev.extend(attrs.swap_remove(identifier!(vm, __annotate__))); + Ok(prev) + })?; Ok(()) } @@ -1645,13 +1925,7 @@ impl PyType { .get(identifier!(vm, __module__)) .cloned() // We need to exclude this method from going into recursion: - .and_then(|found| { - if found.fast_isinstance(vm.ctx.types.getset_type) { - None - } else { - Some(found) - } - }) + .filter(|found| !found.fast_isinstance(vm.ctx.types.getset_type)) .unwrap_or_else(|| { // For non-heap types, extract module from tp_name (e.g. "typing.TypeAliasType" -> "typing") let slot_name = self.slot_name(); @@ -1666,9 +1940,13 @@ impl PyType { #[pygetset(setter)] fn set___module__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { self.check_set_special_type_attr(identifier!(vm, __module__), vm)?; - let mut attributes = self.attributes.write(); - attributes.swap_remove(identifier!(vm, __firstlineno__)); - attributes.insert(identifier!(vm, __module__), value); + let _prev_values = Self::with_type_lock(vm, || { + self.modified_inner(); + let mut attributes = self.attributes.write(); + let removed = attributes.swap_remove(identifier!(vm, __firstlineno__)); + let prev = attributes.insert(identifier!(vm, __module__), value); + (removed, prev) + }); Ok(()) } @@ -1684,13 +1962,18 @@ impl PyType { } #[pymethod] - fn __subclasses__(&self) -> PyList { + fn __subclasses__(&self, vm: &VirtualMachine) -> PyList { let mut subclasses = self.subclasses.write(); subclasses.retain(|x| x.upgrade().is_some()); + let interpreter_id = vm.state.interpreter_id; PyList::from( subclasses .iter() - .map(|x| x.upgrade().unwrap()) + .filter_map(|x| x.upgrade()) + .filter(|obj| { + obj.downcast_ref::() + .is_none_or(|typ| typ.is_visible_to_interpreter(interpreter_id)) + }) .collect::>(), ) } @@ -1790,24 +2073,26 @@ impl PyType { value: PySetterValue, vm: &VirtualMachine, ) -> PyResult<()> { + let key = identifier!(vm, __type_params__); match value { - PySetterValue::Assign(ref val) => { - let key = identifier!(vm, __type_params__); + PySetterValue::Assign(val) => { self.check_set_special_type_attr(key, vm)?; - self.modified(); - self.attributes.write().insert(key, val.clone().into()); + let _prev_value = Self::with_type_lock(vm, || { + self.modified_inner(); + self.attributes.write().insert(key, val.into()) + }); } PySetterValue::Delete => { - // For delete, we still need to check if the type is immutable if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) { return Err(vm.new_type_error(format!( "cannot delete '__type_params__' attribute of immutable type '{}'", self.slot_name() ))); } - let key = identifier!(vm, __type_params__); - self.modified(); - self.attributes.write().shift_remove(&key); + let _prev_value = Self::with_type_lock(vm, || { + self.modified_inner(); + self.attributes.write().shift_remove(&key) + }); } } Ok(()) @@ -1922,14 +2207,11 @@ impl Constructor for PyType { *f = PyStaticMethod::from(f.clone()).into_pyobject(vm); } - if let Some(current_frame) = vm.current_frame() { + if let Some(globals) = crate::frame::current_globals() { let entry = attributes.entry(identifier!(vm, __module__)); if matches!(entry, Entry::Vacant(_)) { - let module_name = vm.unwrap_or_none( - current_frame - .globals - .get_item_opt(identifier!(vm, __name__), vm)?, - ); + let module_name = + vm.unwrap_or_none(globals.get_item_opt(identifier!(vm, __name__), vm)?); entry.or_insert(module_name); } } @@ -2105,6 +2387,7 @@ impl Constructor for PyType { slots: heaptype_slots.clone(), type_data: PyRwLock::new(None), specialization_cache: TypeSpecializationCache::new(), + interpreter_id: HeapTypeExt::creating_interpreter_id(), }; (slots, heaptype_ext) }; @@ -2429,10 +2712,12 @@ impl Py { // Check if we can set this special type attribute self.check_set_special_type_attr(identifier!(vm, __doc__), vm)?; - // Set the __doc__ in the type's dict - self.attributes - .write() - .insert(identifier!(vm, __doc__), value); + let _prev_value = PyType::with_type_lock(vm, || { + self.modified_inner(); + self.attributes + .write() + .insert(identifier!(vm, __doc__), value) + }); Ok(()) } @@ -2494,31 +2779,40 @@ impl SetAttr for PyType { } let assign = value.is_assign(); - // Invalidate inline caches before modifying attributes. - // This ensures other threads see the version invalidation before - // any attribute changes, preventing use-after-free of cached descriptors. - zelf.modified(); - - if let PySetterValue::Assign(value) = value { - zelf.attributes.write().insert(attr_name, value); - } else { - let prev_value = zelf.attributes.write().shift_remove(attr_name); // TODO: swap_remove applicable? - if prev_value.is_none() { - return Err(vm.new_attribute_error(format!( - "type object '{}' has no attribute '{}'", - zelf.name(), - attr_name, - ))); - } - } - - if attr_name.as_wtf8().starts_with("__") && attr_name.as_wtf8().ends_with("__") { - if assign { - zelf.update_slot::(attr_name, &vm.ctx); + // Drop old value OUTSIDE the type lock to avoid deadlock: + // dropping may trigger weakref callbacks → method calls → + // LOAD_ATTR specialization → version_for_specialization → type lock. + let _prev_value = Self::with_type_lock(vm, || { + // Invalidate inline caches before modifying attributes. + // This ensures other threads see the version invalidation before + // any attribute changes, preventing use-after-free of cached descriptors. + zelf.modified_inner(); + + let prev_value = if let PySetterValue::Assign(value) = value { + zelf.attributes.write().insert(attr_name, value) } else { - zelf.update_slot::(attr_name, &vm.ctx); + let prev_value = zelf.attributes.write().shift_remove(attr_name); // TODO: swap_remove applicable? + if prev_value.is_none() { + return Err(vm.new_attribute_error(format!( + "type object '{}' has no attribute '{}'", + zelf.name(), + attr_name, + ))); + } + prev_value + }; + + // Keep the slot-table rewrite inside the same transaction as the + // dict mutation and version invalidation. + if attr_name.as_wtf8().starts_with("__") && attr_name.as_wtf8().ends_with("__") { + if assign { + zelf.update_slot::(attr_name, &vm.ctx); + } else { + zelf.update_slot::(attr_name, &vm.ctx); + } } - } + Ok(prev_value) + })?; Ok(()) } } @@ -2538,18 +2832,44 @@ impl Callable for PyType { } } - let obj = if let Some(slot_new) = zelf.slots.new.load() { - slot_new(zelf.to_owned(), args.clone(), vm)? - } else { + let Some(slot_new) = zelf.slots.new.load() else { return Err(vm.new_type_error(format!("cannot create '{}' instances", zelf.slots.name))); }; + // Both the new and init slots consume args, so the init call gets a + // separate copy prepared before slot_new runs. + let init_args = if args.is_empty() { + // Even cloning empty args costs a kwargs map clone; a default + // FuncArgs is indistinguishable from such a clone. + FuncArgs::default() + } else { + // Skip the clone when no init call can follow: the class has no + // init slot, is not `type` itself, and its new slot is a native + // function. new_wrapper is excluded because a Python `__new__` + // can install an `__init__` on the class or return an instance + // of another class while it runs. + // The address comparison is against the single new_wrapper fn item, + // so a mismatch is conservative: if it ever compared unequal for the + // wrapper it would only take the slower cloning path, never the fast + // path incorrectly. + if zelf.slots.init.load().is_none() + && !zelf.is(vm.ctx.types.type_type) + && crate::types::fn_addr(slot_new) + != crate::types::fn_addr(crate::types::new_wrapper as crate::types::NewFunc) + { + return slot_new(zelf.to_owned(), args, vm); + } + args.clone() + }; + + let obj = slot_new(zelf.to_owned(), args, vm)?; + if !obj.class().fast_issubclass(zelf) { return Ok(obj); } if let Some(init_method) = obj.class().slots.init.load() { - init_method(obj.clone(), args, vm)?; + init_method(obj.clone(), init_args, vm)?; } Ok(obj) } @@ -2762,8 +3082,8 @@ pub(crate) fn call_slot_new( // that's not a heap type is this type. let mut staticbase = subtype.clone(); while staticbase.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) { - if let Some(base) = staticbase.base.as_ref() { - staticbase = base.clone(); + if let Some(base) = staticbase.base.to_owned() { + staticbase = base; } else { break; } @@ -2772,7 +3092,8 @@ pub(crate) fn call_slot_new( // Check if staticbase's tp_new differs from typ's tp_new let typ_new = typ.slots.new.load(); let staticbase_new = staticbase.slots.new.load(); - if typ_new.map(|f| f as usize) != staticbase_new.map(|f| f as usize) { + if typ_new.map(|f| crate::types::fn_addr(f)) != staticbase_new.map(|f| crate::types::fn_addr(f)) + { return Err(vm.new_type_error(format!( "{}.__new__({}) is not safe, use {}.__new__()", typ.slot_name(), @@ -2895,7 +3216,7 @@ fn shape_differs(t1: &Py, t2: &Py) -> bool { } fn solid_base<'a>(typ: &'a Py, vm: &VirtualMachine) -> &'a Py { - let base = if let Some(base) = &typ.base { + let base = if let Some(base) = typ.base.deref() { solid_base(base, vm) } else { vm.ctx.types.object_type @@ -2939,6 +3260,90 @@ fn best_base<'a>(bases: &'a [PyTypeRef], vm: &VirtualMachine) -> PyResult<&'a Py Ok(base.unwrap()) } +fn type_has_dict(typ: &Py) -> bool { + typ.slots.flags.has_feature(PyTypeFlags::HAS_DICT) +} + +fn type_has_weakref(typ: &Py) -> bool { + typ.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF) +} + +/// Returns true if `child` adds no instance layout of its own beyond its base, +/// so the base can stand in for it when comparing object layouts. +fn compatible_with_base(child: &Py) -> bool { + let Some(parent) = child.base.deref() else { + return false; + }; + child.slots.basicsize == parent.slots.basicsize + && child.slots.itemsize == parent.slots.itemsize + && child.slots.member_count == parent.slots.member_count + && type_has_dict(child) == type_has_dict(parent) + && type_has_weakref(child) == type_has_weakref(parent) +} + +/// Walk up to the most derived base that actually fixes the instance layout. +fn layout_solid_base(mut typ: &Py) -> &Py { + while compatible_with_base(typ) { + typ = typ.base.deref().unwrap(); + } + typ +} + +/// Returns true if `a` and `b`, which share the same base, added the same +/// instance layout (`__dict__`, `__weakref__`, and `__slots__`). +fn same_slots_added(a: &Py, b: &Py) -> bool { + if a.slots.basicsize != b.slots.basicsize + || a.slots.itemsize != b.slots.itemsize + || a.slots.member_count != b.slots.member_count + || type_has_dict(a) != type_has_dict(b) + || type_has_weakref(a) != type_has_weakref(b) + { + return false; + } + match ( + a.heaptype_ext.as_ref().and_then(|e| e.slots.as_ref()), + b.heaptype_ext.as_ref().and_then(|e| e.slots.as_ref()), + ) { + (Some(x), Some(y)) => { + x.len() == y.len() + && x.iter() + .zip(y.iter()) + .all(|(p, q)| p.as_wtf8() == q.as_wtf8()) + } + (None, None) => true, + _ => false, + } +} + +/// Validates that instances of `old_to` and `new_to` share an interchangeable +/// object layout, the check `__class__` and `__bases__` assignment perform. +/// +/// `attr` names the attribute being assigned for the error message; the +/// message reports `new_to` first and `old_to` second. +pub(crate) fn compatible_for_assignment( + old_to: &Py, + new_to: &Py, + attr: &str, + vm: &VirtualMachine, +) -> PyResult<()> { + let newbase = layout_solid_base(new_to); + let oldbase = layout_solid_base(old_to); + let bases_equal = match (newbase.base.deref(), oldbase.base.deref()) { + (Some(x), Some(y)) => x.is(y), + (None, None) => true, + _ => false, + }; + let compatible = newbase.is(oldbase) || (bases_equal && same_slots_added(newbase, oldbase)); + if compatible { + return Ok(()); + } + Err(vm.new_type_error(format!( + "{attr} assignment: '{}' object layout differs from '{}'", + new_to.name(), + old_to.name() + ))) +} + /// Apply Python name mangling for private attributes. /// `__x` becomes `_ClassName__x` if inside a class. fn mangle_name(class_name: &str, name: &str) -> String { diff --git a/crates/vm/src/builtins/union.rs b/crates/vm/src/builtins/union.rs index cb6dd0d6559..c1be5c8ec9a 100644 --- a/crates/vm/src/builtins/union.rs +++ b/crates/vm/src/builtins/union.rs @@ -234,7 +234,7 @@ pub(crate) fn or_op(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) } fn make_parameters(args: &Py, vm: &VirtualMachine) -> PyResult { - let parameters = genericalias::make_parameters(args, vm); + let parameters = genericalias::make_parameters(args, vm)?; let result = dedup_and_flatten_args(¶meters, vm)?; Ok(result.args) } diff --git a/crates/vm/src/builtins/weakproxy.rs b/crates/vm/src/builtins/weakproxy.rs index 1bdd7721ffe..35871c07b12 100644 --- a/crates/vm/src/builtins/weakproxy.rs +++ b/crates/vm/src/builtins/weakproxy.rs @@ -4,7 +4,7 @@ use crate::{ Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, class::PyClassImpl, common::hash::PyHash, - function::{OptionalArg, PyArithmeticValue, PyComparisonValue, PySetterValue}, + function::{FuncArgs, OptionalArg, PyArithmeticValue, PyComparisonValue, PySetterValue}, protocol::{PyIter, PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, stdlib::builtins::reversed, types::{ @@ -13,13 +13,22 @@ use crate::{ }, }; -#[pyclass(module = false, name = "weakproxy", unhashable = true, traverse)] +#[pyclass(module = false, name = "weakproxy", unhashable = true)] #[derive(Debug)] -pub struct PyWeakProxy { - weak: PyRef, -} +#[repr(transparent)] +pub struct PyWeakProxy(PyWeak); impl PyPayload for PyWeakProxy { + const PAYLOAD_TYPE_ID: core::any::TypeId = ::PAYLOAD_TYPE_ID; + + #[inline] + unsafe fn validate_downcastable_from(obj: &PyObject) -> bool { + ::BASICSIZE <= obj.class().slots.basicsize + && obj + .class() + .fast_issubclass(::static_type()) + } + #[inline] fn class(ctx: &Context) -> &'static Py { ctx.types.weakproxy_type @@ -37,52 +46,34 @@ pub struct WeakProxyNewArgs { impl Constructor for PyWeakProxy { type Args = WeakProxyNewArgs; - fn py_new( - _cls: &Py, - Self::Args { referent, callback }: Self::Args, - vm: &VirtualMachine, - ) -> PyResult { - let weak = Self::new_weak(referent.as_ref(), callback.into_option(), vm)?; - // TODO: PyWeakProxy should use the same payload as PyWeak - Ok(Self { weak }) + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let _ = cls; + let Self::Args { referent, callback } = args.bind(vm)?; + let callback = callback + .into_option() + .filter(|callback| !vm.is_none(callback)); + let proxy = Self::new_weakproxy(referent.as_ref(), callback, vm)?; + Ok(proxy.into()) } -} -crate::common::static_cell! { - static WEAK_SUBCLASS: PyTypeRef; + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + unimplemented!("use slot_new") + } } impl PyWeakProxy { - fn new_weak( - referent: &PyObject, - callback: Option, - vm: &VirtualMachine, - ) -> PyResult> { - // using an internal subclass as the class prevents us from getting the generic weakref, - // which would mess up the weakref count - let weak_cls = WEAK_SUBCLASS.get_or_init(|| { - vm.ctx.new_class( - None, - "__weakproxy", - vm.ctx.types.weakref_type.to_owned(), - super::PyWeak::make_slots(), - ) - }); - referent.downgrade_with_typ(callback, weak_cls.clone(), vm) - } - pub fn new_weakproxy( referent: &PyObject, callback: Option, vm: &VirtualMachine, - ) -> PyResult> { - let weak = Self::new_weak(referent, callback, vm)?; - Ok(Self { weak }.into_ref(&vm.ctx)) + ) -> PyResult> { + let typ = vm.ctx.types.weakproxy_type.to_owned(); + referent.downgrade_with_typ(callback, typ, vm) } #[must_use] - pub fn get_weak(&self) -> &PyRef { - &self.weak + pub fn get_weak(&self) -> &PyWeak { + &self.0 } } @@ -99,7 +90,7 @@ impl PyWeakProxy { ))] impl PyWeakProxy { fn try_upgrade(&self, vm: &VirtualMachine) -> PyResult { - self.weak.upgrade().ok_or_else(|| new_reference_error(vm)) + self.0.upgrade().ok_or_else(|| new_reference_error(vm)) } #[pymethod] diff --git a/crates/vm/src/builtins/weakref.rs b/crates/vm/src/builtins/weakref.rs index 11e21684724..e0f012f169c 100644 --- a/crates/vm/src/builtins/weakref.rs +++ b/crates/vm/src/builtins/weakref.rs @@ -49,7 +49,7 @@ impl Constructor for PyWeak { let referent = positional .next() .ok_or_else(|| vm.new_type_error("__new__ expected at least 1 argument, got 0"))?; - let callback = positional.next(); + let callback = positional.next().filter(|callback| !vm.is_none(callback)); if let Some(_extra) = positional.next() { let got = positional.count() + 3; return Err( @@ -92,7 +92,11 @@ impl PyWeak { } #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/byte.rs b/crates/vm/src/byte.rs index d9e927cbfa5..0e90f296ac9 100644 --- a/crates/vm/src/byte.rs +++ b/crates/vm/src/byte.rs @@ -1,11 +1,17 @@ //! byte operation APIs -use crate::object::AsObject; -use crate::{PyObject, PyResult, VirtualMachine}; + use num_traits::ToPrimitive; +use crate::{ + AsObject, PyObject, PyResult, VirtualMachine, + protocol::{BufferFlags, PyBuffer}, +}; + +// PyBytes_FromObject pub fn bytes_from_object(vm: &VirtualMachine, obj: &PyObject) -> PyResult> { - if let Ok(elements) = obj.try_bytes_like(vm, |bytes| bytes.to_vec()) { - return Ok(elements); + if obj.check_buffer() { + let buffer = PyBuffer::from_object(vm, obj, BufferFlags::FULL_RO)?; + return Ok(buffer.contiguous_or_collect(|bytes| bytes.to_vec())); } if !obj.fast_isinstance(vm.ctx.types.str_type) diff --git a/crates/vm/src/bytes_inner.rs b/crates/vm/src/bytes_inner.rs index 08518deb1c4..65a9dc0a01c 100644 --- a/crates/vm/src/bytes_inner.rs +++ b/crates/vm/src/bytes_inner.rs @@ -1,6 +1,7 @@ // spell-checker:ignore unchunked use crate::{ - AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, VirtualMachine, + AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, + VirtualMachine, anystr::{self, AnyStr, AnyStrContainer, AnyStrWrapper}, builtins::{ PyBaseExceptionRef, PyByteArray, PyBytes, PyBytesRef, PyInt, PyIntRef, PyStr, PyStrRef, @@ -9,9 +10,10 @@ use crate::{ byte::bytes_from_object, cformat::cformat_bytes, common::hash, + common::wtf8::is_py_ascii_whitespace, function::{ArgIterable, Either, OptionalArg, OptionalOption, PyComparisonValue}, literal::escape::Escape, - protocol::PyBuffer, + protocol::{BufferFlags, PyBuffer}, sequence::{SequenceExt, SequenceMutExt}, types::PyComparisonOp, }; @@ -34,9 +36,10 @@ impl From> for PyBytesInner { } } +/// "y*": any bytes-like object, and nothing else. impl<'a> TryFromBorrowedObject<'a> for PyBytesInner { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { - bytes_from_object(vm, obj).map(Self::from) + obj.try_bytes_like(vm, <[u8]>::to_vec).map(Self::from) } } @@ -75,7 +78,7 @@ impl ByteInnerNewOptions { } else { size as usize }; - Ok(vec![0; size].into()) + Ok(vm.new_zeroed_bytes(size)?.into()) } fn handle_object_fallback(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { @@ -136,10 +139,50 @@ impl ByteInnerNewOptions { } } +/// What is searched for: a bytes-like object, or a single byte given as an +/// integer. parse_args_finds_byte +pub enum ByteInnerSub { + Buffer(PyBytesInner), + Byte(PyIntRef), +} + +impl TryFromObject for ByteInnerSub { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + if obj.check_buffer() { + return PyBytesInner::try_from_object(vm, obj).map(Self::Buffer); + } + match obj.try_index_opt(vm) { + Some(int) => int.map(Self::Byte), + None => Err(vm.new_type_error(format!( + "argument should be integer or bytes-like object, not '{}'", + obj.class().name() + ))), + } + } +} + +impl ByteInnerSub { + /// The needle of a containment test, which is an integer if it is one at + /// all and a bytes-like object otherwise. bytes_contains + pub fn from_contains_arg(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + match obj.try_index_opt(vm) { + Some(int) => int.map(Self::Byte), + None => PyBytesInner::try_from_object(vm, obj).map(Self::Buffer), + } + } + + fn into_vec(self, vm: &VirtualMachine) -> PyResult> { + Ok(match self { + Self::Buffer(buffer) => buffer.elements, + Self::Byte(int) => vec![int.as_bigint().byte_or(vm)?], + }) + } +} + #[derive(FromArgs)] pub struct ByteInnerFindOptions { #[pyarg(positional)] - sub: Either, + sub: ByteInnerSub, #[pyarg(positional, default)] start: Option, #[pyarg(positional, default)] @@ -152,10 +195,7 @@ impl ByteInnerFindOptions { len: usize, vm: &VirtualMachine, ) -> PyResult<(Vec, core::ops::Range)> { - let sub = match self.sub { - Either::A(v) => v.elements.to_vec(), - Either::B(int) => vec![int.as_bigint().byte_or(vm)?], - }; + let sub = self.sub.into_vec(vm)?; let range = anystr::adjust_indices(self.start, self.end, len); Ok((sub, range)) } @@ -202,14 +242,11 @@ impl ByteInnerTranslateOptions { let table = self.table.map_or_else( || Ok((0..=u8::MAX).collect::>()), |v| { - let bytes = v - .try_into_value::(vm) - .ok() - .filter(|v| v.elements.len() == 256) - .ok_or_else(|| { - vm.new_value_error("translation table must be 256 characters long") - })?; - Ok(bytes.elements.to_vec()) + let bytes: PyBytesInner = v.try_into_value(vm)?; + if bytes.elements.len() != 256 { + return Err(vm.new_value_error("translation table must be 256 characters long")); + } + Ok(bytes.elements) }, )?; @@ -227,10 +264,35 @@ impl ByteInnerTranslateOptions { pub(crate) type ByteInnerSplitOptions = anystr::SplitArgs; +fn bytearray_repr_char_len(ch: u8) -> usize { + match ch { + b'\'' | b'\\' | b'\t' | b'\r' | b'\n' => 2, + 0x20..=0x7e => 1, + _ => 4, // \xHH + } +} + +fn write_bytearray_repr_char(ch: u8, buf: &mut String) { + match ch { + b'\'' => buf.push_str(r#"\'"#), + b'\\' => buf.push_str(r#"\\"#), + b'\t' => buf.push_str(r#"\t"#), + b'\n' => buf.push_str(r#"\n"#), + b'\r' => buf.push_str(r#"\r"#), + 0x20..=0x7e => buf.push(ch as char), + ch => { + const HEX: &[u8; 16] = b"0123456789abcdef"; + buf.push_str(r#"\x"#); + buf.push(HEX[(ch >> 4) as usize] as char); + buf.push(HEX[(ch & 0x0f) as usize] as char); + } + } +} + impl PyBytesInner { #[inline] - pub fn as_bytes(&self) -> &[u8] { - &self.elements + pub const fn as_bytes(&self) -> &[u8] { + self.elements.as_slice() } fn new_repr_overflow_error(vm: &VirtualMachine) -> PyBaseExceptionRef { @@ -250,17 +312,33 @@ impl PyBytesInner { } pub fn repr_with_name(&self, class_name: &str, vm: &VirtualMachine) -> PyResult { - const DECORATION_LEN: isize = 2 + 3; // 2 for (), 3 for b"" => bytearray(b"") - let escape = crate::literal::escape::AsciiEscape::new_repr(&self.elements); - let len = escape - .layout() - .len - .and_then(|len| (len as isize).checked_add(DECORATION_LEN + class_name.len() as isize)) - .ok_or_else(|| Self::new_repr_overflow_error(vm))? as usize; + const DECORATION_LEN: usize = 2 + 3; // 2 for (), 3 for b"" => bytearray(b"") + let quote = if self.elements.contains(&b'\'') && !self.elements.contains(&b'"') { + '"' + } else { + '\'' + }; + let body_len = self + .elements + .iter() + .try_fold(0usize, |len, &ch| { + len.checked_add(bytearray_repr_char_len(ch)) + }) + .ok_or_else(|| Self::new_repr_overflow_error(vm))?; + let len = class_name + .len() + .checked_add(DECORATION_LEN) + .and_then(|len| len.checked_add(body_len)) + .ok_or_else(|| Self::new_repr_overflow_error(vm))?; let mut buf = String::with_capacity(len); buf.push_str(class_name); buf.push('('); - escape.bytes_repr().write(&mut buf).unwrap(); + buf.push('b'); + buf.push(quote); + for &ch in &self.elements { + write_bytearray_repr_char(ch, &mut buf); + } + buf.push(quote); buf.push(')'); debug_assert_eq!(buf.len(), len); Ok(buf) @@ -303,7 +381,12 @@ impl PyBytesInner { // but not memoryview, and not equal if compare with unicode str(PyStr) PyComparisonValue::from_option( other - .try_bytes_like(vm, |other| op.eval_ord(self.elements.as_slice().cmp(other))) + .try_bytes_like(vm, |other| { + // Equality does not need the ordering, and answers two + // buffers of different length without reading either. + op.eval_eq(|| self.elements.as_slice() == other) + .unwrap_or_else(|| op.eval_ord(self.elements.as_slice().cmp(other))) + }) .ok(), ) } @@ -316,10 +399,10 @@ impl PyBytesInner { self.elements.py_add(other) } - pub fn contains(&self, needle: Either, vm: &VirtualMachine) -> PyResult { + pub fn contains(&self, needle: ByteInnerSub, vm: &VirtualMachine) -> PyResult { Ok(match needle { - Either::A(byte) => self.elements.contains_str(byte.elements.as_slice()), - Either::B(int) => self.elements.contains(&int.as_bigint().byte_or(vm)?), + ByteInnerSub::Buffer(sub) => self.elements.contains_str(sub.elements.as_slice()), + ByteInnerSub::Byte(int) => self.elements.contains(&int.as_bigint().byte_or(vm)?), }) } @@ -355,44 +438,40 @@ impl PyBytesInner { self.elements.py_isupper() } + // _Py_bytes_isspace pub fn isspace(&self) -> bool { + // is_ascii_whitespace excludes vertical tab, while Py_ISSPACE accepts it !self.elements.is_empty() && self .elements .iter() - .all(|x| char::from(*x).is_ascii_whitespace()) + .all(|x| x.is_ascii_whitespace() || *x == b'\x0b') } + // _Py_bytes_istitle pub fn istitle(&self) -> bool { - if self.elements.is_empty() { - return false; - } - - let mut iter = self.elements.iter().peekable(); - let mut prev_cased = false; + let mut cased = false; + let mut previous_is_cased = false; - while let Some(c) = iter.next() { - let current = char::from(*c); - let next = if let Some(k) = iter.peek() { - char::from(**k) - } else if current.is_uppercase() { - return !prev_cased; + for byte in &self.elements { + if byte.is_ascii_uppercase() { + if previous_is_cased { + return false; + } + previous_is_cased = true; + cased = true; + } else if byte.is_ascii_lowercase() { + if !previous_is_cased { + return false; + } + previous_is_cased = true; + cased = true; } else { - return prev_cased; - }; - - let is_cased = current.to_uppercase().next().unwrap() != current - || current.to_lowercase().next().unwrap() != current; - if (is_cased && next.is_uppercase() && !prev_cased) - || (!is_cased && next.is_lowercase()) - { - return false; + previous_is_cased = false; } - - prev_cased = is_cased; } - true + cased } pub fn lower(&self) -> Vec { @@ -479,7 +558,8 @@ impl PyBytesInner { pub fn fromhex_object(string: PyObjectRef, vm: &VirtualMachine) -> PyResult> { if let Some(s) = string.downcast_ref::() { Self::fromhex(s.as_bytes(), vm) - } else if let Ok(buffer) = PyBuffer::try_from_borrowed_object(vm, &string) { + } else if string.check_buffer() { + let buffer = PyBuffer::from_object(vm, &string, BufferFlags::SIMPLE)?; let borrowed = buffer .as_contiguous() .ok_or_else(|| vm.new_buffer_error("fromhex() requires a contiguous buffer"))?; @@ -496,16 +576,15 @@ impl PyBytesInner { fn _pad( &self, options: ByteInnerPaddingOptions, - pad: fn(&[u8], usize, u8, usize) -> Vec, + pad: PadFn, vm: &VirtualMachine, ) -> PyResult> { let (width, fillchar) = options.get_value("center", vm)?; let len = self.len(); - Ok(if len as isize >= width { - Vec::from(&self.elements[..]) - } else { - pad(&self.elements, width as usize, fillchar, len) - }) + if len as isize >= width { + return Ok(Vec::from(&self.elements[..])); + } + pad(&self.elements, width as usize, fillchar, len).ok_or_else(|| vm.new_memory_error("")) } pub fn center( @@ -741,8 +820,10 @@ impl PyBytesInner { self.elements.py_bytes_splitlines(options, into_wrapper) } - pub fn zfill(&self, width: isize) -> Vec { - self.elements.py_zfill(width) + pub fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult> { + self.elements + .py_zfill(width) + .ok_or_else(|| vm.new_memory_error("")) } // len(self)>=1, from="", len(to)>=1, max_count>=1 @@ -931,7 +1012,7 @@ impl PyBytesInner { } pub fn concat(&self, other: &PyObject, vm: &VirtualMachine) -> PyResult> { - let buffer = PyBuffer::try_from_borrowed_object(vm, other)?; + let buffer = PyBuffer::from_object(vm, other, BufferFlags::SIMPLE)?; let borrowed = buffer.as_contiguous(); if let Some(other) = borrowed { let mut v = Vec::with_capacity(self.elements.len() + other.len()); @@ -997,11 +1078,21 @@ impl AnyStrContainer<[u8]> for Vec { Self::with_capacity(capacity) } + fn try_with_capacity(capacity: usize) -> Option { + let mut v = Self::new(); + v.try_reserve_exact(capacity).ok()?; + Some(v) + } + fn push_str(&mut self, other: &[u8]) { self.extend(other) } } +/// A padding function from `AnyStr`, returning `None` for a width whose result +/// cannot be allocated. +type PadFn = fn(&[u8], usize, u8, usize) -> Option>; + const ASCII_WHITESPACES: [u8; 6] = [0x20, 0x09, 0x0a, 0x0c, 0x0d, 0x0b]; impl anystr::AnyChar for u8 { @@ -1112,6 +1203,14 @@ pub(crate) fn bytes_decode( .decode_text(zelf, encoding, errors, vm) } +#[derive(FromArgs)] +pub(crate) struct ByteInnerHexOptions { + #[pyarg(any, optional)] + pub sep: OptionalArg>, + #[pyarg(any, optional)] + pub bytes_per_sep: OptionalArg, +} + fn hex_impl_no_sep(bytes: &[u8]) -> String { let mut buf: Vec = vec![0; bytes.len() * 2]; hex::encode_to_slice(bytes, buf.as_mut_slice()).unwrap(); @@ -1207,10 +1306,6 @@ pub(crate) fn bytes_to_hex( } } -pub(crate) const fn is_py_ascii_whitespace(b: u8) -> bool { - matches!(b, b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' | b'\x0B') -} - /// ASCII-only title casing. /// /// This is purposely naive as is CPython's implementation. diff --git a/crates/vm/src/cformat.rs b/crates/vm/src/cformat.rs index 6bf6062c84f..7d47da39928 100644 --- a/crates/vm/src/cformat.rs +++ b/crates/vm/src/cformat.rs @@ -3,8 +3,9 @@ //! Implementation of Printf-Style string formatting //! as per the [Python Docs](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting). -use crate::common::cformat::*; -use crate::common::wtf8::{CodePoint, Wtf8, Wtf8Buf}; +use itertools::Itertools; +use num_traits::cast::ToPrimitive; + use crate::{ AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, @@ -12,12 +13,18 @@ use crate::{ PyBaseExceptionRef, PyByteArray, PyBytes, PyFloat, PyInt, PyStr, int::check_int_to_str_digits, try_f64_to_bigint, tuple, }, + common::{ + cformat::{ + CCharacterType, CConversionFlags, CFormatBytes, CFormatConversion, CFormatPart, + CFormatPrecision, CFormatQuantity, CFormatSpec, CFormatSpecKeyed, CFormatType, + CFormatWtf8, CNumberType, + }, + wtf8::{CodePoint, Wtf8, Wtf8Buf}, + }, function::ArgIntoFloat, - protocol::PyBuffer, + protocol::{BufferFlags, PyBuffer}, stdlib::builtins, }; -use itertools::Itertools; -use num_traits::cast::ToPrimitive; fn spec_format_bytes( vm: &VirtualMachine, @@ -32,23 +39,29 @@ fn spec_format_bytes( let b = builtins::ascii(obj, vm)?.as_bytes().to_vec(); Ok(b) } + // format_obj CFormatConversion::Str | CFormatConversion::Bytes => { - if let Ok(buffer) = PyBuffer::try_from_borrowed_object(vm, &obj) { - Ok(buffer.contiguous_or_collect(|bytes| spec.format_bytes(bytes))) - } else { - let bytes = vm - .get_special_method(&obj, identifier!(vm, __bytes__))? - .ok_or_else(|| { - vm.new_type_error(format!( - "%b requires a bytes-like object, or an object that \ - implements __bytes__, not '{}'", - obj.class().name() - )) - })? - .invoke((), vm)?; + if let Some(bytes) = obj.downcast_ref::() { + return Ok(spec.format_bytes(bytes.as_bytes())); + } + if let Some(bytearray) = obj.downcast_ref::() { + return Ok(spec.format_bytes(&bytearray.borrow_buf())); + } + if let Some(method) = vm.get_special_method(&obj, identifier!(vm, __bytes__))? { + let bytes = method.invoke((), vm)?; let bytes = PyBytes::try_from_borrowed_object(vm, &bytes)?; - Ok(spec.format_bytes(bytes.as_bytes())) + return Ok(spec.format_bytes(bytes.as_bytes())); + } + if obj.check_buffer() { + let buffer = PyBuffer::from_object(vm, &obj, BufferFlags::FULL_RO)?; + return Ok(buffer.contiguous_or_collect(|bytes| spec.format_bytes(bytes))); } + let msg = format!( + "%b requires a bytes-like object, or an object that \ + implements __bytes__, not '{}'", + obj.class().name() + ); + Err(vm.new_type_error(msg)) } }, CFormatType::Number(number_type) => match number_type { @@ -71,6 +84,7 @@ fn spec_format_bytes( check_int_to_str_digits(i.as_bigint(), vm)?; return Ok(spec.format_number(i.as_bigint()).into_bytes()); } + if let Some(method) = vm.get_method(obj.clone(), identifier!(vm, __int__)) { let result = method?.call((), vm)?; if let Some(i) = result.downcast_ref::() { @@ -78,6 +92,7 @@ fn spec_format_bytes( return Ok(spec.format_number(i.as_bigint()).into_bytes()); } } + Err(vm.new_type_error(format!( "%{} format: a real number is required, not {}", spec.format_type.to_char(), @@ -301,6 +316,7 @@ fn try_update_quantity_from_tuple<'a, I: Iterator>( let Some(CFormatQuantity::FromValuesTuple) = q else { return Ok(()); }; + let element = elements.next(); f.insert(try_conversion_flag_from_tuple( vm, @@ -319,6 +335,7 @@ fn try_update_precision_from_tuple<'a, I: Iterator>( let Some(CFormatPrecision::Quantity(CFormatQuantity::FromValuesTuple)) = p else { return Ok(()); }; + let quantity = try_update_quantity_from_element(vm, elements.next().map(|v| v.as_ref()))?; *p = Some(CFormatPrecision::Quantity(quantity)); Ok(()) @@ -347,42 +364,45 @@ pub(crate) fn cformat_bytes( && !values_obj.fast_isinstance(vm.ctx.types.bytearray_type); if num_specifiers == 0 { - // literal only - return if is_mapping - || values_obj + if !is_mapping + && values_obj .downcast_ref::() - .is_some_and(|e| e.is_empty()) + .is_none_or(|e| !e.is_empty()) { - for (_, part) in format.iter_mut() { - match part { - CFormatPart::Literal(literal) => result.append(literal), - CFormatPart::Spec(_) => unreachable!(), - } + return Err(vm.new_type_error("not all arguments converted during bytes formatting")); + } + + // literal only + for (_, part) in format.iter_mut() { + if let CFormatPart::Literal(literal) = part { + result.append(literal) + } else { + unreachable!() } - Ok(result) - } else { - Err(vm.new_type_error("not all arguments converted during bytes formatting")) - }; + } + + return Ok(result); } if mapping_required { + if !is_mapping { + return Err(vm.new_type_error("format requires a mapping")); + } + // dict - return if is_mapping { - for (_, part) in format { - match part { - CFormatPart::Literal(literal) => result.extend(literal), - CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => { - let key = mapping_key.unwrap(); - let value = values_obj.get_item(&key, vm)?; - let part_result = spec_format_bytes(vm, &spec, value)?; - result.extend(part_result); - } + for (_, part) in format { + match part { + CFormatPart::Literal(literal) => result.extend(literal), + CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => { + let key = mapping_key.unwrap(); + let value = values_obj.get_item(&key, vm)?; + let part_result = spec_format_bytes(vm, &spec, value)?; + result.extend(part_result); } } - Ok(result) - } else { - Err(vm.new_type_error("format requires a mapping")) - }; + } + + return Ok(result); } // tuple @@ -405,18 +425,18 @@ pub(crate) fn cformat_bytes( )?; try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?; - let value = match value_iter.next() { - Some(obj) => Ok(obj.clone()), - None => Err(vm.new_type_error("not enough arguments for format string")), - }?; - let part_result = spec_format_bytes(vm, &spec, value)?; + let Some(value) = value_iter.next() else { + return Err(vm.new_type_error("not enough arguments for format string")); + }; + + let part_result = spec_format_bytes(vm, &spec, value.clone())?; result.extend(part_result); } } } // check that all arguments were converted - if value_iter.next().is_some() && !is_mapping { + if !is_mapping && value_iter.next().is_some() { Err(vm.new_type_error("not all arguments converted during bytes formatting")) } else { Ok(result) @@ -441,41 +461,44 @@ pub(crate) fn cformat_string( && !values_obj.fast_isinstance(vm.ctx.types.str_type); if num_specifiers == 0 { - // literal only - return if is_mapping - || values_obj + if !is_mapping + && values_obj .downcast_ref::() - .is_some_and(|e| e.is_empty()) + .is_none_or(|e| !e.is_empty()) { - for (_, part) in format.iter() { - match part { - CFormatPart::Literal(literal) => result.push_wtf8(literal), - CFormatPart::Spec(_) => unreachable!(), - } + return Err(vm.new_type_error("not all arguments converted during string formatting")); + } + + // literal only + for (_, part) in format.iter() { + if let CFormatPart::Literal(literal) = part { + result.push_wtf8(literal) + } else { + unreachable!() } - Ok(result) - } else { - Err(vm.new_type_error("not all arguments converted during string formatting")) - }; + } + + return Ok(result); } if mapping_required { + if !is_mapping { + return Err(vm.new_type_error("format requires a mapping")); + } + // dict - return if is_mapping { - for (idx, part) in format { - match part { - CFormatPart::Literal(literal) => result.push_wtf8(&literal), - CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => { - let value = values_obj.get_item(&mapping_key.unwrap(), vm)?; - let part_result = spec_format_string(vm, &spec, value, idx)?; - result.push_wtf8(&part_result); - } + for (idx, part) in format { + match part { + CFormatPart::Literal(literal) => result.push_wtf8(&literal), + CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => { + let value = values_obj.get_item(&mapping_key.unwrap(), vm)?; + let part_result = spec_format_string(vm, &spec, value, idx)?; + result.push_wtf8(&part_result); } } - Ok(result) - } else { - Err(vm.new_type_error("format requires a mapping")) - }; + } + + return Ok(result); } // tuple @@ -484,6 +507,7 @@ pub(crate) fn cformat_string( } else { core::slice::from_ref(&values_obj) }; + let mut value_iter = values.iter(); for (idx, part) in format { @@ -498,18 +522,18 @@ pub(crate) fn cformat_string( )?; try_update_precision_from_tuple(vm, &mut value_iter, &mut spec.precision)?; - let value = match value_iter.next() { - Some(obj) => Ok(obj.clone()), - None => Err(vm.new_type_error("not enough arguments for format string")), - }?; - let part_result = spec_format_string(vm, &spec, value, idx)?; + let Some(value) = value_iter.next() else { + return Err(vm.new_type_error("not enough arguments for format string")); + }; + + let part_result = spec_format_string(vm, &spec, value.clone(), idx)?; result.push_wtf8(&part_result); } } } // check that all arguments were converted - if value_iter.next().is_some() && !is_mapping { + if !is_mapping && value_iter.next().is_some() { Err(vm.new_type_error("not all arguments converted during string formatting")) } else { Ok(result) diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index 2e8af54f974..d9f8b848d2a 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -5,7 +5,7 @@ use crate::{ builtins::{PyBaseObject, PyType, PyTypeRef, descriptor::PyWrapper}, function::PyMethodDef, object::Py, - types::{PyTypeFlags, PyTypeSlots, SLOT_DEFS, hash_not_implemented}, + types::{PyTypeFlags, PyTypeSlots, SLOT_DEFS, fn_addr, hash_not_implemented}, vm::Context, }; use rustpython_common::static_cell; @@ -24,11 +24,9 @@ pub fn add_operators(class: &'static Py, ctx: &Context) { // Special handling for __hash__ = None if def.name == "__hash__" - && class - .slots - .hash - .load() - .is_some_and(|h| h as usize == hash_not_implemented as *const () as usize) + && class.slots.hash.load().is_some_and(|h| { + fn_addr(h) == fn_addr(hash_not_implemented as crate::types::HashFunc) + }) { class.set_attr(ctx.names.__hash__, ctx.none.clone().into()); continue; @@ -66,16 +64,19 @@ pub fn add_operators(class: &'static Py, ctx: &Context) { pub trait StaticType { // Ideally, saving PyType is better than PyTypeRef fn static_cell() -> &'static static_cell::StaticCell; + #[inline] #[must_use] fn static_metaclass() -> &'static Py { PyType::static_type() } + #[inline] #[must_use] fn static_baseclass() -> &'static Py { PyBaseObject::static_type() } + #[inline] #[must_use] fn static_type() -> &'static Py { @@ -87,6 +88,7 @@ pub trait StaticType { } Self::static_cell().get().unwrap_or_else(|| fail()) } + #[must_use] fn init_manually(typ: PyTypeRef) -> &'static Py { let cell = Self::static_cell(); @@ -94,6 +96,7 @@ pub trait StaticType { .unwrap_or_else(|_| panic!("double initialization from init_manually")); cell.get().unwrap() } + #[must_use] fn init_builtin_type() -> &'static Py where @@ -105,6 +108,7 @@ pub trait StaticType { .unwrap_or_else(|_| panic!("double initialization of {}", Self::NAME)); cell.get().unwrap() } + #[must_use] fn create_static_type() -> PyTypeRef where @@ -137,14 +141,19 @@ pub trait PyClassDef { pub trait PyClassImpl: PyClassDef { const TP_FLAGS: PyTypeFlags = PyTypeFlags::DEFAULT; + const METHOD_DEFS: &'static [PyMethodDef]; + + fn impl_extend_class(ctx: &'static Context, class: &'static Py); + + fn extend_slots(slots: &mut PyTypeSlots); + fn extend_class(ctx: &'static Context, class: &'static Py) where Self: Sized, { + // NOTE: `is_created_with_flags` if only available when debug_assertions is true #[cfg(debug_assertions)] - { - assert!(class.slots.flags.is_created_with_flags()); - } + debug_assert!(class.slots.flags.is_created_with_flags()); let _ = ctx.intern_str(Self::NAME); // intern type name @@ -161,7 +170,9 @@ pub trait PyClassImpl: PyClassDef { .into(), ); } + Self::impl_extend_class(ctx, class); + if let Some(doc) = Self::DOC { // Only set __doc__ if it doesn't already exist (e.g., as a member descriptor) // This matches CPython's behavior in type_dict_set_doc @@ -170,6 +181,7 @@ pub trait PyClassImpl: PyClassDef { class.set_attr(doc_attr_name, ctx.new_str(doc).into()); } } + if let Some(module_name) = Self::MODULE_NAME { let module_key = identifier!(ctx, __module__); // Don't overwrite a getset descriptor for __module__ (e.g. TypeAliasType @@ -191,7 +203,7 @@ pub trait PyClassImpl: PyClassDef { let object_new = ctx.types.object_type.slots.new.load(); let is_object_itself = core::ptr::eq(class, ctx.types.object_type); let is_inherited_from_object = !is_object_itself - && object_new.is_some_and(|obj_new| slot_new as usize == obj_new as usize); + && object_new.is_some_and(|obj_new| fn_addr(slot_new) == fn_addr(obj_new)); if !is_inherited_from_object { let bound_new = @@ -230,10 +242,6 @@ pub trait PyClassImpl: PyClassDef { .to_owned() } - fn impl_extend_class(ctx: &'static Context, class: &'static Py); - const METHOD_DEFS: &'static [PyMethodDef]; - fn extend_slots(slots: &mut PyTypeSlots); - fn make_slots() -> PyTypeSlots { let mut slots = PyTypeSlots { flags: Self::TP_FLAGS, diff --git a/crates/vm/src/codecs.rs b/crates/vm/src/codecs.rs index a07ffb47e77..073fd102f38 100644 --- a/crates/vm/src/codecs.rs +++ b/crates/vm/src/codecs.rs @@ -1,14 +1,19 @@ +use alloc::borrow::Cow; +use core::ops::{Deref, Range}; +use std::collections::HashMap; + use rustpython_common::{ + ascii, borrow::BorrowedValue, encodings::{ CodecContext, DecodeContext, DecodeErrorHandler, EncodeContext, EncodeErrorHandler, EncodeReplace, StrBuffer, StrSize, errors, }, + lock::{OnceCell, PyRwLock}, str::StrKind, wtf8::{CodePoint, Wtf8, Wtf8Buf}, }; -use crate::common::lock::OnceCell; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, @@ -16,13 +21,9 @@ use crate::{ PyBaseExceptionRef, PyBytes, PyBytesRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyUtf8Str, PyUtf8StrRef, }, - common::{ascii, lock::PyRwLock}, convert::ToPyObject, function::{ArgBytesLike, PyMethodDef}, }; -use alloc::borrow::Cow; -use core::ops::{self, Range}; -use std::collections::HashMap; pub struct CodecsRegistry { inner: PyRwLock, @@ -39,6 +40,7 @@ pub(crate) const DEFAULT_ENCODING: &str = "utf-8"; #[derive(Clone)] #[repr(transparent)] pub struct PyCodec(PyTupleRef); + impl PyCodec { #[inline] pub fn from_tuple(tuple: PyTupleRef) -> Result { @@ -48,10 +50,12 @@ impl PyCodec { Err(tuple) } } + #[inline] pub fn into_tuple(self) -> PyTupleRef { self.0 } + #[inline] pub fn as_tuple(&self) -> &Py { &self.0 @@ -61,6 +65,7 @@ impl PyCodec { pub fn get_encode_func(&self) -> &PyObject { &self.0[0] } + #[inline] pub fn get_decode_func(&self) -> &PyObject { &self.0[1] @@ -116,10 +121,7 @@ impl PyCodec { errors: Option, vm: &VirtualMachine, ) -> PyResult { - let args = match errors { - Some(e) => vec![e.into()], - None => vec![], - }; + let args = errors.map_or_else(Vec::new, |e| vec![e.into()]); vm.call_method(self.0.as_object(), "incrementalencoder", args) } @@ -128,10 +130,7 @@ impl PyCodec { errors: Option, vm: &VirtualMachine, ) -> PyResult { - let args = match errors { - Some(e) => vec![e.into()], - None => vec![], - }; + let args = errors.map_or_else(Vec::new, |e| vec![e.into()]); vm.call_method(self.0.as_object(), "incrementaldecoder", args) } } @@ -191,16 +190,17 @@ impl CodecsRegistry { ("namereplace", methods[5].build_function(ctx)), ("surrogatepass", methods[6].build_function(ctx)), ("surrogateescape", methods[7].build_function(ctx)), - ]; - let errors = errors - .into_iter() - .map(|(name, f)| (name.to_owned(), f.into())) - .collect(); + ] + .into_iter() + .map(|(name, f)| (name.to_owned(), f.into())) + .collect(); + let inner = RegistryInner { search_path: Vec::new(), search_cache: HashMap::new(), errors, }; + Self { inner: PyRwLock::new(inner), } @@ -210,6 +210,7 @@ impl CodecsRegistry { if !search_function.is_callable() { return Err(vm.new_type_error("argument must be callable")); } + self.inner.write().search_path.push(search_function); Ok(()) } @@ -250,6 +251,7 @@ impl CodecsRegistry { } inner.search_path.clone() }; + let encoding: PyUtf8StrRef = vm.ctx.new_utf8_str(encoding.as_ref()); for func in search_path { let res = func.call((encoding.clone(),), vm)?; @@ -264,6 +266,7 @@ impl CodecsRegistry { return Ok(codec.clone()); } } + Err(vm.new_lookup_error(format!("unknown encoding: {encoding}"))) } @@ -274,6 +277,7 @@ impl CodecsRegistry { vm: &VirtualMachine, ) -> PyResult { let codec = self.lookup(encoding, vm)?; + if codec.is_text_codec(vm)? { Ok(codec) } else { @@ -430,7 +434,7 @@ fn normalize_encoding_name(encoding: &str) -> Cow<'_, str> { out.into() } -#[derive(Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq)] enum StandardEncoding { Utf8, Utf16Be, @@ -455,12 +459,14 @@ impl StandardEncoding { let encoding = encoding .strip_prefix(|c| ['-', '_'].contains(&c)) .unwrap_or(encoding); + if encoding == "8" { Some(Self::Utf8) } else if let Some(encoding) = encoding.strip_prefix("16") { if encoding.is_empty() { return Some(Self::UTF_16_NE); } + let encoding = encoding.strip_prefix(['-', '_']).unwrap_or(encoding); match encoding { "be" => Some(Self::Utf16Be), @@ -471,6 +477,7 @@ impl StandardEncoding { if encoding.is_empty() { return Some(Self::UTF_32_NE); } + let encoding = encoding.strip_prefix(['-', '_']).unwrap_or(encoding); match encoding { "be" => Some(Self::Utf32Be), @@ -504,10 +511,12 @@ impl<'a> EncodeErrorHandler> for SurrogatePass { let mut out: Vec = Vec::with_capacity(num_chars * 4); for ch in err_str.code_points() { let c = ch.to_u32(); - let 0xd800..=0xdfff = c else { + + if !(0xd800..=0xdfff).contains(&c) { // Not a surrogate, fail with original exception return Err(ctx.error_encoding(range, reason)); - }; + } + match standard_encoding { StandardEncoding::Utf8 => out.extend(ch.encode_wtf8(&mut [0; 4]).as_bytes()), StandardEncoding::Utf16Le => out.extend((c as u16).to_le_bytes()), @@ -601,7 +610,9 @@ impl<'a> PyEncodeContext<'a> { impl CodecContext for PyEncodeContext<'_> { type Error = PyBaseExceptionRef; + type StrBuf = PyStrRef; + type BytesBuf = PyBytesRef; fn string(&self, s: Wtf8Buf) -> Self::StrBuf { @@ -612,6 +623,7 @@ impl CodecContext for PyEncodeContext<'_> { self.vm.ctx.new_bytes(b) } } + impl EncodeContext for PyEncodeContext<'_> { fn full_data(&self) -> &Wtf8 { self.data.as_wtf8() @@ -690,12 +702,15 @@ pub(crate) struct PyDecodeContext<'a> { pos: usize, exception: OnceCell, } + enum PyDecodeData<'a> { Original(BorrowedValue<'a, [u8]>), Modified(PyBytesRef), } -impl ops::Deref for PyDecodeData<'_> { + +impl Deref for PyDecodeData<'_> { type Target = [u8]; + fn deref(&self) -> &Self::Target { match self { PyDecodeData::Original(data) => data, @@ -719,7 +734,9 @@ impl<'a> PyDecodeContext<'a> { impl CodecContext for PyDecodeContext<'_> { type Error = PyBaseExceptionRef; + type StrBuf = PyStrRef; + type BytesBuf = PyBytesRef; fn string(&self, s: Wtf8Buf) -> Self::StrBuf { @@ -730,6 +747,7 @@ impl CodecContext for PyDecodeContext<'_> { self.vm.ctx.new_bytes(b) } } + impl DecodeContext for PyDecodeContext<'_> { fn full_data(&self) -> &[u8] { &self.data @@ -784,7 +802,7 @@ impl DecodeContext for PyDecodeContext<'_> { } else { vm.ctx.new_bytes(self.data.to_vec()) }; - vm.new_unicode_decode_error_real( + vm.new_unicode_decode_error( vm.ctx.new_str(self.encoding), data, byte_range.start, @@ -872,17 +890,19 @@ enum ResolvedError { impl<'a> ErrorsHandler<'a> { #[inline] pub(crate) fn new(errors: Option<&'a Py>, vm: &VirtualMachine) -> Self { - match errors { - Some(errors) => Self { + if let Some(errors) = errors { + Self { errors, resolved: OnceCell::new(), - }, - None => Self { + } + } else { + Self { errors: identifier_utf8!(vm, strict), resolved: OnceCell::from(ResolvedError::Standard(StandardError::Strict)), - }, + } } } + #[inline] fn resolve(&self, vm: &VirtualMachine) -> PyResult<&ResolvedError> { if let Some(val) = self.resolved.get() { @@ -901,11 +921,13 @@ impl<'a> ErrorsHandler<'a> { Ok(self.resolved.get().unwrap()) } } + impl StrBuffer for PyStrRef { fn is_compatible_with(&self, kind: StrKind) -> bool { self.kind() <= kind } } + impl<'a> EncodeErrorHandler> for ErrorsHandler<'_> { fn handle_encode_error( &self, @@ -956,6 +978,7 @@ impl<'a> EncodeErrorHandler> for ErrorsHandler<'_> { Ok((replace, restart)) } } + impl<'a> DecodeErrorHandler> for ErrorsHandler<'_> { fn handle_decode_error( &self, @@ -1110,8 +1133,10 @@ where fn extract_unicode_error_range(err: &PyObject, vm: &VirtualMachine) -> PyResult> { let start = err.get_attr("start", vm)?; let start = start.try_into_value(vm)?; + let end = err.get_attr("end", vm)?; let end = end.try_into_value(vm)?; + Ok(Range { start, end }) } @@ -1134,10 +1159,12 @@ fn update_unicode_error_attrs( fn is_encode_err(err: &PyObject, vm: &VirtualMachine) -> bool { err.fast_isinstance(vm.ctx.exceptions.unicode_encode_error) } + #[inline] fn is_decode_err(err: &PyObject, vm: &VirtualMachine) -> bool { err.fast_isinstance(vm.ctx.exceptions.unicode_decode_error) } + #[inline] fn is_translate_err(err: &PyObject, vm: &VirtualMachine) -> bool { err.fast_isinstance(vm.ctx.exceptions.unicode_translate_error) @@ -1151,10 +1178,9 @@ fn bad_err_type(err: PyObjectRef, vm: &VirtualMachine) -> PyBaseExceptionRef { } fn strict_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let err = err + Err(err .downcast() - .unwrap_or_else(|_| vm.new_type_error("codec must pass exception instance")); - Err(err) + .unwrap_or_else(|_| vm.new_type_error("codec must pass exception instance"))) } fn ignore_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(PyObjectRef, usize)> { diff --git a/crates/vm/src/compiler.rs b/crates/vm/src/compiler.rs index 25fa33302a5..9842bc0d1c7 100644 --- a/crates/vm/src/compiler.rs +++ b/crates/vm/src/compiler.rs @@ -1,45 +1,42 @@ +#[cfg(all(not(feature = "compiler"), feature = "parser", feature = "codegen",))] +compile_error!("Use --features=compiler to enable both parser and codegen"); + #[cfg(feature = "codegen")] pub use rustpython_codegen::CompileOpts; -#[cfg(feature = "compiler")] -pub use rustpython_compiler::*; - -#[cfg(not(feature = "compiler"))] -pub use rustpython_compiler_core::Mode; +cfg_select! { + feature = "compiler" => { + pub use rustpython_compiler::*; + } + _ => { + pub use ruff_python_parser as parser; -#[cfg(not(feature = "compiler"))] -pub use rustpython_compiler_core as core; + pub use rustpython_compiler_core::Mode; + pub use rustpython_compiler_core as core; + } +} #[cfg(not(feature = "compiler"))] -pub use ruff_python_parser as parser; +#[derive(Debug, thiserror::Error)] +pub enum CompileErrorType { + #[cfg(feature = "codegen")] + #[error(transparent)] + Codegen(#[from] super::codegen::error::CodegenErrorType), + #[cfg(feature = "parser")] + #[error(transparent)] + Parse(#[from] super::parser::ParseErrorType), +} #[cfg(not(feature = "compiler"))] -mod error { - #[cfg(all(feature = "parser", feature = "codegen"))] - panic!("Use --features=compiler to enable both parser and codegen"); - - #[derive(Debug, thiserror::Error)] - pub enum CompileErrorType { - #[cfg(feature = "codegen")] - #[error(transparent)] - Codegen(#[from] super::codegen::error::CodegenErrorType), - #[cfg(feature = "parser")] - #[error(transparent)] - Parse(#[from] super::parser::ParseErrorType), - } - - #[derive(Debug, thiserror::Error)] - pub enum CompileError { - #[cfg(feature = "codegen")] - #[error(transparent)] - Codegen(#[from] super::codegen::error::CodegenError), - #[cfg(feature = "parser")] - #[error(transparent)] - Parse(#[from] super::parser::ParseError), - } +#[derive(Debug, thiserror::Error)] +pub enum CompileError { + #[cfg(feature = "codegen")] + #[error(transparent)] + Codegen(#[from] super::codegen::error::CodegenError), + #[cfg(feature = "parser")] + #[error(transparent)] + Parse(#[from] super::parser::ParseError), } -#[cfg(not(feature = "compiler"))] -pub use error::{CompileError, CompileErrorType}; #[cfg(any(feature = "parser", feature = "codegen"))] impl crate::convert::ToPyException for (CompileError, Option<&str>) { diff --git a/crates/vm/src/convert/try_from.rs b/crates/vm/src/convert/try_from.rs index 85d6f5e20e3..10b1449d7eb 100644 --- a/crates/vm/src/convert/try_from.rs +++ b/crates/vm/src/convert/try_from.rs @@ -1,10 +1,11 @@ +use malachite_bigint::Sign; +use num_traits::ToPrimitive; + use crate::{ Py, VirtualMachine, builtins::PyFloat, object::{AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult}, }; -use malachite_bigint::Sign; -use num_traits::ToPrimitive; /// Implemented by any type that can be created from a Python object. /// @@ -62,7 +63,7 @@ impl PyObject { } } -/// Lower-cost variation of `TryFromObject` +/// Lower-cost variation of [`TryFromObject`]. pub trait TryFromBorrowedObject<'a>: Sized where Self: 'a, @@ -126,12 +127,15 @@ impl TryFromObject for core::time::Duration { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { if let Some(float) = obj.downcast_ref::() { let f = float.to_f64(); + if f.is_nan() { return Err(vm.new_value_error("Invalid value NaN (not a number)")); } + if f < 0.0 { return Err(vm.new_value_error("negative duration")); } + if !f.is_finite() || f > u64::MAX as f64 { return Err(vm.new_overflow_error("timestamp too large to convert to C PyTime_t")); } diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 34d280acdca..61431ea82e3 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -3,7 +3,7 @@ use crate::{ builtins::PyStrRef, common::lock::PyMutex, exceptions::types::PyBaseException, - frame::{ExecutionResult, Frame, FrameOwner, FrameRef}, + frame::{ExecutionResult, FrameObject, FrameObjectRef, FrameOwner}, function::OptionalArg, object::{PyAtomicRef, Traverse, TraverseFn}, protocol::PyIterReturn, @@ -23,13 +23,14 @@ impl ExecutionResult { }; PyIterReturn::StopIteration(arg) } + Self::TailCall => unreachable!("TailCall in generator/coroutine"), } } } #[derive(Debug)] pub struct Coro { - frame: FrameRef, + frame: FrameObjectRef, pub closed: AtomicCell, // TODO: https://github.com/RustPython/RustPython/pull/3183#discussion_r720560652 running: AtomicCell, // code @@ -50,6 +51,21 @@ unsafe impl Traverse for Coro { } } +/// An exclusive claim on a generator's frame, released when dropped. +/// +/// Only the holder may look at the frame or resume it. Resuming decides from +/// the frame state whether the sent value goes on the value stack, so a state +/// read taken before the claim can be answered by a frame that another thread +/// then advances: resuming it leaves the stack short of what the code after +/// the yield pops. +struct RunningGuard<'a>(&'a Coro); + +impl Drop for RunningGuard<'_> { + fn drop(&mut self) { + self.0.running.store(false); + } +} + fn gen_name(jen: &PyObject, vm: &VirtualMachine) -> &'static str { let typ = jen.class(); if typ.is(vm.ctx.types.coroutine_type) { @@ -62,7 +78,7 @@ fn gen_name(jen: &PyObject, vm: &VirtualMachine) -> &'static str { } impl Coro { - pub fn new(frame: FrameRef, name: PyStrRef, qualname: PyStrRef) -> Self { + pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { Self { frame, closed: AtomicCell::new(false), @@ -73,65 +89,79 @@ impl Coro { } } - fn maybe_close(&self, res: &PyResult, entered_frame: bool) { - if !entered_frame { - return; + /// Free the finished frame's locals and stack, unless a frame object has + /// escaped (e.g. through an `f_locals` proxy or `sys._getframe`). An + /// escaped frame husk owns its heap-resident locals and must keep them + /// readable after the generator closes. + fn clear_frame_locals_on_close(&self) { + // Keep locals alive if a durable frame reference escaped (e.g. through + // an `f_locals` proxy or `sys._getframe`): that reference now owns the + // heap-resident locals and must keep them readable after close, + // matching `take_ownership`. + if !self.frame.has_escaped() { + self.frame.clear_locals_and_stack(); } + } + + /// Retire the generator if the frame it just ran came to an end. The claim + /// is still held, so a thread waiting for it cannot resume a frame that has + /// already finished. + fn maybe_close(&self, res: &PyResult, _claim: &RunningGuard<'_>) { match res { Ok(ExecutionResult::Return(_)) | Err(_) => { self.closed.store(true); - // Frame is no longer suspended; allow frame.clear() to succeed. - self.frame.owner.store( + // FrameObject is no longer suspended; allow frame.clear() to succeed. + self.frame.iframe().owner.store( FrameOwner::FrameObject as i8, core::sync::atomic::Ordering::Release, ); // Completed generators/coroutines should not keep their locals // alive while the wrapper object itself remains referenced. - self.frame.clear_locals_and_stack(); + self.clear_frame_locals_on_close(); } Ok(ExecutionResult::Yield(_)) => {} + Ok(ExecutionResult::TailCall) => unreachable!("TailCall in generator/coroutine"), } } - fn run_with_context( + /// Take the frame for this thread, or report that another thread holds it. + /// + /// What the resume depends on -- whether the generator is closed, and + /// whether it has started -- has to be read from here onwards. + fn claim(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult> { + if self.running.compare_exchange(false, true).is_err() { + return Err(vm.new_value_error(format!("{} already executing", gen_name(jen, vm)))); + } + Ok(RunningGuard(self)) + } + + fn run_claimed( &self, - jen: &PyObject, + _claim: &RunningGuard<'_>, vm: &VirtualMachine, func: F, - ) -> (PyResult, bool) + ) -> PyResult where - F: FnOnce(&Py) -> PyResult, + F: FnOnce(&Py) -> PyResult, { - if self.running.compare_exchange(false, true).is_err() { - return ( - Err(vm.new_value_error(format!("{} already executing", gen_name(jen, vm)))), - false, - ); - } - - // SAFETY: running.compare_exchange guarantees exclusive access + // SAFETY: the claim guarantees exclusive access let gen_exc = unsafe { self.exception.swap(None) }; let exception_ptr = &self.exception as *const PyAtomicRef>; - let result = vm.resume_gen_frame(&self.frame, gen_exc, |f| { + vm.resume_gen_frame(&self.frame, gen_exc, |f| { let result = func(f); - // SAFETY: exclusive access guaranteed by running flag + // SAFETY: exclusive access guaranteed by the claim let _old = unsafe { (*exception_ptr).swap(vm.current_exception()) }; result - }); - - self.running.store(false); - (result, true) + }) } fn finalize_send_result( &self, result: PyResult, - entered_frame: bool, jen: &PyObject, vm: &VirtualMachine, ) -> PyResult { - self.maybe_close(&result, entered_frame); match result { Ok(exec_res) => Ok(exec_res.into_iter_return(vm)), Err(e) => { @@ -161,19 +191,20 @@ impl Coro { if self.closed.load() { return Ok(PyIterReturn::StopIteration(None)); } - if self.running.load() { - return Err(vm.new_value_error(format!("{} already executing", gen_name(jen, vm)))); + let claim = self.claim(jen, vm)?; + // The generator can have run to its end in the meantime. + if self.closed.load() { + return Ok(PyIterReturn::StopIteration(None)); } let value = if self.frame.lasti() > 0 { Some(vm.ctx.none()) } else { None }; - let (result, entered_frame) = self.run_with_context(jen, vm, |f| { - self.frame.locals_to_fast(vm)?; - f.resume(value, vm) - }); - self.finalize_send_result(result, entered_frame, jen, vm) + let result = self.run_claimed(&claim, vm, |f| f.resume(value, vm)); + self.maybe_close(&result, &claim); + drop(claim); + self.finalize_send_result(result, jen, vm) } pub fn send( @@ -185,8 +216,10 @@ impl Coro { if self.closed.load() { return Ok(PyIterReturn::StopIteration(None)); } - if self.running.load() { - return Err(vm.new_value_error(format!("{} already executing", gen_name(jen, vm)))); + let claim = self.claim(jen, vm)?; + // The generator can have run to its end in the meantime. + if self.closed.load() { + return Ok(PyIterReturn::StopIteration(None)); } let value = if self.frame.lasti() > 0 { Some(value) @@ -198,11 +231,10 @@ impl Coro { } else { None }; - let (result, entered_frame) = self.run_with_context(jen, vm, |f| { - self.frame.locals_to_fast(vm)?; - f.resume(value, vm) - }); - self.finalize_send_result(result, entered_frame, jen, vm) + let result = self.run_claimed(&claim, vm, |f| f.resume(value, vm)); + self.maybe_close(&result, &claim); + drop(claim); + self.finalize_send_result(result, jen, vm) } pub fn throw( @@ -227,13 +259,25 @@ impl Coro { // Validate exception type before entering generator context. // Invalid types propagate to caller without closing the generator. crate::exceptions::ExceptionCtor::try_from_object(vm, exc_type.clone())?; - let (result, entered_frame) = - self.run_with_context(jen, vm, |f| f.gen_throw(vm, exc_type, exc_val, exc_tb)); - self.maybe_close(&result, entered_frame); + let claim = self.claim(jen, vm)?; + // The generator can have run to its end in the meantime. Normalizing + // runs the exception's constructor, so let the claim go first. + if self.closed.load() { + drop(claim); + return Err(vm.normalize_exception(exc_type, exc_val, exc_tb)?); + } + let result = self.run_claimed(&claim, vm, |f| f.gen_throw(vm, exc_type, exc_val, exc_tb)); + self.maybe_close(&result, &claim); + drop(claim); Ok(result?.into_iter_return(vm)) } pub fn close(&self, jen: &PyObject, vm: &VirtualMachine) -> PyResult { + if self.closed.load() { + return Ok(vm.ctx.none()); + } + let claim = self.claim(jen, vm)?; + // The generator can have run to its end in the meantime. if self.closed.load() { return Ok(vm.ctx.none()); } @@ -242,7 +286,7 @@ impl Coro { self.closed.store(true); return Ok(vm.ctx.none()); } - let (result, entered_frame) = self.run_with_context(jen, vm, |f| { + let result = self.run_claimed(&claim, vm, |f| { f.gen_throw( vm, vm.ctx.exceptions.generator_exit.to_owned().into(), @@ -250,16 +294,11 @@ impl Coro { vm.ctx.none(), ) }); - if !entered_frame { - return match result { - Err(err) => Err(err), - Ok(_) => unreachable!("run_with_context preflight returned without an error"), - }; - } self.closed.store(true); // Release frame locals and stack to free references held by the // closed generator, matching gen_send_ex2 with close_on_completion. - self.frame.clear_locals_and_stack(); + self.clear_frame_locals_on_close(); + drop(claim); match result { Ok(ExecutionResult::Yield(_)) => { Err(vm.new_runtime_error(format!("{} ignored GeneratorExit", gen_name(jen, vm)))) @@ -282,7 +321,7 @@ impl Coro { self.closed.load() } - pub fn frame(&self) -> FrameRef { + pub fn frame(&self) -> FrameObjectRef { self.frame.clone() } @@ -329,7 +368,8 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes || obj.downcast_ref::().is_some_and(|g| { g.as_coro() .frame() - .code + .iframe() + .code() .flags .contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE) }) @@ -344,7 +384,8 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes || result.downcast_ref::().is_some_and(|g| { g.as_coro() .frame() - .code + .iframe() + .code() .flags .contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE) }) diff --git a/crates/vm/src/datastack.rs b/crates/vm/src/datastack.rs index 101369fba57..ec4c22ae808 100644 --- a/crates/vm/src/datastack.rs +++ b/crates/vm/src/datastack.rs @@ -61,6 +61,9 @@ pub struct DataStack { top: *mut u8, /// End of usable space in the current chunk. limit: *mut u8, + /// Most recently popped full-frame allocation whose localsplus slots were + /// cleared before the pop. An exact LIFO reuse can skip zero-filling them. + reusable_frame: Option<(*mut u8, usize)>, } impl DataStack { @@ -73,7 +76,12 @@ impl DataStack { // Skip one ALIGN-sized slot in the root chunk so that `pop()` never // frees it (`push_chunk` convention). let top = unsafe { top.add(ALIGN) }; - Self { chunk, top, limit } + Self { + chunk, + top, + limit, + reusable_frame: None, + } } /// Check if the current chunk has at least `size` bytes available. @@ -91,6 +99,26 @@ impl DataStack { /// (LIFO order). #[inline(always)] pub fn push(&mut self, size: usize) -> *mut u8 { + self.reusable_frame = None; + self.push_inner(size) + } + + /// Allocate a full interpreter frame and report whether it exactly reuses + /// a just-cleared frame block. + #[inline(always)] + pub fn push_frame(&mut self, size: usize) -> (*mut u8, bool) { + let reusable_frame = self.reusable_frame.take(); + let ptr = self.push_inner(size); + // Exact sizes, not aligned ones: the caller reads "reused" as "every + // slot of this frame was cleared by the last one", and two frames whose + // sizes differ by less than ALIGN share an aligned size while the + // larger one's tail slots were never touched, let alone cleared. + let reused = reusable_frame.is_some_and(|(base, old_size)| base == ptr && old_size == size); + (ptr, reused) + } + + #[inline(always)] + fn push_inner(&mut self, size: usize) -> *mut u8 { let aligned_size = (size + ALIGN - 1) & !(ALIGN - 1); unsafe { if self.top.add(aligned_size) <= self.limit { @@ -138,6 +166,24 @@ impl DataStack { /// and all allocations made after it must already have been popped. #[inline(always)] pub unsafe fn pop(&mut self, base: *mut u8) { + self.reusable_frame = None; + unsafe { self.pop_inner(base) }; + } + + /// Pop a full frame whose localsplus slots have already been cleared. + /// + /// # Safety + /// `base` and `size` must describe the most recent allocation returned by + /// `push_frame`, every later allocation must already be popped, and all + /// localsplus slots in the frame must have been cleared. + #[inline(always)] + pub unsafe fn pop_frame(&mut self, base: *mut u8, size: usize) { + unsafe { self.pop_inner(base) }; + self.reusable_frame = Some((base, size)); + } + + #[inline(always)] + unsafe fn pop_inner(&mut self, base: *mut u8) { debug_assert!(!base.is_null()); if self.is_in_current_chunk(base) { // Common case: base is within the current chunk. diff --git a/crates/vm/src/dict_inner.rs b/crates/vm/src/dict_inner.rs index 8112bbe252b..76d2c50f0cb 100644 --- a/crates/vm/src/dict_inner.rs +++ b/crates/vm/src/dict_inner.rs @@ -20,8 +20,8 @@ use alloc::fmt; use core::mem::size_of; use core::ops::ControlFlow; use core::sync::atomic::{ - AtomicU64, - Ordering::{Acquire, Release}, + AtomicU32, + Ordering::{AcqRel, Acquire, Relaxed, Release}, }; use num_traits::ToPrimitive; @@ -39,7 +39,104 @@ type EntryIndex = usize; pub(crate) struct Dict { inner: PyRwLock>, - version: AtomicU64, + /// Keys-version stamp, assigned lazily by `assign_keys_version` and + /// reset to 0 whenever the key set changes. Value-only updates keep it. + /// + /// A nonzero stamp identifies either a *shape* — an exact hole-free + /// entry sequence of interned string keys, shared by every dict with + /// that layout — or, when no shape is derivable, this dict's key set + /// frozen at assignment time. Either way a stamp match guarantees the + /// entry layout is exactly the one the stamp was issued for, so entry + /// indexes cached against a stamp stay valid wherever the stamp matches. + keys_version: AtomicU32, +} + +/// Source of keys-version stamps. Allocated globally so a shape stamp and a +/// dict-unique stamp can never collide. +static KEYS_VERSION: AtomicU32 = AtomicU32::new(0); + +/// Allocate a new keys-version stamp. Returns 0 once the stamp space is +/// exhausted; stamps are only allocated on specialization, so exhaustion is +/// unrealistic in practice. +fn next_keys_version() -> u32 { + KEYS_VERSION + .try_update(Relaxed, Relaxed, |v| v.checked_add(1)) + .map_or(0, |v| v + 1) +} + +/// Largest key count eligible for a shared shape stamp. +const SHAPE_MAX_KEYS: usize = 32; +/// Shape registry slot count (power of two). +const SHAPE_TABLE_SIZE: usize = 1 << 12; +/// Linear-probe limit before giving up on registering a shape. +const SHAPE_MAX_PROBE: usize = 8; + +/// A registered shape: the ordered interned-key-pointer sequence of a +/// hole-free dict, plus the stamp shared by every dict with that layout. +/// Interned strings are never freed, so the addresses are stable identities. +struct ShapeData { + keys: Box<[usize]>, + stamp: u32, +} + +/// Lock-free registry mapping shapes to shared stamps. Fixed-size open +/// addressing; slots are installed with CAS and never removed, so a stamp +/// permanently means "exactly this entry sequence". Registered `ShapeData` +/// is intentionally leaked (bounded by the table size). Lock-free makes the +/// registry safe across fork() without reinitialization. +static SHAPE_TABLE: std::sync::LazyLock]>> = + std::sync::LazyLock::new(|| { + (0..SHAPE_TABLE_SIZE) + .map(|_| core::sync::atomic::AtomicPtr::new(core::ptr::null_mut())) + .collect() + }); + +fn shape_stamp(shape: &[usize]) -> Option { + use core::hash::BuildHasher; + use core::sync::atomic::AtomicPtr; + // The hasher seed must be process-stable so equal shapes always probe + // the same slots. + static SHAPE_HASHER: std::sync::LazyLock = + std::sync::LazyLock::new(Default::default); + let hash = SHAPE_HASHER.hash_one(shape) as usize; + let mut candidate: *mut ShapeData = core::ptr::null_mut(); + let mut result = None; + for probe in 0..SHAPE_MAX_PROBE { + let slot: &AtomicPtr = &SHAPE_TABLE[(hash + probe) & (SHAPE_TABLE_SIZE - 1)]; + let mut installed = slot.load(Acquire); + if installed.is_null() { + if candidate.is_null() { + let stamp = next_keys_version(); + if stamp == 0 { + break; + } + candidate = Box::into_raw(Box::new(ShapeData { + keys: shape.into(), + stamp, + })); + } + match slot.compare_exchange(core::ptr::null_mut(), candidate, AcqRel, Acquire) { + Ok(_) => { + // SAFETY: candidate was just leaked into the table. + result = Some(unsafe { (*candidate).stamp }); + candidate = core::ptr::null_mut(); + break; + } + Err(current) => installed = current, + } + } + // SAFETY: non-null slots reference leaked ShapeData, never freed. + let data = unsafe { &*installed }; + if *data.keys == *shape { + result = Some(data.stamp); + break; + } + } + if !candidate.is_null() { + // SAFETY: the candidate lost the race and was never shared. + drop(unsafe { Box::from_raw(candidate) }); + } + result } unsafe impl Traverse for Dict { @@ -104,7 +201,7 @@ impl Clone for Dict { fn clone(&self) -> Self { Self { inner: PyRwLock::new(self.inner.read().clone()), - version: AtomicU64::new(0), + keys_version: AtomicU32::new(0), } } } @@ -118,7 +215,7 @@ impl Default for Dict { indices: vec![IndexEntry::FREE; 8], entries: Vec::new(), }), - version: AtomicU64::new(0), + keys_version: AtomicU32::new(0), } } } @@ -140,6 +237,10 @@ pub struct DictSize { filled: usize, } +/// The dict was resized under an iterator holding an older [`DictSize`]. +#[derive(Debug)] +pub(crate) struct DictChanged; + struct GenIndexes { idx: HashIndex, perturb: HashValue, @@ -209,7 +310,7 @@ impl DictInner { key: PyObjectRef, value: T, index_entry: IndexEntry, - ) { + ) -> usize { let entry = DictEntry { hash: hash_value, key, @@ -230,6 +331,9 @@ impl DictInner { self.resize(new_size) } } + // A resize keeps entry positions and rewrites only the index-index, so + // this stays the entry's index afterwards. + entry_index } const fn size(&self) -> DictSize { @@ -262,14 +366,81 @@ impl DictInner { type PopInnerResult = ControlFlow>>; impl Dict { - /// Monotonically increasing version counter for mutation tracking. - pub(crate) fn version(&self) -> u64 { - self.version.load(Acquire) + /// Current keys-version stamp, or 0 if none has been assigned since the + /// last key-set change. Equal nonzero stamps guarantee an unchanged key + /// set (values may differ). + pub(crate) fn keys_version(&self) -> u32 { + self.keys_version.load(Acquire) + } + + /// Return the current keys-version stamp, assigning one if none is set. + /// Returns 0 only if no stamp could be allocated. + /// + /// When the dict is hole-free and all keys are interned strings, the + /// stamp is the *shared shape stamp* for that exact key sequence, so + /// dicts with identical layouts (e.g. instances of the same class built + /// by the same `__init__`) carry equal stamps and one cached stamp or + /// entry index serves them all. Otherwise a dict-unique stamp is used. + /// + /// The shape inspection and the stamp install happen under the inner + /// read lock. Key-set changes reset the stamp under the write lock, so + /// an installed stamp always attests the layout it was derived from. + pub(crate) fn assign_keys_version(&self) -> u32 { + let version = self.keys_version.load(Acquire); + if version != 0 { + return version; + } + let inner = self.read(); + // Re-check under the lock: a concurrent assign may have won. + let version = self.keys_version.load(Acquire); + if version != 0 { + return version; + } + let new_version = Self::derive_shape_stamp(&inner).unwrap_or_else(next_keys_version); + if new_version == 0 { + return 0; + } + // Only install over 0 so an already-valid stamp is never replaced. + match self + .keys_version + .compare_exchange(0, new_version, AcqRel, Acquire) + { + Ok(_) => new_version, + Err(current) => current, + } + } + + /// Compute the shared shape stamp for the current layout, if it + /// qualifies: hole-free entries, bounded size, all keys interned strings. + fn derive_shape_stamp(inner: &DictInner) -> Option { + if inner.entries.len() != inner.used || inner.used > SHAPE_MAX_KEYS { + return None; + } + let shape = inner + .entries + .iter() + .map(|entry| { + let key = &entry.as_ref()?.key; + key.is_interned() + .then(|| key.as_ref() as *const PyObject as usize) + }) + .collect::>>()?; + shape_stamp(&shape) } - /// Bump the version counter after any mutation. - fn bump_version(&self) { - self.version.fetch_add(1, Release); + /// Reset the keys-version stamp on a key-set change (insert of a new + /// key, deletion, or clear). Value-only updates keep the stamp. + /// + /// Must be called while holding the write lock, *before* the key set is + /// modified: a lock-free stamp reader that still observes the old stamp + /// then provably ran before the change became visible, so acting on the + /// old key set is linearizable. A stamp assigned concurrently (between + /// this reset and the mutation) can only be trusted by a caller whose + /// subsequent probe serializes after the mutation through the inner + /// lock, which then reflects the new key set. Since stamps are never + /// reused, a cached stamp can never spuriously match again. + fn invalidate_keys_version(&self) { + self.keys_version.store(0, Release); } fn read(&self) -> PyRwLockReadGuard<'_, DictInner> { @@ -286,7 +457,44 @@ impl Dict { K: DictKey + ?Sized, { let hash = key.key_hash(vm)?; - let _removed = loop { + self.insert_known_hash(vm, key, hash, value) + } + + /// Store a key whose hash the caller already knows. + /// + /// `hash` must equal `key.key_hash(vm)`; a wrong one lands the entry in a + /// bucket no lookup probes, silently losing the key. Only pass a hash from + /// [`Self::keys_with_hashes`] on a container holding this same key. + pub(crate) fn insert_known_hash( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + value: T, + ) -> PyResult<()> + where + K: DictKey + ?Sized, + { + self.insert_known_hash_indexed(vm, key, hash, value)?; + Ok(()) + } + + /// [`Self::insert_known_hash`], also reporting the entry index it stored to. + /// + /// The index doubles as a `hint` for [`Self::get_hint`] / + /// [`Self::insert_with_hint`], so a caller that wants one gets it from the + /// store itself instead of probing the dict a second time. + fn insert_known_hash_indexed( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + value: T, + ) -> PyResult + where + K: DictKey + ?Sized, + { + let (stored_index, _removed) = loop { let (entry_index, index_index) = self.lookup(vm, key, hash, None)?; let mut inner = self.write(); if let Some(index) = entry_index.index() { @@ -305,9 +513,8 @@ impl Dict { )] if entry.index == index_index { let removed = core::mem::replace(&mut entry.value, value); - self.bump_version(); // defer dec RC - break Some(removed); + break (index, Some(removed)); } else { // stuff shifted around, let's try again } @@ -320,12 +527,18 @@ impl Dict { // Dict was resized since lookup, retry continue; } - inner.unchecked_push(index_index, hash, key.to_pyobject(vm), value, entry_index); - self.bump_version(); - break None; + self.invalidate_keys_version(); + let stored = inner.unchecked_push( + index_index, + hash, + key.to_pyobject(vm), + value, + entry_index, + ); + break (stored, None); } }; - Ok(()) + Ok(stored_index) } pub(crate) fn contains( @@ -334,7 +547,18 @@ impl Dict { key: &K, ) -> PyResult { let key_hash = key.key_hash(vm)?; - let (entry, _) = self.lookup(vm, key, key_hash, None)?; + self.contains_known_hash(vm, key, key_hash) + } + + /// [`Self::contains`] with a known hash. Same contract as + /// [`Self::insert_known_hash`]. + pub(crate) fn contains_known_hash( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + ) -> PyResult { + let (entry, _) = self.lookup(vm, key, hash, None)?; Ok(entry.index().is_some()) } @@ -366,6 +590,62 @@ impl Dict { Ok(u16::try_from(index).ok()) } + /// Retrieve a key along with its entry index, for hint caching. + /// + /// Same as [`Self::get`], but on a hit also returns the entry index + /// usable as a `hint` for [`Self::get_hint`] (`None` if it doesn't fit). + pub(crate) fn get_with_hint( + &self, + vm: &VirtualMachine, + key: &K, + ) -> PyResult)>> { + let hash = key.key_hash(vm)?; + let ret = loop { + let (entry, index_index) = self.lookup(vm, key, hash, None)?; + if let Some(index) = entry.index() { + let inner = self.read(); + if let Some(entry) = inner.get_entry_checked(index, index_index) { + // The dict was not changed since we did lookup + break Some((entry.value.clone(), u16::try_from(index).ok())); + } + // The dict was changed since we did lookup. Let's try again. + } else { + break None; + } + }; + Ok(ret) + } + + /// Replace the value at entry index `hint` if that entry's key is + /// identical to `key`, otherwise fall back to a full probing store. + /// + /// On a hint miss, returns a refreshed hint for the key (`None` when the + /// hint hit or no hint is representable). + pub(crate) fn insert_with_hint( + &self, + vm: &VirtualMachine, + key: &K, + hint: usize, + value: T, + ) -> PyResult> { + let value = { + let mut inner = self.write(); + match inner.entries.get_mut(hint) { + Some(Some(entry)) if key.key_is(&entry.key) => { + let removed = core::mem::replace(&mut entry.value, value); + drop(inner); + // defer dec RC until after the lock is released + drop(removed); + return Ok(None); + } + _ => value, + } + }; + let hash = key.key_hash(vm)?; + let stored = self.insert_known_hash_indexed(vm, key, hash, value)?; + Ok(u16::try_from(stored).ok()) + } + /// Fast path lookup using a cached entry index (`hint`). /// /// Returns `None` if the hint is stale or the key no longer matches. @@ -393,6 +673,22 @@ impl Dict { } } + /// Read an entry directly when a cached keys-version still describes the + /// dictionary layout. The version is rechecked while holding the read lock + /// so the entry index and value are observed from the same key-set state. + #[inline] + pub(crate) fn get_index_if_keys_version(&self, version: u32, index: usize) -> Option { + let inner = self.read(); + if self.keys_version.load(Acquire) != version { + return None; + } + inner + .entries + .get(index) + .and_then(Option::as_ref) + .map(|entry| entry.value.clone()) + } + fn _get_inner( &self, vm: &VirtualMachine, @@ -400,7 +696,12 @@ impl Dict { hash: HashValue, ) -> PyResult> { let ret = loop { - let (entry, index_index) = self.lookup(vm, key, hash, None)?; + let (entry, index_index) = + match self.lookup_extract(vm, key, hash, None, |entry| entry.value.clone())? { + // Read under the probe's own guard: nothing to re-check. + (_, Some(value)) => break Some(value), + (lookup, None) => lookup, + }; if let Some(index) = entry.index() { let inner = self.read(); if let Some(entry) = inner.get_entry_checked(index, index_index) { @@ -433,11 +734,11 @@ impl Dict { pub(crate) fn clear(&self) { let _removed = { let mut inner = self.write(); + self.invalidate_keys_version(); inner.indices.clear(); inner.indices.resize(8, IndexEntry::FREE); inner.used = 0; inner.filled = 0; - self.bump_version(); // defer dec rc core::mem::take(&mut inner.entries) }; @@ -462,6 +763,21 @@ impl Dict { self.remove_if_exists(vm, key).map(|opt| opt.is_some()) } + /// [`Self::delete_if_exists`] with a known hash. Same contract as + /// [`Self::insert_known_hash`]. + pub(crate) fn delete_if_exists_known_hash( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + ) -> PyResult + where + K: DictKey + ?Sized, + { + self.remove_if_known_hash(vm, key, hash, |_| Ok(true)) + .map(|opt| opt.is_some()) + } + pub(crate) fn delete_if(&self, vm: &VirtualMachine, key: &K, pred: F) -> PyResult where K: DictKey + ?Sized, @@ -490,6 +806,22 @@ impl Dict { F: Fn(&T) -> PyResult, { let hash = key.key_hash(vm)?; + self.remove_if_known_hash(vm, key, hash, pred) + } + + /// [`Self::remove_if`] with a known hash. Same contract as + /// [`Self::insert_known_hash`]. + fn remove_if_known_hash( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + pred: F, + ) -> PyResult> + where + K: DictKey + ?Sized, + F: Fn(&T) -> PyResult, + { let removed = loop { let lookup = self.lookup(vm, key, hash, None)?; match self.pop_inner_if(lookup, &pred)? { @@ -507,6 +839,18 @@ impl Dict { value: T, ) -> PyResult<()> { let hash = key.key_hash(vm)?; + self.delete_or_insert_known_hash(vm, key, hash, value) + } + + /// [`Self::delete_or_insert`] with a known hash. Same contract as + /// [`Self::insert_known_hash`]. + pub(crate) fn delete_or_insert_known_hash( + &self, + vm: &VirtualMachine, + key: &PyObject, + hash: HashValue, + value: T, + ) -> PyResult<()> { let _removed = loop { let lookup = self.lookup(vm, key, hash, None)?; let (entry, index_index) = lookup; @@ -521,8 +865,8 @@ impl Dict { if inner.indices.get(index_index) != Some(&entry) { continue; } + self.invalidate_keys_version(); inner.unchecked_push(index_index, hash, key.to_owned(), value, entry); - self.bump_version(); break None; }; Ok(()) @@ -551,6 +895,7 @@ impl Dict { let value = default .take() .expect("default must only be computed on insertion")(); + self.invalidate_keys_version(); inner.unchecked_push( index_index, hash, @@ -558,7 +903,6 @@ impl Dict { value.clone(), index_entry, ); - self.bump_version(); return Ok(value); } } @@ -594,8 +938,8 @@ impl Dict { .expect("default must only be computed on insertion")(); let key_obj = key.to_pyobject(vm); let ret = (key_obj.clone(), value.clone()); + self.invalidate_keys_version(); inner.unchecked_push(index_index, hash, key_obj, value, index_entry); - self.bump_version(); return Ok(ret); } } @@ -608,6 +952,58 @@ impl Dict { self.read().size() } + /// Step to the first live entry at or after `position`, verifying the size + /// against `old` under the same read guard. + /// + /// `project` runs under that guard, so it must not run Python or take + /// another dict lock; it is there so an iterator clones only the field it + /// keeps rather than both the key and the value. + pub(crate) fn next_entry_checked( + &self, + mut position: EntryIndex, + old: &DictSize, + project: impl FnOnce(&PyObjectRef, &T) -> R, + ) -> Result, DictChanged> { + let inner = self.read(); + if inner.size() != *old { + return Err(DictChanged); + } + loop { + let Some(entry) = inner.entries.get(position) else { + return Ok(None); + }; + position += 1; + if let Some(entry) = entry { + return Ok(Some((position, project(&entry.key, &entry.value)))); + } + } + } + + /// [`Self::next_entry_checked`] in reverse. + pub(crate) fn prev_entry_checked( + &self, + mut position: EntryIndex, + old: &DictSize, + project: impl FnOnce(&PyObjectRef, &T) -> R, + ) -> Result, DictChanged> { + let inner = self.read(); + if inner.size() != *old { + return Err(DictChanged); + } + loop { + let Some(entry) = inner.entries.get(position) else { + return Ok(None); + }; + if let Some(entry) = entry { + return Ok(Some((position, project(&entry.key, &entry.value)))); + } + if position == 0 { + return Ok(None); + } + position -= 1; + } + } + pub(crate) fn next_entry(&self, mut position: EntryIndex) -> Option<(usize, PyObjectRef, T)> { let inner = self.read(); loop { @@ -623,10 +1019,13 @@ impl Dict { let inner = self.read(); loop { let entry = inner.entries.get(position)?; - position = position.saturating_sub(1); if let Some(entry) = entry { break Some((position, entry.key.clone(), entry.value.clone())); } + if position == 0 { + break None; + } + position -= 1; } } @@ -647,6 +1046,16 @@ impl Dict { .collect() } + /// All keys paired with the hash stored in their entry, for feeding + /// [`Self::insert_known_hash`] without re-calling `__hash__`. + pub(crate) fn keys_with_hashes(&self) -> Vec<(PyObjectRef, HashValue)> { + self.read() + .entries + .iter() + .filter_map(|v| v.as_ref().map(|v| (v.key.clone(), v.hash))) + .collect() + } + pub(crate) fn values(&self) -> Vec { self.read() .entries @@ -681,8 +1090,30 @@ impl Dict { vm: &VirtualMachine, key: &K, hash_value: HashValue, - mut lock: Option>>, + lock: Option>>, ) -> PyResult { + let (ret, _) = self.lookup_extract(vm, key, hash_value, lock, |_| ())?; + Ok(ret) + } + + /// [`Self::lookup`], additionally reading the matched entry when the probe + /// settles it by key identity. + /// + /// That is the common case, and it is decided while the read guard is still + /// held — so a caller that only wants the entry's value gets it here instead + /// of taking the lock a second time to re-find what the probe already had. + /// `extract` therefore runs under the guard and must not run Python. It is + /// not called when the key had to be compared with `key_eq`, which does run + /// Python and so releases the guard first. + #[cfg_attr(feature = "flame-it", flame("Dict"))] + fn lookup_extract( + &self, + vm: &VirtualMachine, + key: &K, + hash_value: HashValue, + mut lock: Option>>, + extract: impl Fn(&DictEntry) -> R, + ) -> PyResult<(LookupResult, Option)> { let mut idxs = None; let mut free_slot = None; let ret = 'outer: loop { @@ -712,7 +1143,7 @@ impl Dict { Some(free) => (IndexEntry::DUMMY, free), None => (IndexEntry::FREE, index_index), }; - return Ok(idxs); + return Ok((idxs, None)); } idx => { let entry = unsafe { @@ -728,7 +1159,7 @@ impl Dict { reason = "Keeping the empty `else` block here for documentation" )] if key.key_is(&entry.key) { - break 'outer ret; + return Ok((ret, Some(extract(entry)))); } else if entry.hash == hash_value { break (entry.key.clone(), ret); } else { @@ -753,7 +1184,7 @@ impl Dict { // warn!("Perturb value: {}", i); }; - Ok(ret) + Ok((ret, None)) } // returns Err(()) if changed since lookup @@ -787,13 +1218,13 @@ impl Dict { // The dict was changed since we did lookup. Let's try again. _ => return Ok(ControlFlow::Continue(())), } + self.invalidate_keys_version(); *unsafe { // index_index is result of lookup inner.indices.get_unchecked_mut(index_index) } = IndexEntry::DUMMY; inner.used -= 1; let removed = slot.take(); - self.bump_version(); Ok(ControlFlow::Break(removed)) } @@ -822,12 +1253,12 @@ impl Dict { break entry; } }; + self.invalidate_keys_version(); inner.used -= 1; *unsafe { // entry.index always refers valid index inner.indices.get_unchecked_mut(entry.index) } = IndexEntry::DUMMY; - self.bump_version(); Some((entry.key, entry.value)) } @@ -843,6 +1274,7 @@ impl Dict { /// This is used for circular reference resolution in GC. /// Requires &mut self to avoid lock contention. pub(crate) fn drain_entries(&mut self) -> impl Iterator + '_ { + self.keys_version.store(0, Release); let inner = self.inner.get_mut(); inner.used = 0; inner.filled = 0; diff --git a/crates/vm/src/eval.rs b/crates/vm/src/eval.rs index 5f52799d0b9..5a3a804688f 100644 --- a/crates/vm/src/eval.rs +++ b/crates/vm/src/eval.rs @@ -6,7 +6,7 @@ pub fn eval(vm: &VirtualMachine, source: &str, scope: Scope, source_path: &str) debug!("Code object: {bytecode:?}"); vm.run_code_obj(bytecode, scope) } - Err(err) => Err(vm.new_syntax_error(&err, Some(source))), + Err(err) => Err(err.into_pyexception(vm, Some(source))), } } diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index 198273a6914..11c13912b76 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -60,7 +60,7 @@ pub(super) mod types { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } @@ -71,11 +71,8 @@ pub(super) mod types { vm: &VirtualMachine, ) -> PyResult { let message = zelf.get_arg(0).unwrap_or_else(|| vm.ctx.new_str("").into()); - vm.invoke_exception( - vm.ctx.exceptions.base_exception_group.to_owned(), - vec![message, excs], - ) - .map(|e| e.into()) + vm.invoke_exception(vm.ctx.exceptions.base_exception_group, vec![message, excs]) + .map(|e| e.into()) } #[pymethod] @@ -258,20 +255,11 @@ pub(super) mod types { ))); } - // Validate exceptions is a sequence (not set or None) + // Validate exceptions is a sequence let exceptions_arg = &args[1]; - - // Check for set/frozenset (not a sequence - unordered) - if exceptions_arg.fast_isinstance(vm.ctx.types.set_type) - || exceptions_arg.fast_isinstance(vm.ctx.types.frozenset_type) - { - return Err(vm.new_type_error("second argument (exceptions) must be a sequence")); - } - - // Check for None - if exceptions_arg.is(&vm.ctx.none) { - return Err(vm.new_type_error("second argument (exceptions) must be a sequence")); - } + exceptions_arg.try_sequence(vm).map_err(|_| { + vm.new_type_error("second argument (exceptions) must be a sequence") + })?; let exceptions: Vec = exceptions_arg.try_to_value(vm).map_err(|_| { vm.new_type_error("second argument (exceptions) must be a sequence") @@ -334,7 +322,7 @@ pub(super) mod types { let exceptions_tuple = vm.ctx.new_tuple(exceptions); let init_args = vec![message, exceptions_tuple.into()]; PyBaseException::new(init_args, vm) - .into_ref_with_type(vm, actual_cls) + .into_ref_with_type_lazy_dict(vm, actual_cls) .map(Into::into) } diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index df22f4d822d..0a1c2cb75ee 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -15,6 +15,7 @@ use crate::{ suggestion::offer_suggestions, types::{Callable, Constructor, Initializer, Representable}, }; +use core::fmt::{self, Display, Formatter}; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; #[cfg(feature = "host_env")] @@ -244,7 +245,11 @@ impl VirtualMachine { _ => true, }; - if same_line { + // A lone continuation at EOF has no highlighted source span. + let lone_line_continuation = + maybe_end_offset == Some(-1) && l_text.to_string_lossy() == "\\"; + + if same_line && !lone_line_continuation { let mut end_offset = match maybe_end_offset { Some(0) | None => offset, Some(end_offset) => end_offset, @@ -353,11 +358,11 @@ impl VirtualMachine { pub fn invoke_exception( &self, - cls: PyTypeRef, + cls: &Py, args: Vec, ) -> PyResult { // TODO: fast-path built-in exceptions by directly instantiating them? Is that really worth it? - let res = PyType::call(&cls, args.into_args(self), self)?; + let res = PyType::call(cls, args.into_args(self), self)?; res.downcast::().map_err(|obj| { self.new_type_error(format!( "calling {} should have returned an instance of BaseException, not {}", @@ -400,13 +405,13 @@ fn write_traceback_entry( output: &mut W, tb_entry: &Py, ) -> Result<(), W::Error> { - let filename = tb_entry.frame.code.source_path().as_str(); + let filename = tb_entry.frame.iframe().code().source_path().as_str(); writeln!( output, r##" File "{}", line {}, in {}"##, filename.trim_start_matches(r"\\?\"), tb_entry.lineno, - tb_entry.frame.code.obj_name + tb_entry.frame.iframe().code().obj_name )?; #[cfg(feature = "host_env")] @@ -444,7 +449,7 @@ impl TryFromObject for ExceptionCtor { impl ExceptionCtor { pub fn instantiate(self, vm: &VirtualMachine) -> PyResult { match self { - Self::Class(cls) => vm.invoke_exception(cls, vec![]), + Self::Class(cls) => vm.invoke_exception(&cls, vec![]), Self::Instance(exc) => Ok(exc), } } @@ -472,7 +477,7 @@ impl ExceptionCtor { exc @ PyBaseException => exc.args().to_vec(), obj => vec![obj], }); - vm.invoke_exception(cls, args) + vm.invoke_exception(&cls, args) } } } @@ -690,10 +695,8 @@ impl PyRef { #[pymethod] fn add_note(self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { - let dict = self - .as_object() - .dict() - .ok_or_else(|| vm.new_attribute_error("Exception object has no __dict__"))?; + let dict = crate::builtins::object::object_get_dict(self.as_object().to_owned(), vm) + .map_err(|_| vm.new_attribute_error("Exception object has no __dict__"))?; let notes = if let Ok(notes) = dict.get_item("__notes__", vm) { notes @@ -705,7 +708,7 @@ impl PyRef { let notes = notes .downcast::() - .map_err(|_| vm.new_type_error("__notes__ must be a list"))?; + .map_err(|_| vm.new_type_error("Cannot add note: __notes__ is not a list"))?; notes.borrow_vec_mut().push(note.into()); Ok(()) @@ -747,7 +750,7 @@ impl Constructor for PyBaseException { return Err(vm.new_type_error("BaseException() takes no keyword arguments")); } Self::new(args.args, vm) - .into_ref_with_type(vm, cls) + .into_ref_with_type_lazy_dict(vm, cls) .map(Into::into) } @@ -961,17 +964,13 @@ impl ExceptionZoo { "exceptions" => ctx.new_readonly_getset("exceptions", excs.base_exception_group, make_arg_getter(1)), }); - extend_exception!(PySystemExit, ctx, excs.system_exit, { - "code" => ctx.new_readonly_getset("code", excs.system_exit, system_exit_code), - }); + extend_exception!(PySystemExit, ctx, excs.system_exit); extend_exception!(PyKeyboardInterrupt, ctx, excs.keyboard_interrupt); extend_exception!(PyGeneratorExit, ctx, excs.generator_exit); extend_exception!(PyException, ctx, excs.exception_type); - extend_exception!(PyStopIteration, ctx, excs.stop_iteration, { - "value" => ctx.none(), - }); + extend_exception!(PyStopIteration, ctx, excs.stop_iteration); extend_exception!(PyStopAsyncIteration, ctx, excs.stop_async_iteration); extend_exception!(PyArithmeticError, ctx, excs.arithmetic_error); @@ -989,6 +988,9 @@ impl ExceptionZoo { extend_exception!(PyImportError, ctx, excs.import_error, { "msg" => ctx.new_readonly_getset("msg", excs.import_error, make_arg_getter(0)), + "name" => ctx.none(), + "path" => ctx.none(), + "name_from" => ctx.none(), }); extend_exception!(PyModuleNotFoundError, ctx, excs.module_not_found_error); @@ -1102,19 +1104,6 @@ fn syntax_error_set_msg(exc: PyBaseExceptionRef, value: PySetterValue, vm: &Virt *args = PyTuple::new_ref(new_args, &vm.ctx); } -fn system_exit_code(exc: PyBaseExceptionRef) -> Option { - // SystemExit.code based on args length: - // - size == 0: code is None - // - size == 1: code is args[0] - // - size > 1: code is args (the whole tuple) - let args = exc.args.read(); - Some(match args.len() { - 0 => return None, - 1 => args.first().unwrap().clone(), - _ => args.as_object().to_owned(), - }) -} - #[cfg(feature = "serde")] pub struct SerializeException<'vm, 's> { vm: &'vm VirtualMachine, @@ -1207,20 +1196,62 @@ impl serde::Serialize for SerializeException<'_, '_> { } } -pub fn cstring_error(vm: &VirtualMachine) -> PyBaseExceptionRef { +#[derive(Debug)] +pub struct NulError; + +impl Display for NulError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "embedded null character") + } +} + +pub fn nul_char_error(vm: &VirtualMachine) -> PyBaseExceptionRef { vm.new_value_error("embedded null character") } +pub fn nul_char_type_error(vm: &VirtualMachine) -> PyBaseExceptionRef { + vm.new_type_error("embedded null character") +} + +pub fn nul_byte_error(vm: &VirtualMachine) -> PyBaseExceptionRef { + vm.new_value_error("embedded null byte") +} + impl ToPyException for alloc::ffi::NulError { fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { - cstring_error(vm) + nul_char_error(vm) + } +} + +impl ToPyException for alloc::ffi::FromVecWithNulError { + fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { + nul_char_error(vm) + } +} + +impl ToPyException for core::ffi::FromBytesWithNulError { + fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { + nul_char_error(vm) + } +} + +impl ToPyException for NulError { + fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { + nul_char_error(vm) } } #[cfg(windows)] impl ToPyException for widestring::error::ContainsNul { fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { - cstring_error(vm) + nul_char_error(vm) + } +} + +#[cfg(windows)] +impl ToPyException for widestring::error::MissingNulTerminator { + fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { + vm.new_value_error(self.to_string()) } } @@ -1361,14 +1392,8 @@ impl OSErrorBuilder { vec![strerror.to_pyobject(vm)] }; - let payload = PyOSError::py_new(&exc_type, args.clone().into(), vm) - .expect("new_os_error usage error"); - let os_error = payload - .into_ref_with_type(vm, exc_type) - .expect("new_os_error usage error"); - PyOSError::slot_init(os_error.as_object().to_owned(), args.into(), vm) - .expect("new_os_error usage error"); - os_error + vm.new_payload_exception::(exc_type, args.into()) + .expect("new_os_error usage error") } } @@ -1585,7 +1610,7 @@ impl ToPyException for rustpython_host_env::multiprocessing::SemError { pub(super) mod types { use crate::common::lock::PyRwLock; - use crate::object::{MaybeTraverse, Traverse, TraverseFn}; + use crate::object::{Traverse, TraverseFn}; #[cfg_attr(target_arch = "wasm32", allow(unused_imports))] use crate::{ AsObject, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, @@ -1595,7 +1620,7 @@ pub(super) mod types { tuple::IntoPyTuple, }, convert::ToPyResult, - function::{ArgBytesLike, FuncArgs, KwArgs}, + function::{ArgBytesLike, FuncArgs, KwArgs, PySetterValue}, set_attrs, types::{Constructor, Initializer}, }; @@ -1626,22 +1651,65 @@ pub(super) mod types { pub(super) args: PyRwLock, } - #[pyexception(name, base = PyBaseException, ctx = "system_exit")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PySystemExit(PyBaseException); + #[pyexception(name, base = PyBaseException, ctx = "system_exit", traverse = "manual")] + #[repr(C)] + pub struct PySystemExit { + base: PyBaseException, + code: PyAtomicRef>, + } - // SystemExit_init: has its own __init__ that sets the code attribute - #[pyexception(with(Initializer))] - impl PySystemExit {} + impl crate::class::PySubclass for PySystemExit { + type Base = PyBaseException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + unsafe impl Traverse for PySystemExit { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.traverse(tracer_fn); + if let Some(obj) = self.code.deref() { + tracer_fn(obj); + } + } + } + + impl core::fmt::Debug for PySystemExit { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PySystemExit").finish_non_exhaustive() + } + } + + #[pyexception(with(Constructor, Initializer))] + impl PySystemExit { + #[pygetset] + fn code(&self) -> Option { + self.code.to_owned() + } + + #[pygetset(setter)] + fn set_code(&self, value: PySetterValue, vm: &VirtualMachine) { + let code = match value { + PySetterValue::Assign(v) => Some(v), + PySetterValue::Delete => None, + }; + self.code.swap_to_temporary_refs(code, vm); + } + } impl Initializer for PySystemExit { type Args = FuncArgs; fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { // Call BaseException_init first (handles args) - PyBaseException::slot_init(zelf, args, vm) - // Note: code is computed dynamically via system_exit_code getter - // so we don't need to set it here explicitly + let code = match args.args.len() { + 0 => vm.ctx.none(), + 1 => args.args[0].clone(), + _ => vm.ctx.new_tuple(args.args.clone()).into(), + }; + PyBaseException::slot_init(zelf.clone(), args, vm)?; + let exc: &Py = zelf.downcast_ref::().unwrap(); + exc.code.swap_to_temporary_refs(Some(code), vm); + Ok(()) } fn init(_zelf: PyRef, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { @@ -1649,6 +1717,18 @@ pub(super) mod types { } } + impl Constructor for PySystemExit { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let base_exception = PyBaseException::new(args.args, vm); + Ok(Self { + base: base_exception, + code: None.into(), + }) + } + } + #[pyexception(name, base = PyBaseException, ctx = "generator_exit", impl)] #[derive(Debug)] #[repr(transparent)] @@ -1664,18 +1744,57 @@ pub(super) mod types { #[repr(transparent)] pub struct PyException(PyBaseException); - #[pyexception(name, base = PyException, ctx = "stop_iteration")] - #[derive(Debug)] - #[repr(transparent)] - pub struct PyStopIteration(PyException); + #[pyexception(name, base = PyException, ctx = "stop_iteration", traverse = "manual")] + #[repr(C)] + pub struct PyStopIteration { + base: PyException, + value: PyAtomicRef>, + } - #[pyexception(with(Initializer))] - impl PyStopIteration {} + impl crate::class::PySubclass for PyStopIteration { + type Base = PyException; + fn as_base(&self) -> &Self::Base { + &self.base + } + } + + impl core::fmt::Debug for PyStopIteration { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PyStopIteration").finish_non_exhaustive() + } + } + + unsafe impl Traverse for PyStopIteration { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.base.0.traverse(tracer_fn); + if let Some(obj) = self.value.deref() { + tracer_fn(obj); + } + } + } + + impl Constructor for PyStopIteration { + type Args = FuncArgs; + + fn py_new(_cls: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + let base_exception = PyBaseException::new(args.args, vm); + Ok(Self { + base: PyException(base_exception), + value: None.into(), + }) + } + } impl Initializer for PyStopIteration { type Args = FuncArgs; fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { - zelf.set_attr("value", vm.unwrap_or_none(args.args.first().cloned()), vm)?; + let value = match args.args.len() { + 0 => vm.ctx.none(), + _ => args.args[0].clone(), + }; + PyBaseException::slot_init(zelf.clone(), args, vm)?; + let exc: &Py = zelf.downcast_ref::().unwrap(); + exc.value.swap_to_temporary_refs(Some(value), vm); Ok(()) } @@ -1684,6 +1803,23 @@ pub(super) mod types { } } + #[pyexception(with(Constructor, Initializer))] + impl PyStopIteration { + #[pygetset] + fn value(&self) -> Option { + self.value.to_owned() + } + + #[pygetset(setter)] + fn set_value(&self, setter_value: PySetterValue, vm: &VirtualMachine) { + let value = match setter_value { + PySetterValue::Assign(v) => Some(v), + PySetterValue::Delete => None, + }; + self.value.swap_to_temporary_refs(value, vm); + } + } + #[pyexception(name, base = PyException, ctx = "stop_async_iteration", impl)] #[derive(Debug)] #[repr(transparent)] @@ -1775,10 +1911,11 @@ pub(super) mod types { #[pymethod] fn __reduce__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef { let obj = exc.as_object().to_owned(); - let mut result: Vec = vec![ - obj.class().to_owned().into(), - vm.new_tuple((exc.get_arg(0).unwrap(),)).into(), - ]; + let args: PyObjectRef = match exc.get_arg(0) { + Some(arg) => vm.new_tuple((arg,)).into(), + None => exc.args().into(), + }; + let mut result: Vec = vec![obj.class().to_owned().into(), args]; if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) { result.push(dict.into()); @@ -1805,10 +1942,21 @@ pub(super) mod types { ))); } - let dict = zelf.dict().unwrap(); - dict.set_item("name", vm.unwrap_or_none(name), vm)?; - dict.set_item("path", vm.unwrap_or_none(path), vm)?; - dict.set_item("name_from", vm.unwrap_or_none(name_from), vm)?; + if let Some(name) = name { + zelf.set_attr("name", name, vm)?; + } else if let Some(dict) = zelf.dict() { + dict.del_item("name", vm).ok(); + } + if let Some(path) = path { + zelf.set_attr("path", path, vm)?; + } else if let Some(dict) = zelf.dict() { + dict.del_item("path", vm).ok(); + } + if let Some(name_from) = name_from { + zelf.set_attr("name_from", name_from, vm)?; + } else if let Some(dict) = zelf.dict() { + dict.del_item("name_from", vm).ok(); + } PyBaseException::slot_init(zelf, args, vm) } @@ -1902,7 +2050,7 @@ pub(super) mod types { #[repr(transparent)] pub struct PyUnboundLocalError(PyNameError); - #[pyexception(name, base = PyException, ctx = "os_error")] + #[pyexception(name, base = PyException, ctx = "os_error", traverse = "manual")] #[repr(C)] pub struct PyOSError { base: PyException, @@ -1932,7 +2080,10 @@ pub(super) mod types { unsafe impl Traverse for PyOSError { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - self.base.try_traverse(tracer_fn); + // `self.base` is a `PyException` newtype whose `MaybeTraverse` is a + // no-op; reach the underlying `PyBaseException` so its traceback, + // cause, context and args are visited by the collector. + self.base.0.traverse(tracer_fn); if let Some(obj) = self.errno.deref() { tracer_fn(obj); } @@ -2006,14 +2157,19 @@ pub(super) mod types { .downcast_ref::() .and_then(|errno| errno.try_to_primitive::(vm).ok()) .and_then(|errno| super::errno_to_exc_type(errno, vm)) - .and_then(|typ| vm.invoke_exception(typ.to_owned(), args_vec).ok()) + .and_then(|typ| { + vm.new_payload_exception::(typ.to_owned(), args_vec.into()) + .ok() + }) { return error.to_pyresult(vm); } } } let payload = Self::py_new(&cls, args, vm)?; - payload.into_ref_with_type(vm, cls).map(Into::into) + payload + .into_ref_with_type_lazy_dict(vm, cls) + .map(Into::into) } } @@ -2480,12 +2636,18 @@ pub(super) mod types { let maybe_lineno = zelf .as_object() .get_attr("lineno", vm) - .and_then(|obj| obj.str_utf8(vm)) - .ok(); - let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| { - obj.str(vm) - .unwrap_or_else(|_| vm.ctx.new_str("")) - }); + .ok() + .filter(|obj| !vm.is_none(obj)) + .and_then(|obj| obj.str_utf8(vm).ok()); + let maybe_filename = zelf + .as_object() + .get_attr("filename", vm) + .ok() + .filter(|obj| !vm.is_none(obj)) + .map(|obj| { + obj.str(vm) + .unwrap_or_else(|_| vm.ctx.new_str("")) + }); let msg = match zelf.as_object().get_attr("msg", vm) { Ok(obj) => obj @@ -2643,7 +2805,7 @@ pub(super) mod types { Ok(vm.ctx.new_str(if start < object.len() && end <= object.len() && end == start + 1 { let b = object.borrow_buf()[start]; format!( - "'{encoding}' codec can't decode byte {b:#02x} in position {start}: {reason}" + "'{encoding}' codec can't decode byte {b:#04x} in position {start}: {reason}" ) } else { format!( diff --git a/crates/vm/src/format.rs b/crates/vm/src/format.rs index 7e18dd75d2f..657601e1470 100644 --- a/crates/vm/src/format.rs +++ b/crates/vm/src/format.rs @@ -48,6 +48,7 @@ impl IntoPyException for FormatSpecError { vm.new_value_error("Too many decimal digits in format string") } Self::PrecisionTooBig => vm.new_value_error("Precision too big"), + Self::PrecisionMissing => vm.new_value_error("Format specifier missing precision"), Self::InvalidFormatSpecifier => vm.new_value_error("Invalid format specifier"), Self::UnspecifiedFormat(c1, c2) => { let msg = format!("Cannot specify '{c1}' with '{c2}'."); @@ -76,6 +77,15 @@ impl IntoPyException for FormatSpecError { Self::AlignmentFlag => { vm.new_value_error("'=' alignment flag is not allowed in complex format specifier") } + Self::NegativeZeroCoercionNotAllowed(type_name) => { + let msg = format!( + "Negative zero coercion (z) not allowed in {type_name} format specifier" + ); + vm.new_value_error(msg) + } + Self::StringAlignmentFlag => { + vm.new_value_error("'=' alignment not allowed in string format specifier") + } Self::NotImplemented(c, s) => { let msg = format!("Format code '{c}' for object of type '{s}' not implemented yet"); vm.new_value_error(msg) diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 7050d851743..d102b9a6d8e 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -12,10 +12,7 @@ use crate::{ builtin_func::PyNativeFunction, descriptor::{MemberGetter, PyMemberDescriptor, PyMethodDescriptor}, frame::stack_analysis, - function::{ - PyBoundMethod, PyCell, PyCellRef, PyFunction, datastack_frame_size_bytes_for_code, - vectorcall_function, - }, + function::{PyBoundMethod, PyCell, PyCellRef, PyFunction, vectorcall_function}, list::PyListIterator, range::PyRangeIterator, tuple::{PyTuple, PyTupleIterator, PyTupleRef}, @@ -39,8 +36,8 @@ use crate::{ use alloc::fmt; use bstr::ByteSlice; use core::cell::UnsafeCell; +use core::ptr::NonNull; use core::sync::atomic; -use core::sync::atomic::AtomicPtr; use core::sync::atomic::Ordering::{Acquire, Relaxed}; use itertools::Itertools; use malachite_bigint::BigInt; @@ -50,9 +47,154 @@ use rustpython_common::{ lock::{OnceCell, PyMutex}, wtf8::{Wtf8, Wtf8Buf, wtf8_concat}, }; -use rustpython_compiler_core::SourceLocation; +use rustpython_compiler_core::{OneIndexed, SourceLocation}; + +pub type FrameObjectRef = PyRef; + +// -- Frame chain utilities -- +// The frame chain is a singly-linked list of `*const InterpreterFrame` stored +// as `usize` values in `InterpreterFrame.previous` and the TLS +// `CURRENT_FRAME`. Null (0) marks the end. +// Each InterpreterFrame has a `materialized` pointer that is non-null when +// a FrameObject wraps it (always the case today; stack-allocated frames will +// leave it null until observed). + +/// Recover an owned reference to the FrameObject that wraps the given +/// InterpreterFrame, or `None` if null or not materialized. +/// +/// # Safety +/// A non-null `iframe` must reference a live InterpreterFrame on the current +/// thread's execution chain. +unsafe fn owned_chain_frame(iframe: *const InterpreterFrame) -> Option { + if iframe.is_null() { + return None; + } + let iframe_ref = unsafe { &*iframe }; + let fo = iframe_ref.frame_obj()?; + Some(fo.to_owned()) +} + +/// The current thread's topmost frame object, if any. +#[must_use] +pub fn current_thread_frame() -> Option { + let ptr = crate::vm::thread::get_current_frame(); + unsafe { owned_chain_frame(ptr) } +} + +/// Get the current thread's topmost InterpreterFrame pointer. +#[must_use] +pub fn current_thread_iframe() -> *const InterpreterFrame { + crate::vm::thread::get_current_frame() +} + +/// The current thread's topmost frame object, materializing if necessary. +/// Unlike `current_thread_frame()`, this always returns `Some` if there is +/// an active frame, even if it's stack-allocated and hasn't been observed yet. +/// Uses `vm.current_frame` Cell for fast lookup (no TLS). +#[must_use] +pub fn current_thread_frame_materialize(vm: &VirtualMachine) -> Option { + let ptr = crate::vm::thread::get_current_frame(); + if ptr.is_null() { + return None; + } + let iframe = unsafe { &*ptr }; + Some(iframe.materialize(vm).to_owned()) +} + +/// Read the globals dict from the topmost frame on this thread's chain. +/// Returns `None` if the chain is empty. +#[must_use] +pub fn current_globals() -> Option { + let ptr = crate::vm::thread::get_current_frame(); + if ptr.is_null() { + return None; + } + Some(unsafe { (*ptr).globals().to_owned() }) +} + +/// Read the code object from the topmost frame on this thread's chain. +/// Returns `None` if the chain is empty. +#[must_use] +pub fn current_code() -> Option> { + let ptr = crate::vm::thread::get_current_frame(); + if ptr.is_null() { + return None; + } + Some(unsafe { (*ptr).code().to_owned() }) +} + +/// Read the builtins object from the topmost frame on this thread's chain. +#[must_use] +pub fn current_builtins() -> Option { + let ptr = crate::vm::thread::get_current_frame(); + if ptr.is_null() { + return None; + } + Some(unsafe { (*ptr).builtins().to_owned() }) +} + +/// The frame `offset` positions below the current thread's top frame (offset 0 +/// is the top), or `None` if the stack is not that deep. +/// Materializes the FrameObject on demand for stack-allocated frames. +#[must_use] +pub fn frame_at_offset(offset: usize, vm: &VirtualMachine) -> Option { + let mut cur = crate::vm::thread::get_current_frame(); + let mut remaining = offset; + while !cur.is_null() { + if remaining == 0 { + let iframe = unsafe { &*cur }; + return Some(iframe.materialize(vm).to_owned()); + } + remaining -= 1; + cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame }; + } + None +} + +/// If a FrameObject wrapping `target` InterpreterFrame is on the current +/// thread's chain, return an owned reference to it; otherwise `None`. +#[must_use] +pub fn find_owned_chain_frame_by_iframe(target: *const InterpreterFrame) -> Option { + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + if core::ptr::eq(cur, target) { + return unsafe { owned_chain_frame(cur) }; + } + cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame }; + } + None +} + +/// If `target` FrameObject is on the current thread's chain, return an +/// owned reference to it; otherwise `None`. Presence on the chain proves liveness. +#[must_use] +pub fn find_owned_chain_frame(target: *const FrameObject) -> Option { + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let iframe_ref = unsafe { &*cur }; + if let Some(fo) = iframe_ref.frame_obj() { + let fo_payload: *const FrameObject = &**fo; + if core::ptr::eq(fo_payload, target) { + return Some(fo.to_owned()); + } + } + cur = iframe_ref.previous.load(Relaxed) as *const InterpreterFrame; + } + None +} -pub type FrameRef = PyRef; +/// Invoke `f` for each frame on the current thread's chain, from the +/// topmost frame down to the bottom. +pub fn for_each_current_frame(mut f: impl FnMut(&Py)) { + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let iframe_ref = unsafe { &*cur }; + if let Some(fo) = iframe_ref.frame_obj() { + f(fo); + } + cur = iframe_ref.previous.load(Relaxed) as *const InterpreterFrame; + } +} /// The reason why we might be unwinding a block. /// This could be return of function, exception being @@ -94,7 +236,7 @@ impl FrameOwner { /// Lock-free mutable storage for frame-internal data. /// /// # Safety -/// Frame execution is single-threaded: only one thread at a time executes +/// FrameObject execution is single-threaded: only one thread at a time executes /// a given frame (enforced by the owner field and generator running flag). /// External readers (e.g. `f_locals`) are on the same thread as execution /// (trace callback) or the frame is not executing. @@ -111,9 +253,15 @@ impl FrameUnsafeCell { unsafe fn get(&self) -> *mut T { self.0.get() } + + /// Safe exclusive access through `&mut self`. + #[inline(always)] + fn get_mut(&mut self) -> &mut T { + self.0.get_mut() + } } -// SAFETY: Frame execution is single-threaded. See FrameUnsafeCell doc. +// SAFETY: FrameObject execution is single-threaded. See FrameUnsafeCell doc. #[cfg(feature = "threading")] unsafe impl Send for FrameUnsafeCell {} #[cfg(feature = "threading")] @@ -147,7 +295,7 @@ enum LocalsPlusData { } // SAFETY: DataStack variant points to thread-local DataStack memory. -// Frame execution is single-threaded (enforced by owner field). +// FrameObject execution is single-threaded (enforced by owner field). #[cfg(feature = "threading")] unsafe impl Send for LocalsPlusData {} #[cfg(feature = "threading")] @@ -175,9 +323,14 @@ impl LocalsPlus { /// Create a new LocalsPlus backed by the thread data stack. /// All slots are zero-initialized. /// - /// The caller must call `materialize_localsplus()` when the frame finishes - /// to migrate data to the heap, then `datastack_pop()` to free the memory. - fn new_on_datastack(nlocalsplus: usize, stacksize: usize, vm: &VirtualMachine) -> Self { + /// When the frame finishes, the caller must migrate data to the heap with + /// `materialize_localsplus()` (or drop it in place with + /// `release_localsplus()`), then `datastack_pop()` to free the memory. + pub(crate) fn new_on_datastack( + nlocalsplus: usize, + stacksize: usize, + vm: &VirtualMachine, + ) -> Self { let capacity = nlocalsplus .checked_add(stacksize) .expect("LocalsPlus capacity overflow"); @@ -211,6 +364,46 @@ impl LocalsPlus { } } + /// Drop all contained values and detach the data stack backing without + /// copying to the heap, leaving an empty heap-backed husk. + /// Returns the data stack base pointer for `DataStack::pop()`. + /// Returns `None` if already heap-backed. + /// + /// Only valid when the values can never be observed again (the enclosing + /// frame is uniquely referenced): the locals are gone afterwards. + pub(crate) fn release_datastack(&mut self) -> Option<*mut u8> { + let LocalsPlusData::DataStack { ptr, .. } = &self.data else { + return None; + }; + let base = *ptr as *mut u8; + // Drop values while the backing store is still valid. Value drops may + // run `__del__`, which can push nested data stack frames above `base`; + // those are popped before the caller pops `base` (LIFO preserved). + self.drop_values(); + self.data = LocalsPlusData::Heap(Box::default()); + // Keep the accessors consistent with the empty backing store. + // stack_top is already 0 after drop_values(). + self.nlocalsplus = 0; + Some(base) + } + + /// Update fastlocals in `self` from `src`. For each slot, drops the old + /// value and clones the new one. `self` must be heap-backed. + /// + /// # Safety + /// Both `self` and `src` must have valid backing storage, and the caller + /// must ensure no concurrent mutable access. + pub(crate) unsafe fn sync_fastlocals_from(&mut self, src: &Self) { + let n = core::cmp::min(self.nlocalsplus as usize, src.nlocalsplus as usize); + let dst = self.fastlocals_mut(); + let source = src.fastlocals(); + for i in 0..n { + let old = dst[i].take(); + dst[i].clone_from(&source[i]); + drop(old); + } + } + /// Drop all contained values without freeing the backing storage. fn drop_values(&mut self) { self.stack_clear(); @@ -251,6 +444,12 @@ impl LocalsPlus { } } + /// Whether the backing storage still lives on the thread data stack (a + /// running call frame that has not been materialized onto the heap). + fn is_datastack_backed(&self) -> bool { + matches!(self.data, LocalsPlusData::DataStack { .. }) + } + /// Stack capacity (max stack depth). #[inline(always)] fn stack_capacity(&self) -> usize { @@ -261,7 +460,7 @@ impl LocalsPlus { /// Immutable access to fastlocals as `Option` slice. #[inline(always)] - fn fastlocals(&self) -> &[Option] { + pub(crate) fn fastlocals(&self) -> &[Option] { let data = self.data_as_slice(); let ptr = data.as_ptr() as *const Option; unsafe { core::slice::from_raw_parts(ptr, self.nlocalsplus as usize) } @@ -269,7 +468,7 @@ impl LocalsPlus { /// Mutable access to fastlocals as `Option` slice. #[inline(always)] - fn fastlocals_mut(&mut self) -> &mut [Option] { + pub(crate) fn fastlocals_mut(&mut self) -> &mut [Option] { let nlocalsplus = self.nlocalsplus as usize; let data = self.data_as_mut_slice(); let ptr = data.as_mut_ptr() as *mut Option; @@ -318,6 +517,13 @@ impl LocalsPlus { Ok(()) } + /// Push a PyObjectRef onto the evaluation stack. + /// Panics on overflow. + pub(crate) fn push_stack(&mut self, value: PyObjectRef) { + self.stack_try_push(Some(PyStackRef::new_owned(value))) + .expect("stack overflow in push_stack"); + } + /// Pop a value from the evaluation stack. #[inline(always)] fn stack_pop(&mut self) -> Option { @@ -329,6 +535,19 @@ impl LocalsPlus { unsafe { core::mem::transmute::>(raw) } } + /// Give every borrowed stack ref its own reference. + /// + /// A borrowed ref is only sound while whatever it points at is guaranteed + /// to outlive it, which stops holding where the frame itself outlives the + /// running block — at a yield, where the stack is saved with the frame. + fn promote_stack(&mut self) { + for idx in 0..self.stack_top as usize { + if let Some(stack_ref) = self.stack_index_mut(idx) { + stack_ref.promote(); + } + } + } + /// Immutable view of the active stack as `Option` slice. #[inline(always)] fn stack_as_slice(&self) -> &[Option] { @@ -494,7 +713,7 @@ pub struct FrameLocals { impl FrameLocals { /// Create with an already-initialized locals mapping (non-NEWLOCALS frames). - fn with_locals(locals: ArgMapping) -> Self { + pub(crate) fn with_locals(locals: ArgMapping) -> Self { let cell = OnceCell::new(); let _ = cell.set(locals); Self { inner: cell } @@ -502,7 +721,7 @@ impl FrameLocals { /// Create an empty lazy locals (for NEWLOCALS frames). /// The dict will be created on first access. - fn lazy() -> Self { + pub(crate) fn lazy() -> Self { Self { inner: OnceCell::new(), } @@ -566,131 +785,126 @@ unsafe impl Traverse for FrameLocals { } } +/// Cold fields of InterpreterFrame that are only accessed during tracing, +/// debugging, frame inspection, or GC. Lazily allocated on first access +/// to keep the hot InterpreterFrame small. +pub(crate) struct FrameColdData { + pub trace: PyMutex>, + pub trace_lines: PyMutex, + pub trace_opcodes: PyMutex, + pub temporary_refs: PyMutex>, + pub f_locals_hidden_overlay: PyMutex>, + pub f_extra_locals: PyMutex>, + pub escaped: atomic::AtomicBool, + pub retained_back: PyMutex>, + pub pending_stack_pops: PyAtomic, + pub pending_unwind_from_stack: PyAtomic, + /// Thread that is still running the frame this one was materialized from, + /// or 0 once that frame has returned (and for every frame object that was + /// not materialized from a running frame). Only a thread id, never a + /// pointer: reading it can never chase freed memory, so it stays usable + /// as the gate for frames that belong to another thread. + pub attached_tid: atomic::AtomicU64, +} + +impl Default for FrameColdData { + fn default() -> Self { + Self { + trace: PyMutex::new(None), + trace_lines: PyMutex::new(true), + trace_opcodes: PyMutex::new(false), + temporary_refs: PyMutex::new(Vec::new()), + f_locals_hidden_overlay: PyMutex::new(None), + f_extra_locals: PyMutex::new(None), + escaped: atomic::AtomicBool::new(false), + retained_back: PyMutex::new(None), + pending_stack_pops: Default::default(), + pending_unwind_from_stack: Default::default(), + attached_tid: atomic::AtomicU64::new(0), + } + } +} + /// Lightweight execution frame. Not a PyObject. /// Analogous to CPython's `_PyInterpreterFrame`. /// -/// Currently always embedded inside a `Frame` PyObject via `FrameUnsafeCell`. -/// In future PRs this will be usable independently for normal function calls -/// (allocated on the Rust stack + DataStack), eliminating PyObject overhead. +/// The four "identity" fields (`code`, `globals`, `builtins`, `func_obj`) +/// are borrowed raw pointers — refcounts are maintained by the owner +/// (FrameObject's owned fields, or the PyFunction on the caller's stack +/// in the DataStack path). +#[repr(C)] pub struct InterpreterFrame { - pub code: PyRef, - pub func_obj: Option, + // Borrowed pointers — owned by FrameObject or by PyFunction on caller's stack. + pub(crate) code: *const Py, + pub(crate) func_obj: *const PyObject, // nullable + pub(crate) globals: *const Py, + pub(crate) builtins: *const PyObject, /// Unified storage for local variables and evaluation stack. pub(crate) localsplus: LocalsPlus, pub locals: FrameLocals, - pub globals: PyDictRef, - pub builtins: PyObjectRef, /// index of last instruction ran pub lasti: PyAtomic, - /// tracer function for this frame (usually is None) - pub trace: PyMutex, /// Previous line number for LINE event suppression. - pub(crate) prev_line: u32, + pub(crate) prev_line: core::cell::Cell, - // member - pub trace_lines: PyMutex, - pub trace_opcodes: PyMutex, - pub temporary_refs: PyMutex>, /// Back-reference to owning generator/coroutine/async generator. - /// Borrowed reference (not ref-counted) to avoid Generator↔Frame cycle. + /// Borrowed reference (not ref-counted) to avoid Generator↔FrameObject cycle. /// Cleared by the generator's Drop impl. pub generator: PyAtomicBorrow, - /// Previous frame in the call chain for signal-safe traceback walking. - /// Mirrors `_PyInterpreterFrame.previous`. - pub(crate) previous: AtomicPtr, + /// Linked-list pointer to the previous frame in the call chain. + /// Stores a `*const FrameObject` as `usize`. + pub(crate) previous: PyAtomic, /// Who owns this frame. Mirrors `_PyInterpreterFrame.owner`. /// Used by `frame.clear()` to reject clearing an executing frame, /// even when called from a different thread. pub(crate) owner: atomic::AtomicI8, - /// Set when f_locals is accessed. Cleared after locals_to_fast() sync. - pub(crate) locals_dirty: atomic::AtomicBool, - /// Persistent overlay for `frame.f_locals` when hidden locals need a - /// snapshot separate from the backing locals mapping. - pub(crate) f_locals_hidden_overlay: PyMutex>, - /// Number of stack entries to pop after set_f_lineno returns to the - /// execution loop. set_f_lineno cannot pop directly because the - /// execution loop holds the state mutex. - pub(crate) pending_stack_pops: PyAtomic, - /// The encoded stack state that set_f_lineno wants to unwind *from*. - /// Used together with `pending_stack_pops` to identify Except entries - /// that need special exception-state handling. - pub(crate) pending_unwind_from_stack: PyAtomic, + /// Base pointer of the datastack allocation when this frame and its + /// localsplus are bump-allocated together. Null for heap-backed frames. + pub(crate) datastack_base: *mut u8, + /// Pointer to the owning `Py`, or null for stack-allocated + /// frames that have not been materialized yet. + /// Stored as `usize` for `PyAtomic` compatibility. + pub(crate) materialized: PyAtomic, + + /// Lazily-allocated cold data (tracing, debugging, frame inspection). + /// Not allocated until first access via `cold()`. + pub(crate) cold: OnceCell>, } -/// Python-visible frame object. Currently always wraps an `InterpreterFrame`. -/// Analogous to CPython's `PyFrameObject`. -#[pyclass(module = false, name = "frame", traverse = "manual")] -pub struct Frame { - pub(crate) iframe: FrameUnsafeCell, -} +// Raw pointers make InterpreterFrame !Send+!Sync by default. +// SAFETY: The pointers reference heap-resident PyObjects whose lifetimes +// are managed by the owning FrameObject (or PyFunction). Frame execution +// is single-threaded (enforced by the owner field). +#[cfg(feature = "threading")] +unsafe impl Send for InterpreterFrame {} +#[cfg(feature = "threading")] +unsafe impl Sync for InterpreterFrame {} -impl core::ops::Deref for Frame { - type Target = InterpreterFrame; - /// Transparent access to InterpreterFrame fields. +impl InterpreterFrame { + /// Construct a new InterpreterFrame with raw pointers set from the given references. /// - /// # Safety argument - /// Immutable fields (code, globals, builtins, func_obj, locals) are safe - /// to access at any time. Atomic/mutex fields (lasti, trace, owner, etc.) - /// provide their own synchronization. Mutable fields (localsplus, prev_line) - /// are only mutated during single-threaded execution via `with_exec`. + /// The caller must ensure that the pointed-to objects outlive this frame. + /// For FrameObject-owned frames, `init_iframe_ptrs` patches the pointers + /// after heap allocation; the pointers passed here are then overwritten. + /// For stack-allocated frames (future), the pointers remain valid for the + /// frame's lifetime on the native stack. + #[allow(clippy::too_many_arguments)] #[inline(always)] - fn deref(&self) -> &InterpreterFrame { - unsafe { &*self.iframe.get() } - } -} - -impl PyPayload for Frame { - #[inline] - fn class(ctx: &Context) -> &'static Py { - ctx.types.frame_type - } -} - -unsafe impl Traverse for Frame { - fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - // SAFETY: GC traversal does not run concurrently with frame execution. - let iframe = unsafe { &*self.iframe.get() }; - iframe.code.traverse(tracer_fn); - iframe.func_obj.traverse(tracer_fn); - iframe.localsplus.traverse(tracer_fn); - iframe.locals.traverse(tracer_fn); - iframe.globals.traverse(tracer_fn); - iframe.builtins.traverse(tracer_fn); - iframe.trace.traverse(tracer_fn); - iframe.temporary_refs.traverse(tracer_fn); - iframe.f_locals_hidden_overlay.traverse(tracer_fn); - } -} - -// Running a frame can result in one of the below: -pub enum ExecutionResult { - Return(PyObjectRef), - Yield(PyObjectRef), -} - -/// A valid execution result, or an exception -type FrameResult = PyResult>; - -impl Frame { pub(crate) fn new( - code: PyRef, - scope: Scope, - builtins: PyObjectRef, + code: &Py, + globals: &Py, + builtins: &PyObject, + func_obj: Option<&PyObject>, + localsplus: LocalsPlus, + locals: FrameLocals, closure: &[PyCellRef], - func_obj: Option, - use_datastack: bool, - vm: &VirtualMachine, + owner: FrameOwner, ) -> Self { + let mut localsplus = localsplus; let nlocalsplus = code.localspluskinds.len(); - let max_stackdepth = code.max_stackdepth as usize; - let mut localsplus = if use_datastack { - LocalsPlus::new_on_datastack(nlocalsplus, max_stackdepth, vm) - } else { - LocalsPlus::new(nlocalsplus, max_stackdepth) - }; // Pre-copy closure cells into free var slots so that locals() works // even before COPY_FREE_VARS runs (e.g. coroutine before first send). @@ -719,185 +933,898 @@ impl Frame { 0 }; - let iframe = InterpreterFrame { - localsplus, - locals: match scope.locals { - Some(locals) => FrameLocals::with_locals(locals), - None if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) => FrameLocals::lazy(), - None => { - FrameLocals::with_locals(ArgMapping::from_dict_exact(scope.globals.clone())) - } + Self { + code: code as *const Py, + func_obj: match func_obj { + Some(obj) => obj as *const PyObject, + None => core::ptr::null(), }, - globals: scope.globals, - builtins, - code, - func_obj, + globals: globals as *const Py, + builtins: builtins as *const PyObject, + localsplus, + locals, lasti: Radium::new(0), - prev_line, - trace: PyMutex::new(vm.ctx.none()), - trace_lines: PyMutex::new(true), - trace_opcodes: PyMutex::new(false), - temporary_refs: PyMutex::new(vec![]), + prev_line: core::cell::Cell::new(prev_line), generator: PyAtomicBorrow::new(), - previous: AtomicPtr::new(core::ptr::null_mut()), - owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8), - locals_dirty: atomic::AtomicBool::new(false), - f_locals_hidden_overlay: PyMutex::new(None), - pending_stack_pops: Default::default(), - pending_unwind_from_stack: Default::default(), - }; - Self { - iframe: FrameUnsafeCell::new(iframe), + previous: Radium::new(0), + owner: atomic::AtomicI8::new(owner as i8), + datastack_base: core::ptr::null_mut(), + materialized: Radium::new(0), + cold: OnceCell::new(), } } - /// Access fastlocals immutably. + /// Allocate an InterpreterFrame and its LocalsPlus data together on the + /// thread data stack in a single bump allocation. /// - /// # Safety - /// Caller must ensure no concurrent mutable access (frame not executing, - /// or called from the same thread during trace callback). - #[inline(always)] - pub unsafe fn fastlocals(&self) -> &[Option] { - unsafe { (*self.iframe.get()).localsplus.fastlocals() } - } - - /// Access fastlocals mutably. + /// Layout: `[InterpreterFrame | localsplus usize×capacity]` /// - /// # Safety - /// Caller must ensure exclusive access (frame not executing). + /// Returns a mutable reference whose lifetime is bounded by the data + /// stack's LIFO discipline. The caller must call + /// `release_datastack_frame()` (unsafe) when done, then + /// `vm.datastack_pop_frame(base, size)`. The reference must not be used after + /// `release_datastack_frame` returns. + #[allow(clippy::too_many_arguments)] #[inline(always)] - #[allow(clippy::mut_from_ref)] - pub unsafe fn fastlocals_mut(&self) -> &mut [Option] { - unsafe { (*self.iframe.get()).localsplus.fastlocals_mut() } - } + pub(crate) fn new_on_datastack<'a>( + code: &Py, + globals: &Py, + builtins: &PyObject, + func_obj: Option<&PyObject>, + locals: FrameLocals, + closure: &[PyCellRef], + vm: &VirtualMachine, + ) -> &'a mut Self { + let nlocalsplus = code.localspluskinds.len(); + let stacksize = code.max_stackdepth as usize; + let capacity = nlocalsplus + .checked_add(stacksize) + .expect("LocalsPlus capacity overflow"); - /// Migrate data-stack-backed storage to the heap, preserving all values, - /// and return the data stack base pointer for `DataStack::pop()`. - /// Returns `None` if already heap-backed. - /// - /// # Safety - /// Caller must ensure the frame is not executing and the returned - /// pointer is passed to `VirtualMachine::datastack_pop()`. - pub(crate) unsafe fn materialize_localsplus(&self) -> Option<*mut u8> { - unsafe { (*self.iframe.get()).localsplus.materialize_to_heap() } - } + let total_bytes = datastack_iframe_total_bytes(nlocalsplus, stacksize); + let (base, reused_cleared_frame) = vm.datastack_push_frame(total_bytes); - /// Clear evaluation stack and state-owned cell/free references. - /// For full local/cell cleanup, call `clear_locals_and_stack()`. - pub(crate) fn clear_stack_and_cells(&self) { - // SAFETY: Called when frame is not executing (generator closed). - // Cell refs in fastlocals[nlocals..] are cleared by clear_locals_and_stack(). - unsafe { - (*self.iframe.get()).localsplus.stack_clear(); - } - } + // InterpreterFrame lives at the start of the allocation. + let iframe_ptr = base as *mut Self; + // LocalsPlus data follows the InterpreterFrame, aligned to usize. + let localsplus_data_ptr = + unsafe { base.add(datastack_iframe_localsplus_offset()) } as *mut usize; - /// Clear locals and stack after generator/coroutine close. - /// Releases references held by the frame, matching _PyFrame_ClearLocals. - pub(crate) fn clear_locals_and_stack(&self) { - self.clear_stack_and_cells(); - // SAFETY: Frame is not executing (generator closed). - let fastlocals = unsafe { (*self.iframe.get()).localsplus.fastlocals_mut() }; - for slot in fastlocals.iter_mut() { - *slot = None; + if !reused_cleared_frame { + // Fresh or differently shaped storage may contain old frame data. + unsafe { core::ptr::write_bytes(localsplus_data_ptr, 0, capacity) }; } - self.f_locals_hidden_overlay.lock().take(); - } - - /// Get cell contents by localsplus index. - pub(crate) fn get_cell_contents(&self, localsplus_idx: usize) -> Option { - // SAFETY: Frame not executing; no concurrent mutation. - let fastlocals = unsafe { (*self.iframe.get()).localsplus.fastlocals() }; - fastlocals - .get(localsplus_idx) - .and_then(|slot| slot.as_ref()) - .and_then(|obj| obj.downcast_ref::()) - .and_then(|cell| cell.get()) - } - - /// Store a borrowed back-reference to the owning generator/coroutine. - /// The caller must ensure the generator outlives the frame. - pub fn set_generator(&self, generator: &PyObject) { - self.generator.store(generator); - self.owner - .store(FrameOwner::Generator as i8, atomic::Ordering::Release); - } - /// Clear the generator back-reference. Called when the generator is finalized. - pub fn clear_generator(&self) { - self.generator.clear(); - self.owner - .store(FrameOwner::FrameObject as i8, atomic::Ordering::Release); - } + let nlocalsplus_u32 = u32::try_from(nlocalsplus).expect("nlocalsplus exceeds u32"); + let localsplus = LocalsPlus { + data: LocalsPlusData::DataStack { + ptr: localsplus_data_ptr, + capacity, + }, + nlocalsplus: nlocalsplus_u32, + stack_top: 0, + }; - pub fn current_location(&self) -> SourceLocation { - self.code.locations[self.lasti() as usize - 1].0 - } + let mut iframe = Self::new( + code, + globals, + builtins, + func_obj, + localsplus, + locals, + closure, + FrameOwner::Thread, + ); + iframe.datastack_base = base; - /// Get the previous frame pointer for signal-safe traceback walking. - pub fn previous_frame(&self) -> *const Self { - self.previous.load(atomic::Ordering::Relaxed) + // Write the fully initialized InterpreterFrame into the datastack. + unsafe { + core::ptr::write(iframe_ptr, iframe); + &mut *iframe_ptr + } } - pub fn lasti(&self) -> u32 { + /// Release this datastack-allocated frame's resources and return the + /// base pointer for `vm.datastack_pop()`. + /// + /// Drops all localsplus values, runs destructors for all frame fields + /// (trace, temporary_refs, retained_back, etc.), and detaches the + /// backing store. + /// Returns `None` if this frame is not datastack-allocated. + /// + /// After this call, the InterpreterFrame at `self` is logically dead — + /// the caller must not use `self` again except to pass the returned + /// base to `vm.datastack_pop()`. + pub(crate) unsafe fn release_datastack_frame(&mut self) -> Option<(*mut u8, usize)> { + let base = self.datastack_base; + if base.is_null() { + return None; + } + let total_bytes = datastack_iframe_total_bytes( + self.localsplus.nlocalsplus as usize, + self.localsplus.stack_capacity(), + ); + self.datastack_base = core::ptr::null_mut(); + // Drop all localsplus values while the backing store is still valid. + self.localsplus.drop_values(); + // Detach from the data stack so further accesses see an empty frame. + self.localsplus.data = LocalsPlusData::Heap(Box::default()); + self.localsplus.nlocalsplus = 0; + // Drop remaining frame fields (trace, temporary_refs, retained_back, + // etc.) by running destructors in place. The localsplus is already + // empty/heap-backed, so this only drops non-localsplus fields. + // SAFETY: `self` points to valid, initialized memory on the data + // stack. After this call the memory is logically dead. + unsafe { core::ptr::drop_in_place(self) }; + Some((base, total_bytes)) + } + + /// Get the last instruction index. + #[inline(always)] + pub fn get_lasti(&self) -> u32 { self.lasti.load(Relaxed) } - pub fn set_lasti(&self, val: u32) { - self.lasti.store(val, Relaxed); - } - - pub(crate) fn pending_stack_pops(&self) -> u32 { - self.pending_stack_pops.load(Relaxed) + /// Get the previous InterpreterFrame in the chain, or null. + #[inline(always)] + pub fn previous(&self) -> *const Self { + self.previous.load(Relaxed) as *const Self } - pub(crate) fn set_pending_stack_pops(&self, val: u32) { - self.pending_stack_pops.store(val, Relaxed); + /// Get the owning FrameObject, if this frame has been materialized. + #[inline(always)] + pub(crate) fn frame_obj(&self) -> Option<&Py> { + let ptr = self.materialized.load(Relaxed); + if ptr == 0 { + None + } else { + Some(unsafe { &*(ptr as *const Py) }) + } } - pub(crate) fn pending_unwind_from_stack(&self) -> i64 { - self.pending_unwind_from_stack.load(Relaxed) + /// Materialize a FrameObject for this InterpreterFrame on demand. + /// If already materialized, returns the existing one. + /// The created FrameObject shares the raw pointers with this frame. + #[cold] + #[inline(never)] + pub(crate) fn materialize(&self, vm: &VirtualMachine) -> &Py { + if let Some(fo) = self.frame_obj() { + return fo; + } + self.materialize_slow(vm) } - pub(crate) fn set_pending_unwind_from_stack(&self, val: i64) { - self.pending_unwind_from_stack.store(val, Relaxed); + /// Take a standalone copy of this frame, values included, for a thread + /// that does not own it. + /// + /// Nothing links the copy back to this frame: the owning thread will not + /// find it at `exit_iframe` and so never writes into it once the world + /// restarts. That is the whole point — a linked copy is a buffer the owner + /// rewrites slot by slot while the reader clones out of it. + /// + /// # Safety + /// Caller must hold the world stopped, so the owning thread is parked and + /// its fast locals are not moving while they are read. + #[cfg(feature = "threading")] + #[cold] + #[inline(never)] + pub(crate) unsafe fn materialize_detached(&self, vm: &VirtualMachine) -> FrameObjectRef { + // Deliberately not `materialize_chain`: that hands back an existing + // linked copy when the owning thread has already made one. + let fo = self.materialize_slow_chain(vm); + unsafe { + fo.iframe_mut() + .localsplus + .sync_fastlocals_from(&self.localsplus) + }; + fo } - /// Sync locals dict back to fastlocals. Called before generator/coroutine resume - /// to apply any modifications made via f_locals. - pub fn locals_to_fast(&self, vm: &VirtualMachine) -> PyResult<()> { - if !self.locals_dirty.load(atomic::Ordering::Acquire) { - return Ok(()); - } - let code = &**self.code; - let overlay_locals = self - .has_active_hidden_locals() - .then(|| self.f_locals_hidden_overlay.lock().clone()) - .flatten() - .map(ArgMapping::from_dict_exact); - let locals_map = overlay_locals - .as_ref() - .map_or_else(|| self.locals.mapping(vm), ArgMapping::mapping); - // SAFETY: Called before generator resume; no concurrent access. - let fastlocals = unsafe { (*self.iframe.get()).localsplus.fastlocals_mut() }; - for (i, &varname) in code.varnames.iter().enumerate() { - if i >= fastlocals.len() { - break; - } - match locals_map.subscript(varname, vm) { - Ok(value) => fastlocals[i] = Some(value), - Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => {} - Err(e) => return Err(e), + /// Copy this frame and everything it was called from for a thread that + /// does not own them, linking `f_back` along the way, and return the copy + /// of this frame. The links are `retained_back`, so the chain keeps + /// resolving once the world restarts and the real frames return. + /// + /// # Safety + /// Caller must hold the world stopped, so the owning thread is parked and + /// the chain is not being popped while it is walked. + #[cfg(feature = "threading")] + #[cold] + #[inline(never)] + pub(crate) unsafe fn materialize_detached_chain(&self, vm: &VirtualMachine) -> FrameObjectRef { + let top = unsafe { self.materialize_detached(vm) }; + let mut child = top.clone(); + let mut cur = self.previous(); + while !cur.is_null() { + let caller = unsafe { &*cur }; + let caller_fo = unsafe { caller.materialize_detached(vm) }; + { + let mut guard = child.iframe().cold().retained_back.lock(); + if guard.is_none() { + *guard = Some(caller_fo.clone()); + } } + child = caller_fo; + cur = caller.previous(); } - self.locals_dirty.store(false, atomic::Ordering::Release); - Ok(()) + top } - fn has_active_hidden_locals(&self) -> bool { - use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE, CO_FAST_HIDDEN}; - let code = &**self.code; - let fastlocals = unsafe { (*self.iframe.get()).localsplus.fastlocals() }; + /// Create a lightweight FrameObject with empty localsplus, suitable for + /// f_back chain building (retained_back). Unlike `materialize`, this does + /// NOT store into `temporary_refs` or set the `materialized` pointer, so + /// the returned FrameObject is only kept alive by the caller's `PyRef`. + /// This prevents non-GC-tracked `temporary_refs` on a stack-allocated + /// iframe from defeating cycle collection. + #[cold] + #[inline(never)] + pub(crate) fn materialize_chain(&self, vm: &VirtualMachine) -> FrameObjectRef { + if let Some(fo) = self.frame_obj() { + return fo.to_owned(); + } + self.materialize_slow_chain(vm) + } + + #[cold] + fn materialize_slow(&self, vm: &VirtualMachine) -> &Py { + // Create a full FrameObject with its own InterpreterFrame copy. + // The FrameObject owns references to the same objects (code, globals, etc). + let code: PyRef = self.code().to_owned(); + let globals: PyDictRef = self.globals().to_owned(); + let builtins: PyObjectRef = self.builtins().to_owned(); + let func_obj: Option = self.func_obj().map(|o| o.to_owned()); + + // Empty localsplus, sized for the code object. While the source frame + // runs, every reader resolves it through `find_live_source_iframe`, and + // `exit_iframe` fills these slots from the live frame as it returns. + // Copying the values here instead would give each of them a second + // reference lasting as long as this FrameObject — a frame reached by + // one traceback entry would keep all of its locals alive. + let nlocalsplus = code.localspluskinds.len() as u32; + let localsplus = LocalsPlus { + data: LocalsPlusData::Heap(vec![0usize; nlocalsplus as usize].into_boxed_slice()), + nlocalsplus, + stack_top: 0, + }; + + // Copy the locals mapping if it exists. + let locals = match self.locals.get() { + Some(mapping) => FrameLocals::with_locals(mapping.clone()), + None => FrameLocals::lazy(), + }; + + // Build a fresh InterpreterFrame inside the FrameObject. + // Its raw pointers will be patched by init_iframe_ptrs. + let inner_iframe = Self { + code: core::ptr::null(), + func_obj: core::ptr::null(), + globals: core::ptr::null(), + builtins: core::ptr::null(), + localsplus, + locals, + lasti: Radium::new(self.lasti.load(Relaxed)), + prev_line: core::cell::Cell::new(self.prev_line.get()), + generator: PyAtomicBorrow::new(), + // Do NOT copy previous — it may point to stack-allocated frames + // that become dangling after their call returns. The f_back chain + // is resolved through the TLS CURRENT_FRAME chain instead. + previous: Radium::new(0), + // Always FrameObject-owned. If we copied Thread from the source + // iframe, frame.clear() would reject the frame with "cannot clear + // an executing frame"; `attached_tid` carries the "still running" + // half of that state instead, so the owner field does not have to. + owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8), + datastack_base: core::ptr::null_mut(), + materialized: Radium::new(0), + cold: OnceCell::from(Box::new(FrameColdData { + escaped: atomic::AtomicBool::new(true), + attached_tid: atomic::AtomicU64::new(current_thread_ident()), + ..FrameColdData::default() + })), + }; + + let frame_obj = FrameObject { + owned_code: Some(code), + owned_globals: Some(globals), + owned_builtins: Some(builtins), + owned_func_obj: func_obj, + iframe: FrameUnsafeCell::new(Some(inner_iframe)), + }; + let frame_ref = frame_obj.into_ref(&vm.ctx); + FrameObject::init_iframe_ptrs(&frame_ref); + // Set the inner iframe's materialized pointer to self + unsafe { + frame_ref + .iframe_mut() + .materialized + .store(&*frame_ref as *const Py as usize, Relaxed); + } + + // Store the materialized pointer on this stack frame. + let fo_ptr = &*frame_ref as *const Py as usize; + self.materialized.store(fo_ptr, Relaxed); + + // Keep the FrameObject alive by storing it in temporary_refs. + // GC tracking is deferred to `exit_iframe`, where the frame is no + // longer executing and temporary_refs is cleared — at that + // point the FrameObject is self-sustaining and GC can safely + // traverse and collect it. + self.cold() + .temporary_refs + .lock() + .push(frame_ref.clone().into()); + + // SAFETY: the pointer we stored above remains valid because + // temporary_refs holds a strong reference. + unsafe { &*(fo_ptr as *const Py) } + } + + /// Like `materialize_slow` but with empty localsplus to avoid extra + /// refcounts on local variables. Only suitable for f_back chain building. + /// Returns an owned `PyRef` without storing into `temporary_refs` or + /// setting the `materialized` pointer, so GC can still detect cycles. + #[cold] + fn materialize_slow_chain(&self, vm: &VirtualMachine) -> FrameObjectRef { + let code: PyRef = self.code().to_owned(); + let globals: PyDictRef = self.globals().to_owned(); + let builtins: PyObjectRef = self.builtins().to_owned(); + let func_obj: Option = self.func_obj().map(|o| o.to_owned()); + + // Empty localsplus — reads go through find_live_source_iframe. + let nlocalsplus = code.localspluskinds.len() as u32; + let localsplus = LocalsPlus { + data: LocalsPlusData::Heap(vec![0usize; nlocalsplus as usize].into_boxed_slice()), + nlocalsplus, + stack_top: 0, + }; + + let locals = match self.locals.get() { + Some(mapping) => FrameLocals::with_locals(mapping.clone()), + None => FrameLocals::lazy(), + }; + + let inner_iframe = Self { + code: core::ptr::null(), + func_obj: core::ptr::null(), + globals: core::ptr::null(), + builtins: core::ptr::null(), + localsplus, + locals, + lasti: Radium::new(self.lasti.load(Relaxed)), + prev_line: core::cell::Cell::new(self.prev_line.get()), + generator: PyAtomicBorrow::new(), + previous: Radium::new(0), + owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8), + datastack_base: core::ptr::null_mut(), + materialized: Radium::new(0), + cold: OnceCell::from(Box::new(FrameColdData { + escaped: atomic::AtomicBool::new(true), + ..FrameColdData::default() + })), + }; + + let frame_obj = FrameObject { + owned_code: Some(code), + owned_globals: Some(globals), + owned_builtins: Some(builtins), + owned_func_obj: func_obj, + iframe: FrameUnsafeCell::new(Some(inner_iframe)), + }; + let frame_ref = frame_obj.into_ref(&vm.ctx); + FrameObject::init_iframe_ptrs(&frame_ref); + + frame_ref + } + + /// Borrowed code object. + #[inline(always)] + pub fn code(&self) -> &Py { + unsafe { &*self.code } + } + + /// Borrowed globals dict. + #[inline(always)] + pub fn globals(&self) -> &Py { + unsafe { &*self.globals } + } + + /// Borrowed builtins object. + #[inline(always)] + pub fn builtins(&self) -> &PyObject { + unsafe { &*self.builtins } + } + + /// Borrowed function object, or None if not set. + #[inline(always)] + pub fn func_obj(&self) -> Option<&PyObject> { + if self.func_obj.is_null() { + None + } else { + Some(unsafe { &*self.func_obj }) + } + } + + /// Access the lazily-allocated cold data, allocating on first use. + #[inline] + pub(crate) fn cold(&self) -> &FrameColdData { + self.cold.get_or_init(|| Box::new(FrameColdData::default())) + } + + /// Access cold data without allocating. Returns `None` if cold data + /// has not been allocated yet. + #[inline] + pub(crate) fn cold_opt(&self) -> Option<&FrameColdData> { + self.cold.get().map(|b| &**b) + } + + /// Thread still running the frame this one was materialized from, or 0. + #[inline] + pub(crate) fn attached_tid(&self) -> u64 { + self.cold_opt() + .map_or(0, |c| c.attached_tid.load(atomic::Ordering::Acquire)) + } + + /// Mark the frame this one was materialized from as returned, so its + /// values may be read from here. + #[inline] + pub(crate) fn detach(&self) { + if let Some(cold) = self.cold_opt() { + cold.attached_tid.store(0, atomic::Ordering::Release); + } + } +} + +/// Python-visible frame object. Currently always wraps an `InterpreterFrame`. +/// Analogous to CPython's `PyFrameObject`. +#[pyclass(module = false, name = "frame", traverse = "manual")] +pub struct FrameObject { + // Owned references — keep the pointed-to objects alive for InterpreterFrame's + // raw pointers. Wrapped in Option so Traverse::clear can release them, + // allowing GC cycle collection to reclaim referenced objects. + pub(crate) owned_code: Option>, + pub(crate) owned_globals: Option, + pub(crate) owned_builtins: Option, + pub(crate) owned_func_obj: Option, + + /// Always `Some` while the frame is reachable from Python. Emptied only + /// by `Traverse::clear` during deallocation, leaving a trivially-droppable + /// husk that the freelist can cache. + pub(crate) iframe: FrameUnsafeCell>, +} + +impl FrameObject { + /// Shared access to the embedded interpreter frame. + /// + /// # Safety + /// Caller must ensure no concurrent mutable access (see `FrameUnsafeCell`) + /// and that the frame has not been cleared (i.e. it is still reachable + /// from Python; `Traverse::clear` only runs during deallocation). + #[inline(always)] + pub(crate) unsafe fn iframe_ref(&self) -> &InterpreterFrame { + let opt = unsafe { &*self.iframe.get() }; + #[cfg(debug_assertions)] + if opt.is_none() { + cleared_frame_access(); + } + // SAFETY: iframe is always Some while the frame is reachable (see above). + unsafe { opt.as_ref().unwrap_unchecked() } + } + + /// Exclusive access to the embedded interpreter frame. + /// + /// # Safety + /// Caller must ensure exclusive access (see `FrameUnsafeCell`) and that + /// the frame has not been cleared. + #[inline(always)] + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn iframe_mut(&self) -> &mut InterpreterFrame { + let opt = unsafe { &mut *self.iframe.get() }; + #[cfg(debug_assertions)] + if opt.is_none() { + cleared_frame_access(); + } + // SAFETY: iframe is always Some while the frame is reachable (see above). + unsafe { opt.as_mut().unwrap_unchecked() } + } + + /// Shared access to the embedded interpreter frame. Safe to call on any + /// reachable FrameObject: immutable fields and atomic/mutex fields are + /// always safe to access. + #[inline(always)] + pub fn iframe(&self) -> &InterpreterFrame { + // SAFETY: FrameObject is always reachable from Python when this is + // called. Immutable fields and atomic/mutex fields provide their own + // synchronization. Mutable fields (localsplus, prev_line) are only + // mutated during single-threaded execution via with_exec. + unsafe { self.iframe_ref() } + } +} + +/// Out-of-line panic for the debug-only cleared-frame check, keeping the +/// inlined accessors' stack frames minimal. +#[cfg(debug_assertions)] +#[cold] +#[inline(never)] +fn cleared_frame_access() -> ! { + panic!("frame accessed after clear"); +} + +// NOTE: Deref removed to decouple FrameObject +// from InterpreterFrame field layout. Access through iframe_ref()/iframe_mut(). + +thread_local! { + /// Free list of dead frame objects for reuse. Entries are cleared husks + /// (`iframe == None`) whose child references were already released. + /// PyInner is fixed-size (localsplus storage is out-of-line), + /// so a single bucket suffices. + static FRAME_FREELIST: core::cell::Cell> = + const { core::cell::Cell::new(crate::object::FreeList::new()) }; +} + +impl PyPayload for FrameObject { + const MAX_FREELIST: usize = 200; + const HAS_FREELIST: bool = true; + // Ordinary call frames are created untracked and only enter the GC when + // they escape (see `release_datastack_frame`); generator/coroutine frames + // are tracked explicitly at creation in `invoke_with_locals`. + const NEW_REF_UNTRACKED: bool = true; + + #[inline] + fn class(ctx: &Context) -> &'static Py { + ctx.types.frame_type + } + + #[inline] + unsafe fn freelist_push(obj: *mut PyObject) -> bool { + FRAME_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let stored = if list.len() < Self::MAX_FREELIST { + list.push(obj); + true + } else { + false + }; + fl.set(list); + stored + }) + .unwrap_or(false) + } + + #[inline] + unsafe fn freelist_pop(_payload: &Self) -> Option> { + FRAME_FREELIST + .try_with(|fl| { + let mut list = fl.take(); + let result = list.pop().map(|p| unsafe { NonNull::new_unchecked(p) }); + fl.set(list); + result + }) + .ok() + .flatten() + } +} + +unsafe impl Traverse for FrameObject { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + // Visit the owned reference anchors on FrameObject. + // After clear(), these are None. + self.owned_code.traverse(tracer_fn); + self.owned_func_obj.traverse(tracer_fn); + self.owned_globals.traverse(tracer_fn); + self.owned_builtins.traverse(tracer_fn); + + // Visit interior references in the InterpreterFrame. + let Some(iframe) = (unsafe { &*self.iframe.get() }) else { + return; + }; + iframe.localsplus.traverse(tracer_fn); + iframe.locals.traverse(tracer_fn); + if let Some(cold) = iframe.cold_opt() { + cold.trace.traverse(tracer_fn); + cold.temporary_refs.traverse(tracer_fn); + cold.f_locals_hidden_overlay.traverse(tracer_fn); + cold.f_extra_locals.traverse(tracer_fn); + cold.retained_back.traverse(tracer_fn); + } + } + + fn clear(&mut self, _out: &mut Vec) { + // Drop the interpreter frame and owned reference anchors so GC + // cycle collection can reclaim the referenced objects. The payload + // is left as a trivially-droppable husk for the freelist. + drop(self.iframe.get_mut().take()); + self.owned_code.take(); + self.owned_globals.take(); + self.owned_builtins.take(); + self.owned_func_obj.take(); + } +} + +// Running a frame can result in one of the below: +pub enum ExecutionResult { + Return(PyObjectRef), + Yield(PyObjectRef), + /// The bytecode loop wants to tail-call into a new frame that has + /// already been prepared on the datastack. The trampoline reads the + /// pending frame pointer from `vm.pending_tailcall_frame`. + TailCall, +} + +/// A valid execution result, or an exception +type FrameResult = PyResult>; + +impl FrameObject { + pub(crate) fn new( + code: PyRef, + scope: Scope, + builtins: PyObjectRef, + closure: &[PyCellRef], + func_obj: Option, + use_datastack: bool, + vm: &VirtualMachine, + ) -> Self { + let nlocalsplus = code.localspluskinds.len(); + let max_stackdepth = code.max_stackdepth as usize; + let localsplus = if use_datastack { + LocalsPlus::new_on_datastack(nlocalsplus, max_stackdepth, vm) + } else { + LocalsPlus::new(nlocalsplus, max_stackdepth) + }; + + let locals = match scope.locals { + Some(locals) => FrameLocals::with_locals(locals), + None if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) => FrameLocals::lazy(), + None => FrameLocals::with_locals(ArgMapping::from_dict_exact(scope.globals.clone())), + }; + + // Build the InterpreterFrame using the constructor. + // Pointers are initially set from owned fields' references but will be + // dangling after the FrameObject moves into heap allocation — they get + // patched by `init_iframe_ptrs` after `into_ref`. + let iframe = InterpreterFrame::new( + &code, + &scope.globals, + &builtins, + func_obj.as_deref(), + localsplus, + locals, + closure, + FrameOwner::FrameObject, + ); + Self { + owned_code: Some(code), + owned_globals: Some(scope.globals), + owned_builtins: Some(builtins), + owned_func_obj: func_obj, + iframe: FrameUnsafeCell::new(Some(iframe)), + } + } + + /// Patch the InterpreterFrame's raw pointers to point at this + /// FrameObject's owned fields. Must be called once after the + /// FrameObject is allocated on the heap (i.e. after `into_ref`). + fn init_iframe_ptrs(self_: &Py) { + let iframe = unsafe { self_.iframe_mut() }; + iframe.code = &**self_.owned_code.as_ref().unwrap() as *const Py; + iframe.globals = &**self_.owned_globals.as_ref().unwrap() as *const Py; + iframe.builtins = &**self_.owned_builtins.as_ref().unwrap() as *const PyObject; + iframe.func_obj = match &self_.owned_func_obj { + Some(obj) => &**obj as *const PyObject, + None => core::ptr::null(), + }; + // Link the InterpreterFrame back to its owning FrameObject. + iframe + .materialized + .store(self_ as *const Py as usize, Relaxed); + } + + /// Create a new FrameObject, allocate it on the heap, and patch + /// the InterpreterFrame's raw pointers. Returns an owned reference. + pub(crate) fn new_ref( + code: PyRef, + scope: Scope, + builtins: PyObjectRef, + closure: &[PyCellRef], + func_obj: Option, + use_datastack: bool, + vm: &VirtualMachine, + ) -> FrameObjectRef { + let frame = Self::new(code, scope, builtins, closure, func_obj, use_datastack, vm) + .into_ref(&vm.ctx); + Self::init_iframe_ptrs(&frame); + frame + } + + /// Access fastlocals immutably. + /// + /// # Safety + /// Caller must ensure no concurrent mutable access (frame not executing, + /// or called from the same thread during trace callback). + #[inline(always)] + pub unsafe fn fastlocals(&self) -> &[Option] { + unsafe { self.iframe_ref().localsplus.fastlocals() } + } + + /// Access fastlocals mutably. + /// + /// # Safety + /// Caller must ensure exclusive access (frame not executing). + #[inline(always)] + #[allow(clippy::mut_from_ref)] + pub unsafe fn fastlocals_mut(&self) -> &mut [Option] { + unsafe { self.iframe_mut().localsplus.fastlocals_mut() } + } + + /// Migrate data-stack-backed storage to the heap, preserving all values, + /// and return the data stack base pointer for `DataStack::pop()`. + /// Returns `None` if already heap-backed. + /// + /// # Safety + /// Caller must ensure the frame is not executing and the returned + /// pointer is passed to `VirtualMachine::datastack_pop()`. + pub(crate) unsafe fn materialize_localsplus(&self) -> Option<*mut u8> { + unsafe { self.iframe_mut().localsplus.materialize_to_heap() } + } + + /// Drop all localsplus values in place and detach the data stack backing + /// without the heap copy. Returns the data stack base pointer for + /// `VirtualMachine::datastack_pop()`, or `None` if heap-backed. + /// + /// # Safety + /// Caller must ensure the frame is not executing, that no other reference + /// to the frame exists or can be created (localsplus is unobservable + /// afterwards), and that the returned pointer is passed to + /// `VirtualMachine::datastack_pop()`. + pub(crate) unsafe fn release_localsplus(&self) -> Option<*mut u8> { + unsafe { self.iframe_mut().localsplus.release_datastack() } + } + + /// Whether this frame's localsplus is still data-stack-backed. A frame + /// must have heap-backed localsplus before it is GC-tracked so that a + /// concurrent collector never reads data-stack-resident, still-mutating + /// storage. Used only in debug assertions at the track sites. + pub(crate) fn localsplus_is_datastack_backed(&self) -> bool { + // SAFETY: called at a track site where the frame is not executing. + unsafe { self.iframe_ref().localsplus.is_datastack_backed() } + } + + /// Clear evaluation stack and state-owned cell/free references. + /// For full local/cell cleanup, call `clear_locals_and_stack()`. + pub(crate) fn clear_stack_and_cells(&self) { + // SAFETY: Called when frame is not executing (generator closed). + // Cell refs in fastlocals[nlocals..] are cleared by clear_locals_and_stack(). + unsafe { + self.iframe_mut().localsplus.stack_clear(); + } + } + + /// Clear locals and stack after generator/coroutine close. + /// Releases references held by the frame, matching _PyFrame_ClearLocals. + pub(crate) fn clear_locals_and_stack(&self) { + self.clear_stack_and_cells(); + // SAFETY: FrameObject is not executing (generator closed). + let fastlocals = unsafe { self.iframe_mut().localsplus.fastlocals_mut() }; + for slot in fastlocals.iter_mut() { + *slot = None; + } + self.iframe().cold().f_locals_hidden_overlay.lock().take(); + self.iframe().cold().f_extra_locals.lock().take(); + } + + /// Store a borrowed back-reference to the owning generator/coroutine. + /// The caller must ensure the generator outlives the frame. + pub fn set_generator(&self, generator: &PyObject) { + self.iframe().generator.store(generator); + self.iframe() + .owner + .store(FrameOwner::Generator as i8, atomic::Ordering::Release); + } + + /// Clear the generator back-reference. Called when the generator is finalized. + pub fn clear_generator(&self) { + // The generator's drop may run after this frame was already cleared + // by cycle collection (both were garbage and the frame was cleared + // first); nothing to unlink then. + // SAFETY: shared access; the finalizing generator owns the frame, + // which is not executing. + let Some(iframe) = (unsafe { &*self.iframe.get() }) else { + return; + }; + iframe.generator.clear(); + iframe + .owner + .store(FrameOwner::FrameObject as i8, atomic::Ordering::Release); + } + + pub fn current_location(&self) -> SourceLocation { + let lasti = self.lasti() as usize; + if lasti == 0 { + return SourceLocation { + line: self + .iframe() + .code() + .first_line_number + .unwrap_or(OneIndexed::MIN), + character_offset: OneIndexed::from_zero_indexed(0), + }; + } + self.iframe().code().locations[lasti - 1].0 + } + + /// Get the previous InterpreterFrame in the chain. + /// Returns null if the frame has been cleared (GC deallocation). + pub fn previous_iframe(&self) -> *const InterpreterFrame { + // Use raw access instead of iframe() to avoid panicking on cleared frames. + let iframe_opt = unsafe { &*self.iframe.get() }; + match iframe_opt.as_ref() { + Some(iframe) => { + iframe.previous.load(atomic::Ordering::Relaxed) as *const InterpreterFrame + } + None => core::ptr::null(), + } + } + + /// Get the previous FrameObject in the chain, if any. + /// Walks through the chain to find the next materialized frame. + pub fn previous_frame(&self) -> *const Self { + let mut cur = self.previous_iframe(); + while !cur.is_null() { + let iframe = unsafe { &*cur }; + if let Some(fo) = iframe.frame_obj() { + return &**fo as *const Self; + } + cur = iframe.previous.load(atomic::Ordering::Relaxed) as *const InterpreterFrame; + } + core::ptr::null() + } + + /// Record that a durable Python-level reference to this frame escaped. + pub(crate) fn mark_escaped(&self) { + self.iframe() + .cold() + .escaped + .store(true, atomic::Ordering::Release); + } + + /// Whether a durable reference to this frame has escaped. + pub(crate) fn has_escaped(&self) -> bool { + self.iframe() + .cold_opt() + .is_some_and(|c| c.escaped.load(atomic::Ordering::Acquire)) + } + + pub fn lasti(&self) -> u32 { + self.iframe().lasti.load(Relaxed) + } + + pub fn set_lasti(&self, val: u32) { + self.iframe().lasti.store(val, Relaxed); + } + + /// Fast-local slots of the live source frame when this frame object's + /// frame is still running on this thread, and this frame object's own + /// slots otherwise. A running frame's slots live on the data stack; the + /// frame object's are empty until `exit_iframe` fills them. + /// + /// # Safety + /// Caller must ensure no concurrent mutable access: either the frame is + /// not executing (callers pass through `check_locals_access`), or this is + /// a trace callback on the thread that is executing it. + unsafe fn live_fastlocals(&self) -> &[Option] { + let live = self.find_live_source_iframe(); + if live.is_null() { + unsafe { self.iframe_ref().localsplus.fastlocals() } + } else { + unsafe { (*live).localsplus.fastlocals() } + } + } + + fn has_active_hidden_locals(&self) -> bool { + use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE, CO_FAST_HIDDEN}; + let code = self.iframe().code(); + // SAFETY: reached from `locals()` on the thread running this frame. + let fastlocals = unsafe { self.live_fastlocals() }; let is_optimized = code.flags.contains(bytecode::CodeFlags::OPTIMIZED); !is_optimized && code.localspluskinds.iter().enumerate().any(|(i, &kind)| { @@ -928,8 +1855,8 @@ impl Frame { }; // SAFETY: Either the frame is not executing (caller checked owner), // or we're in a trace callback on the same thread that's executing. - let code = &**self.code; - let fastlocals = unsafe { (*self.iframe.get()).localsplus.fastlocals() }; + let code = self.iframe().code(); + let fastlocals = unsafe { self.live_fastlocals() }; // Iterate through all localsplus slots using localspluskinds let nlocalsplus = code.localspluskinds.len(); @@ -962,9 +1889,10 @@ impl Frame { } } - // Free variables only included for optimized (function-like) scopes. - // Class/module scopes should not expose free vars in locals(). - if kind == CO_FAST_FREE && !is_optimized { + // CPython only syncs fastlocals/cells into locals() for function + // scope; class/module scope just returns the namespace dict as-is + // (_PyFrame_GetLocals, Objects/frameobject.c). + if !is_optimized && kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 { continue; } @@ -1023,15 +1951,59 @@ impl Frame { Ok(()) } + /// Reject locals access for a frame that is executing on another thread. + /// + /// A thread-owned frame mutates `localsplus` without synchronization, so + /// reading fastlocals from a different thread would be a data race (the + /// executing thread overwrites slots and drops the old values while the + /// reader clones them). Access from the executing thread itself (locals() + /// builtin, trace callbacks) is fine: the frame sits on the current + /// thread's frame chain and is at a bytecode boundary. + pub(crate) fn check_locals_access(&self, vm: &VirtualMachine) -> PyResult<()> { + // A frame object materialized from a running data stack frame is + // FrameObject-owned, so the owner test below cannot speak for it: the + // thread running that frame fills these slots when it returns. + let attached = self.iframe().attached_tid(); + if attached != 0 && attached != current_thread_ident() { + return Err(vm.new_runtime_error( + "cannot access frame locals while the frame is executing in another thread", + )); + } + let owner = FrameOwner::from_i8(self.iframe().owner.load(atomic::Ordering::Acquire)); + if owner != FrameOwner::Thread { + return Ok(()); + } + let self_iframe = self.iframe() as *const InterpreterFrame; + // Get the Py address from &FrameObject (payload). + // materialized stores a *const Py. + let self_py_ptr = unsafe { Py::::from_payload_ptr(self) } as usize; + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + if core::ptr::eq(cur, self_iframe) { + return Ok(()); + } + // Also match if this FrameObject is the materialized version + // of a stack-allocated frame in the chain. + let materialized = unsafe { (*cur).materialized.load(Relaxed) }; + if materialized == self_py_ptr { + return Ok(()); + } + cur = unsafe { (*cur).previous.load(Relaxed) as *const InterpreterFrame }; + } + Err(vm.new_runtime_error( + "cannot access frame locals while the frame is executing in another thread", + )) + } + pub fn f_locals_mapping(&self, vm: &VirtualMachine) -> PyResult { + self.check_locals_access(vm)?; if !self.has_active_hidden_locals() { - self.f_locals_hidden_overlay.lock().take(); + self.iframe().cold().f_locals_hidden_overlay.lock().take(); return self.locals(vm); } - let needs_refresh = !self.locals_dirty.load(atomic::Ordering::Acquire); let overlay_dict = { - let mut overlay = self.f_locals_hidden_overlay.lock(); + let mut overlay = self.iframe().cold().f_locals_hidden_overlay.lock(); match overlay.as_ref() { Some(dict) => dict.clone(), None => { @@ -1041,59 +2013,307 @@ impl Frame { } } }; - if needs_refresh { - PyDict::clear(&overlay_dict); - let overlay = ArgMapping::from_dict_exact(overlay_dict.clone()); - self.sync_visible_locals_to_mapping(overlay.mapping(), vm)?; - } + PyDict::clear(&overlay_dict); + let overlay = ArgMapping::from_dict_exact(overlay_dict.clone()); + self.sync_visible_locals_to_mapping(overlay.mapping(), vm)?; Ok(ArgMapping::from_dict_exact(overlay_dict)) } pub fn locals(&self, vm: &VirtualMachine) -> PyResult { - if self.has_active_hidden_locals() { + let mapping = if self.has_active_hidden_locals() { // Match CPython's locals() behavior for frames with PEP 709 hidden // locals: return a fresh snapshot instead of the backing mapping. let overlay = ArgMapping::from_dict_exact(vm.ctx.new_dict()); self.sync_visible_locals_to_mapping(overlay.mapping(), vm)?; - Ok(overlay) + overlay + } else { + self.sync_visible_locals_to_mapping(self.iframe().locals.mapping(vm), vm)?; + self.iframe().locals.clone_mapping(vm) + }; + self.fold_extra_locals(&mapping, vm)?; + Ok(mapping) + } + + /// Copy the frame's extra-locals side storage (proxy keys that are not + /// fast locals) into `mapping`. No-op when nothing was ever stored. + fn fold_extra_locals(&self, mapping: &ArgMapping, vm: &VirtualMachine) -> PyResult<()> { + let extra = self.iframe().cold().f_extra_locals.lock().clone(); + if let Some(extra) = extra { + for (key, value) in &extra { + mapping.mapping().ass_subscript(&key, Some(value), vm)?; + } + } + Ok(()) + } + + /// Read a fast-local slot's visible value, dereferencing cells. `None` if + /// the slot is empty or its cell holds no value. + fn framelocalsproxy_getval(&self, i: usize) -> Option { + use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE}; + // SAFETY: callers first pass through `check_locals_access`, so the + // frame is not executing on another thread. + let fastlocals = unsafe { self.live_fastlocals() }; + let obj = fastlocals.get(i)?.as_ref()?; + let kind = self + .iframe() + .code() + .localspluskinds + .get(i) + .copied() + .unwrap_or(0); + if kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 { + if let Some(cell) = obj.downcast_ref::() { + cell.get() + } else { + Some(obj.clone()) + } + } else { + Some(obj.clone()) + } + } + + /// Write `value` into fast-local slot `i`, routing through the cell when + /// the slot holds one so closures keep sharing the same cell. + fn framelocalsproxy_setval(&self, i: usize, value: PyObjectRef) { + use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE}; + let kind = self + .iframe() + .code() + .localspluskinds + .get(i) + .copied() + .unwrap_or(0); + // SAFETY: callers first pass through `check_locals_access`. + // Use live source iframe if available so writes reach the + // executing frame's actual local variables. + let live = self.find_live_source_iframe(); + let fastlocals = if !live.is_null() { + unsafe { &mut *live.cast_mut() }.localsplus.fastlocals_mut() } else { - self.sync_visible_locals_to_mapping(self.locals.mapping(vm), vm)?; - Ok(self.locals.clone_mapping(vm)) + unsafe { self.iframe_mut().localsplus.fastlocals_mut() } + }; + if kind & (CO_FAST_CELL | CO_FAST_FREE) != 0 + && let Some(obj) = fastlocals[i].as_ref() + && let Some(cell) = obj.downcast_ref::() + { + cell.set(Some(value)); + return; + } + fastlocals[i] = Some(value); + } + + /// Resolve `key` to a fast-local slot index, or `None` if it names no fast + /// local. `read` selects read semantics (only bound slots match) versus + /// write semantics (hidden slots are skipped, unbound slots still match). + /// Raises `TypeError` for an unhashable key. + fn framelocalsproxy_getkeyindex( + &self, + key: &PyObject, + read: bool, + vm: &VirtualMachine, + ) -> PyResult> { + use rustpython_compiler_core::bytecode::CO_FAST_HIDDEN; + // The proxy hashes the key first; an unhashable key raises TypeError. + key.hash(vm)?; + for (i, &kind) in self.iframe().code().localspluskinds.iter().enumerate() { + let name = localsplus_name(self.iframe().code(), i); + if !name + .as_object() + .rich_compare_bool(key, PyComparisonOp::Eq, vm)? + { + continue; + } + if read { + if self.framelocalsproxy_getval(i).is_some() { + return Ok(Some(i)); + } + } else if kind & CO_FAST_HIDDEN == 0 { + return Ok(Some(i)); + } + } + Ok(None) + } + + /// Build a fresh ordered snapshot dict of the proxy's visible contents: + /// bound fast locals in localsplus order followed by extra locals. + pub(crate) fn framelocalsproxy_snapshot(&self, vm: &VirtualMachine) -> PyResult { + self.check_locals_access(vm)?; + let dict = vm.ctx.new_dict(); + let mapping = ArgMapping::from_dict_exact(dict.clone()); + self.sync_visible_locals_to_mapping(mapping.mapping(), vm)?; + self.fold_extra_locals(&mapping, vm)?; + Ok(dict) + } + + /// `proxy[key]`: read a fast local live, else fall back to extra locals. + pub(crate) fn framelocalsproxy_getitem( + &self, + key: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + self.check_locals_access(vm)?; + if let Some(i) = self.framelocalsproxy_getkeyindex(&key, true, vm)? + && let Some(value) = self.framelocalsproxy_getval(i) + { + return Ok(value); + } + let extra = self.iframe().cold().f_extra_locals.lock().clone(); + if let Some(extra) = extra + && let Some(value) = extra.get_item_opt(&*key, vm)? + { + return Ok(value); + } + Err(vm.new_key_error(key)) + } + + /// `key in proxy`. + pub(crate) fn framelocalsproxy_contains( + &self, + key: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + self.check_locals_access(vm)?; + if self.framelocalsproxy_getkeyindex(&key, true, vm)?.is_some() { + return Ok(true); + } + let extra = self.iframe().cold().f_extra_locals.lock().clone(); + if let Some(extra) = extra { + return Ok(extra.get_item_opt(&*key, vm)?.is_some()); + } + Ok(false) + } + + /// `proxy[key] = value`: fast-key writes the slot in place, other keys go + /// to the extra-locals side dict. + pub(crate) fn framelocalsproxy_setitem( + &self, + key: PyObjectRef, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + self.check_locals_access(vm)?; + if let Some(i) = self.framelocalsproxy_getkeyindex(&key, false, vm)? { + self.framelocalsproxy_setval(i, value); + return Ok(()); + } + let extra = self.extra_locals_get_or_create(vm); + extra.set_item(&*key, value, vm) + } + + /// `del proxy[key]`: deleting a fast local raises ValueError; extra keys are + /// removed (KeyError if absent). + pub(crate) fn framelocalsproxy_delitem( + &self, + key: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + self.check_locals_access(vm)?; + if self + .framelocalsproxy_getkeyindex(&key, false, vm)? + .is_some() + { + return Err(vm.new_value_error("cannot remove local variables from FrameLocalsProxy")); + } + let extra = self.iframe().cold().f_extra_locals.lock().clone(); + if let Some(extra) = extra + && extra.get_item_opt(&*key, vm)?.is_some() + { + return extra.del_item(&*key, vm); + } + Err(vm.new_key_error(key)) + } + + /// `proxy.pop(key[, default])`. + pub(crate) fn framelocalsproxy_pop( + &self, + key: PyObjectRef, + default: Option, + vm: &VirtualMachine, + ) -> PyResult { + self.check_locals_access(vm)?; + if self + .framelocalsproxy_getkeyindex(&key, false, vm)? + .is_some() + { + return Err(vm.new_value_error("cannot remove local variables from FrameLocalsProxy")); + } + let extra = self.iframe().cold().f_extra_locals.lock().clone(); + if let Some(extra) = extra + && let Some(value) = extra.pop_item(&*key, vm)? + { + return Ok(value); } + default.ok_or_else(|| vm.new_key_error(key)) + } + + /// `proxy.setdefault(key, default)`. + pub(crate) fn framelocalsproxy_setdefault( + &self, + key: PyObjectRef, + default: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + match self.framelocalsproxy_getitem(key.clone(), vm) { + Ok(value) => Ok(value), + Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => { + self.framelocalsproxy_setitem(key, default.clone(), vm)?; + Ok(default) + } + Err(e) => Err(e), + } + } + + fn extra_locals_get_or_create(&self, vm: &VirtualMachine) -> PyDictRef { + let mut extra = self.iframe().cold().f_extra_locals.lock(); + extra.get_or_insert_with(|| vm.ctx.new_dict()).clone() } } -impl Py { +impl Py { #[inline(always)] fn with_exec(&self, vm: &VirtualMachine, f: impl FnOnce(ExecutingFrame<'_>) -> R) -> R { - // SAFETY: Frame execution is single-threaded. Only one thread at a time + // SAFETY: FrameObject execution is single-threaded. Only one thread at a time // executes a given frame (enforced by the owner field and generator // running flag). Same safety argument as FastLocals (UnsafeCell). - let iframe = unsafe { &mut *self.iframe.get() }; + let iframe = unsafe { self.iframe_mut() }; + // Dereference the raw pointers before taking mutable borrows + // to localsplus/prev_line. The raw pointers point to FrameObject's + // owned fields, not into InterpreterFrame, so there is no aliasing. + let code: &Py = unsafe { &*iframe.code }; + let globals: &Py = unsafe { &*iframe.globals }; + let builtins: &PyObject = unsafe { &*iframe.builtins }; + let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() { + None + } else { + Some(unsafe { &*iframe.func_obj }) + }; + let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) { + builtins + .downcast_ref_if_exact::(vm) + // SAFETY: downcast_ref_if_exact already verified exact type + .map(|d| unsafe { PyExact::ref_unchecked(d) }) + } else { + None + }; + let iframe_ptr = iframe as *const InterpreterFrame; let exec = ExecutingFrame { - code: &iframe.code, + code, localsplus: &mut iframe.localsplus, locals: &iframe.locals, - globals: &iframe.globals, - builtins: &iframe.builtins, - builtins_dict: if iframe.globals.class().is(vm.ctx.types.dict_type) { - iframe - .builtins - .downcast_ref_if_exact::(vm) - // SAFETY: downcast_ref_if_exact already verified exact type - .map(|d| unsafe { PyExact::ref_unchecked(d) }) - } else { - None - }, + globals, + builtins, + builtins_dict, lasti: &iframe.lasti, - object: self, - prev_line: &mut iframe.prev_line, + iframe: iframe_ptr, + func_obj, + prev_line: &iframe.prev_line, monitoring_mask: 0, + tailcall_enabled: false, }; f(exec) } - // #[cfg_attr(feature = "flame-it", flame("Frame"))] + // #[cfg_attr(feature = "flame-it", flame("FrameObject"))] pub fn run(&self, vm: &VirtualMachine) -> PyResult { self.with_exec(vm, |mut exec| exec.run(vm)) } @@ -1124,23 +2344,34 @@ impl Py { pub fn yield_from_target(&self) -> Option { // If the frame is currently executing (owned by thread), it has no // yield-from target to report. - let owner = FrameOwner::from_i8(self.owner.load(atomic::Ordering::Acquire)); + let owner = FrameOwner::from_i8(self.iframe().owner.load(atomic::Ordering::Acquire)); if owner == FrameOwner::Thread { return None; } - // SAFETY: Frame is not executing, so UnsafeCell access is safe. - let iframe = unsafe { &mut *self.iframe.get() }; + // SAFETY: FrameObject is not executing, so UnsafeCell access is safe. + let iframe = unsafe { self.iframe_mut() }; + let code: &Py = unsafe { &*iframe.code }; + let globals: &Py = unsafe { &*iframe.globals }; + let builtins: &PyObject = unsafe { &*iframe.builtins }; + let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() { + None + } else { + Some(unsafe { &*iframe.func_obj }) + }; + let iframe_ptr = iframe as *const InterpreterFrame; let exec = ExecutingFrame { - code: &iframe.code, + code, localsplus: &mut iframe.localsplus, locals: &iframe.locals, - globals: &iframe.globals, - builtins: &iframe.builtins, + globals, + builtins, builtins_dict: None, lasti: &iframe.lasti, - object: self, - prev_line: &mut iframe.prev_line, + iframe: iframe_ptr, + func_obj, + prev_line: &iframe.prev_line, monitoring_mask: 0, + tailcall_enabled: false, }; exec.yield_from_target().map(PyObject::to_owned) } @@ -1152,7 +2383,7 @@ impl Py { filename.find(b"importlib").is_some() && filename.find(b"_bootstrap").is_some() } - pub fn next_external_frame(&self, vm: &VirtualMachine) -> Option { + pub fn next_external_frame(&self, vm: &VirtualMachine) -> Option { let mut frame = self.f_back(vm); while let Some(ref f) = frame { if !f.is_internal_frame() { @@ -1164,32 +2395,186 @@ impl Py { } } +/// Identity of the calling thread, or 0 where there is only one thread to be. +/// 0 doubles as "no thread", which is what `attached_tid` wants for a build +/// that cannot have a frame running anywhere else. +#[inline] +fn current_thread_ident() -> u64 { + #[cfg(feature = "threading")] + { + crate::stdlib::_thread::get_ident() + } + #[cfg(not(feature = "threading"))] + { + 0 + } +} + +/// Byte offset from the start of a datastack allocation to the LocalsPlus data, +/// accounting for alignment padding after the InterpreterFrame header. +#[inline] +fn datastack_iframe_localsplus_offset() -> usize { + let iframe_size = core::mem::size_of::(); + (iframe_size + core::mem::align_of::() - 1) & !(core::mem::align_of::() - 1) +} + +/// Total bytes needed to co-allocate an InterpreterFrame and its LocalsPlus +/// data on the thread data stack. +pub(crate) fn datastack_iframe_total_bytes(nlocalsplus: usize, stacksize: usize) -> usize { + let iframe_padded = datastack_iframe_localsplus_offset(); + let capacity = nlocalsplus + .checked_add(stacksize) + .expect("LocalsPlus capacity overflow"); + let data_bytes = capacity + .checked_mul(core::mem::size_of::()) + .expect("LocalsPlus byte size overflow"); + iframe_padded + .checked_add(data_bytes) + .expect("datastack iframe total size overflow") +} + +/// Handle an exception propagating into a suspended caller frame in the +/// trampoline. Adds a traceback entry at the caller's call site, then +/// tries the caller's exception table via `unwind_blocks`. +/// +/// Returns: +/// - `Ok(None)` — handler found, the caller's `run_iframe` can be re-entered +/// - `Ok(Some(result))` — handler returned a result (break from the run loop) +/// - `Err(exc)` — no handler, exception propagates to the next caller +pub(crate) fn trampoline_handle_exception( + iframe: &mut InterpreterFrame, + exception: &PyBaseExceptionRef, + vm: &VirtualMachine, +) -> FrameResult { + let code: &Py = unsafe { &*iframe.code }; + let globals: &Py = unsafe { &*iframe.globals }; + let builtins: &PyObject = unsafe { &*iframe.builtins }; + let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() { + None + } else { + Some(unsafe { &*iframe.func_obj }) + }; + let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) { + builtins + .downcast_ref_if_exact::(vm) + .map(|d| unsafe { PyExact::ref_unchecked(d) }) + } else { + None + }; + let iframe_ptr = iframe as *const InterpreterFrame; + let mut exec = ExecutingFrame { + code, + localsplus: &mut iframe.localsplus, + locals: &iframe.locals, + globals, + builtins, + builtins_dict, + lasti: &iframe.lasti, + iframe: iframe_ptr, + func_obj, + prev_line: &mut iframe.prev_line, + monitoring_mask: 0, + tailcall_enabled: false, + }; + + // lasti points past the CallPyExactArgs instruction (+ cache entries). + // The exception occurred at the previous instruction (the call site). + let idx = (exec.lasti() as usize).saturating_sub(1); + + // Add traceback entry at the call site. + if let Some((loc, _end_loc)) = exec.code.locations.get(idx) { + let next = exception.__traceback__(); + let new_traceback = PyTraceback::new(next, exec.frame_object(vm), idx as u32 * 2, loc.line); + exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); + } + + exec.unwind_blocks( + vm, + UnwindReason::Raising { + exception: exception.clone(), + }, + ) +} + +/// Execute an InterpreterFrame's bytecode directly, without a FrameObject. +/// +/// # Safety +/// The InterpreterFrame's raw pointers (code, globals, builtins, func_obj) +/// must be valid for the duration of this call. +#[inline(always)] +pub(crate) fn run_iframe( + iframe: &mut InterpreterFrame, + vm: &VirtualMachine, +) -> PyResult { + let code: &Py = unsafe { &*iframe.code }; + let globals: &Py = unsafe { &*iframe.globals }; + let builtins: &PyObject = unsafe { &*iframe.builtins }; + let func_obj: Option<&PyObject> = if iframe.func_obj.is_null() { + None + } else { + Some(unsafe { &*iframe.func_obj }) + }; + let builtins_dict = if globals.class().is(vm.ctx.types.dict_type) { + builtins + .downcast_ref_if_exact::(vm) + .map(|d| unsafe { PyExact::ref_unchecked(d) }) + } else { + None + }; + let iframe_ptr = iframe as *const InterpreterFrame; + let mut exec = ExecutingFrame { + code, + localsplus: &mut iframe.localsplus, + locals: &iframe.locals, + globals, + builtins, + builtins_dict, + lasti: &iframe.lasti, + iframe: iframe_ptr, + func_obj, + prev_line: &mut iframe.prev_line, + monitoring_mask: 0, + tailcall_enabled: true, + }; + exec.run(vm) +} + /// An executing frame; borrows mutable frame-internal data for the duration /// of bytecode execution. -struct ExecutingFrame<'a> { - code: &'a PyRef, +pub(crate) struct ExecutingFrame<'a> { + code: &'a Py, localsplus: &'a mut LocalsPlus, locals: &'a FrameLocals, - globals: &'a PyDictRef, - builtins: &'a PyObjectRef, + globals: &'a Py, + builtins: &'a PyObject, /// Cached downcast of builtins to PyDict for fast LOAD_GLOBAL. /// Only set when both globals and builtins are exact dict types (not /// subclasses), so that `__missing__` / `__getitem__` overrides are /// not bypassed. builtins_dict: Option<&'a PyExact>, - object: &'a Py, + /// Raw pointer to the underlying InterpreterFrame. Used to access the + /// materialized FrameObject (via `frame_obj()`) and frame-level fields + /// like trace, pending_stack_pops, etc. Stored as a raw pointer because + /// mutable borrows to `localsplus` and `prev_line` are also held. + /// All accesses through this pointer use atomic/mutex operations. + iframe: *const InterpreterFrame, + /// Borrowed function object that created this frame (if any). + func_obj: Option<&'a PyObject>, lasti: &'a PyAtomic, - prev_line: &'a mut u32, + prev_line: &'a core::cell::Cell, /// Cached monitoring events mask. Reloaded at Resume instruction only, monitoring_mask: u32, + /// Whether TailCall is allowed. True when running under the trampoline + /// (`run_frame_fast`), false for FrameObject-based execution. + tailcall_enabled: bool, } #[inline] -fn specialization_compact_int_value(i: &PyInt, vm: &VirtualMachine) -> Option { +fn specialization_compact_int_value(i: &PyInt) -> Option { // _PyLong_IsCompact(): a one-digit PyLong (base 2^30), // i.e. abs(value) <= 2^30 - 1. const CPYTHON_COMPACT_LONG_ABS_MAX: i64 = (1i64 << 30) - 1; - let v = i.try_to_primitive::(vm).ok()?; + let v = i.try_to_i64_fast()?; if (-CPYTHON_COMPACT_LONG_ABS_MAX..=CPYTHON_COMPACT_LONG_ABS_MAX).contains(&v) { Some(v as isize) } else { @@ -1200,7 +2585,7 @@ fn specialization_compact_int_value(i: &PyInt, vm: &VirtualMachine) -> Option Option { obj.downcast_ref_if_exact::(vm) - .and_then(|i| specialization_compact_int_value(i, vm)) + .and_then(|i| specialization_compact_int_value(i)) } #[inline] @@ -1220,12 +2605,129 @@ fn specialization_nonnegative_compact_index(i: &PyInt, vm: &VirtualMachine) -> O } } -fn release_datastack_frame(frame: &Py, vm: &VirtualMachine) { +/// Get the variable name for a localsplus index of `code`. +fn localsplus_name(code: &PyCode, idx: usize) -> &'static PyStrInterned { + use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE, CO_FAST_LOCAL}; + let nlocals = code.varnames.len(); + let kind = code.localspluskinds.get(idx).copied().unwrap_or(0); + if kind & CO_FAST_LOCAL != 0 { + // Merged cell or regular local: name is in varnames + code.varnames[idx] + } else if kind & CO_FAST_FREE != 0 { + // Free var: slots are at the end of localsplus + let nlocalsplus = code.localspluskinds.len(); + let nfrees = code.freevars.len(); + let free_start = nlocalsplus - nfrees; + code.freevars[idx - free_start] + } else if kind & CO_FAST_CELL != 0 { + // Non-merged cell: count how many non-merged cell slots are before + // this index to find the corresponding cellvars entry. + // Non-merged cellvars appear in their original order (skipping merged ones). + let nonmerged_pos = code.localspluskinds[nlocals..idx] + .iter() + .filter(|&&k| k == CO_FAST_CELL) + .count(); + // Skip merged cellvars to find the right one + let mut cv_idx = 0; + let mut nonmerged_count = 0; + for (i, name) in code.cellvars.iter().enumerate() { + let is_merged = code.varnames.contains(name); + if !is_merged { + if nonmerged_count == nonmerged_pos { + cv_idx = i; + break; + } + nonmerged_count += 1; + } + } + code.cellvars[cv_idx] + } else { + code.varnames[idx] + } +} + +/// Free a finished call frame's data stack storage. +/// +/// When the caller holds the only reference to the frame, the locals and +/// stack values are dropped in place and the storage is released without a +/// heap copy. Otherwise (the frame escaped through a traceback, +/// `sys._getframe`, a trace callback, ...) the values are copied to the heap +/// first so they stay readable through the escaped reference. +pub(crate) fn release_datastack_frame(frame: &Py, vm: &VirtualMachine) { + let frame_obj = frame.as_object(); + // Uniqueness argument: at this point the frame is already out of + // the thread-frames registry and the current-frame chain + // (both unlinked inside `with_frame` before it returned), and the + // frame type has no weakref support. A datastack frame is created + // untracked and stays untracked while it runs, so it is in no GC + // generation list and no collector can observe or incref it. Hence no + // thread can mint a new reference without already holding one, and every + // escape (traceback, `sys._getframe`, `f_back`, a stored trace-hook arg) + // is a heap reference created on this thread while the frame ran. + // Therefore `strong_count() == 1` here means nothing escaped, and + // `strong_count() > 1` means the frame escaped. + debug_assert!( + !frame_obj.is_gc_tracked(), + "datastack frame is GC-tracked at release" + ); + if frame_obj.strong_count() == 1 { + // A reference minted and already released by another thread (through a + // heap escape carried across threads) ends in a release-decref; the + // fence orders that thread's memory before our drops below. + atomic::fence(Acquire); + // SAFETY: unique owner and no way to mint a new reference, so + // localsplus can never be observed again. The base pointer came + // from this thread's data stack. + unsafe { + if let Some(base) = frame.release_localsplus() { + vm.datastack_pop(base); + } + } + return; + } + // Escaped. Stabilize localsplus on the heap FIRST, then join the GC. This + // order guarantees a concurrent (stop-the-world) collector only ever sees a + // tracked frame whose localsplus is heap-resident and no longer mutating: + // the frame has stopped executing before it becomes a candidate, so its + // outgoing edges are stable while a collector traverses them. + // SAFETY: the frame finished executing; the base pointer came from this + // thread's data stack. unsafe { if let Some(base) = frame.materialize_localsplus() { vm.datastack_pop(base); } } + // Retain a strong reference to the caller so `f_back` keeps resolving once + // the caller returns and leaves the live frame chain. The caller is still + // executing here (this frame is unwinding back into it), so its payload + // pointer is live. + { + let mut guard = frame.iframe().cold().retained_back.lock(); + if guard.is_none() { + let prev = frame.previous_iframe(); + *guard = unsafe { owned_chain_frame(prev) }; + } + } + // Note: previous is NOT cleared here. retained_back captures the + // caller reference, and previous may be read again by f_back or + // frame chain walkers (the pointer is live as long as the caller + // is still executing, which it is at this point). + // Invariant: a tracked frame must always have heap-backed localsplus + // (proven here for escaped datastack frames and by construction for + // generator frames, which are born heap-backed). A stop-the-world + // collector reads a frame's localsplus only when the frame is a tracked + // candidate, so this keeps it from ever reading data-stack-resident, + // still-mutating storage of an executing frame. + debug_assert!( + !frame.localsplus_is_datastack_backed(), + "escaped frame tracked before its localsplus was materialized" + ); + // SAFETY: the frame is alive (held by `frame` and the escaped reference) + // and untracked. + unsafe { + crate::gc_state::gc_state() + .track_object(NonNull::from(frame_obj), crate::gc_state::current_owner()) + }; } type BinaryOpExtendGuard = fn(&PyObject, &PyObject, &VirtualMachine) -> bool; @@ -1239,6 +2741,34 @@ struct BinaryOpExtendSpecializationDescr { const BINARY_OP_EXTEND_EXTERNAL_CACHE_OFFSET: usize = 1; +/// Max total args (including self) staged in a fixed-size stack buffer by the +/// exact-args call fast paths; larger arities fall back to a heap buffer. +const MAX_INLINE_CALL_ARGS: usize = 8; + +/// Staging buffer for exact-args call fast paths: fixed-size inline storage +/// for small arities, avoiding a per-call Vec allocation. +enum CallArgBuffer { + Inline(usize, [Option; MAX_INLINE_CALL_ARGS]), + Heap(Vec>), +} + +impl CallArgBuffer { + fn new(total_nargs: usize) -> Self { + if total_nargs <= MAX_INLINE_CALL_ARGS { + Self::Inline(total_nargs, [const { None }; MAX_INLINE_CALL_ARGS]) + } else { + Self::Heap(vec![None; total_nargs]) + } + } + + fn slots(&mut self) -> &mut [Option] { + match self { + Self::Inline(len, buf) => &mut buf[..*len], + Self::Heap(buf) => buf, + } + } +} + #[inline] fn compactlongs_guard(lhs: &PyObject, rhs: &PyObject, vm: &VirtualMachine) -> bool { compact_int_from_obj(lhs, vm).is_some() && compact_int_from_obj(rhs, vm).is_some() @@ -1395,56 +2925,87 @@ impl fmt::Debug for ExecutingFrame<'_> { } } +/// Run bytecode in a light frame context. +#[allow(clippy::too_many_arguments)] impl ExecutingFrame<'_> { + /// Get the underlying InterpreterFrame. + #[inline(always)] + fn iframe(&self) -> &InterpreterFrame { + // SAFETY: the iframe pointer is valid for the lifetime of the ExecutingFrame. + unsafe { &*self.iframe } + } + + /// Get the frame object. Materializes a FrameObject on demand if this + /// is a stack-allocated frame that hasn't been observed yet. + #[cold] + #[inline(never)] + fn frame_object(&self, vm: &VirtualMachine) -> FrameObjectRef { + self.iframe().materialize(vm).to_owned() + } + + /// Whether this frame has a per-frame trace function set. #[inline] - fn monitoring_disabled_for_code(&self, vm: &VirtualMachine) -> bool { - self.code.is(&vm.ctx.init_cleanup_code) + fn trace_is_set(&self, _vm: &VirtualMachine) -> bool { + self.iframe() + .cold_opt() + .is_some_and(|c| c.trace.lock().is_some()) } - fn specialization_new_init_cleanup_frame(&self, vm: &VirtualMachine) -> FrameRef { - Frame::new( - vm.ctx.init_cleanup_code.clone(), - Scope::new( - Some(ArgMapping::from_dict_exact(vm.ctx.new_dict())), - self.globals.clone(), - ), - self.builtins.clone(), - &[], - None, - true, - vm, - ) - .into_ref(&vm.ctx) + /// Access the frame's trace_opcodes lock. + #[inline] + fn trace_opcodes_is_set(&self) -> bool { + self.iframe() + .cold_opt() + .is_some_and(|c| *c.trace_opcodes.lock()) + } + + /// Get pending_stack_pops from the frame. + #[inline] + fn pending_stack_pops(&self) -> u32 { + self.iframe() + .cold_opt() + .map_or(0, |c| c.pending_stack_pops.load(Relaxed)) + } + + /// Get pending_unwind_from_stack from the frame. + #[inline] + fn pending_unwind_from_stack(&self) -> i64 { + self.iframe() + .cold_opt() + .map_or(0, |c| c.pending_unwind_from_stack.load(Relaxed)) } - fn specialization_run_init_cleanup_shim( + /// Set pending_stack_pops on the frame. + #[inline] + fn set_pending_stack_pops(&self, val: u32) { + self.iframe().cold().pending_stack_pops.store(val, Relaxed); + } + + /// Run `__init__` for the tp_new specialization. `args` holds the + /// `__init__` args with slot 0 left empty; it is filled with `new_obj` + /// here. Enforces the `__init__() should return None` contract and + /// returns the constructed object. + fn specialization_run_init( &self, new_obj: PyObjectRef, init_func: &Py, - pos_args: Vec, + args: &mut [Option], vm: &VirtualMachine, ) -> PyResult { - let shim = self.specialization_new_init_cleanup_frame(vm); - let shim_result = vm.with_frame_untraced(shim.clone(), |shim| { - shim.with_exec(vm, |mut exec| exec.push_value(new_obj.clone())); - - let mut all_args = Vec::with_capacity(pos_args.len() + 1); - all_args.push(new_obj.clone()); - all_args.extend(pos_args); + args[0] = Some(new_obj.clone()); + let taken = args + .iter_mut() + .map(|slot| slot.take().expect("arg slot must be filled")); - let init_frame = init_func.prepare_exact_args_frame(all_args, vm); - let init_result = vm.run_frame(init_frame.clone()); - release_datastack_frame(&init_frame, vm); - let init_result = init_result?; - - shim.with_exec(vm, |mut exec| exec.push_value(init_result)); - match shim.run(vm)? { - ExecutionResult::Return(value) => Ok(value), - ExecutionResult::Yield(_) => unreachable!("_Py_InitCleanup shim cannot yield"), - } - }); - release_datastack_frame(&shim, vm); - shim_result + let init_result = init_func.invoke_prepared_exact_args(taken, vm)?; + + if !vm.is_none(&init_result) { + return Err(vm.new_type_error(format!( + "__init__() should return None, not '{:.200}'", + init_result.class().name() + ))); + } + Ok(new_obj) } #[inline(always)] @@ -1483,6 +3044,15 @@ impl ExecutingFrame<'_> { if stack_analysis::top_of_stack(cur_stack) == stack_analysis::Kind::Except as i64 && let Some(exc_obj) = val { + // An Except-typed stack slot is only produced by bytecode that + // also carries one of the opcodes scanned by + // `PyCode::has_exc_handling`; otherwise the save/restore that + // brackets this frame's exc_info is elided and this write would + // corrupt the shared exc_info slot. + debug_assert!( + self.code.has_exc_handling, + "unwinding an Except slot in a frame without exc-handling opcodes" + ); if vm.is_none(&exc_obj) { vm.set_exception(None); } else { @@ -1498,7 +3068,7 @@ impl ExecutingFrame<'_> { /// Matches `_PyEval_MonitorRaise` → `PY_MONITORING_EVENT_RAISE` → /// `sys_trace_exception_func` in legacy_tracing.c. fn fire_exception_trace(&self, exc: &PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult<()> { - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let exc_type: PyObjectRef = exc.class().to_owned().into(); let exc_value: PyObjectRef = exc.clone().into(); let exc_tb: PyObjectRef = exc @@ -1512,7 +3082,7 @@ impl ExecutingFrame<'_> { fn run(&mut self, vm: &VirtualMachine) -> PyResult { flame_guard!(format!( - "Frame::run({obj_name})", + "FrameObject::run({obj_name})", obj_name = self.code.obj_name )); // Execute until return or exception: @@ -1522,34 +3092,35 @@ impl ExecutingFrame<'_> { // Advance lasti past the current instruction BEFORE firing the // line event. This ensures that f_lineno (which reads // locations[lasti - 1]) returns the line of the instruction - // being traced, not the previous one. - self.update_lasti(|i| *i += 1); + // being traced, not the previous one. Stored from `idx` rather + // than read-modify-written, which would re-load what was just read. + self.lasti.store(idx as u32 + 1, Relaxed); // Fire 'line' trace event when line number changes. // Only fire if this frame has a per-frame trace function set // (frames entered before sys.settrace() have trace=None). // Skip RESUME – it should not generate user-visible line events. if vm.use_tracing.get() - && !vm.is_none(&self.object.trace.lock()) + && self.trace_is_set(vm) && !matches!( self.code.instructions.read_op(idx), Instruction::Resume { .. } | Instruction::InstrumentedResume ) && let Some((loc, _)) = self.code.locations.get(idx) - && loc.line.get() as u32 != *self.prev_line + && loc.line.get() as u32 != self.prev_line.get() { - *self.prev_line = loc.line.get() as u32; + self.prev_line.set(loc.line.get() as u32); vm.trace_event(crate::protocol::TraceEvent::Line, None)?; // Trace callback may have changed lasti via set_f_lineno. // Re-read and restart the loop from the new position. if self.lasti() != (idx as u32 + 1) { // set_f_lineno defers stack unwinding because we hold // the state mutex. Perform it now. - let pops = self.object.pending_stack_pops(); + let pops = self.pending_stack_pops(); if pops > 0 { - let from_stack = self.object.pending_unwind_from_stack(); + let from_stack = self.pending_unwind_from_stack(); self.unwind_stack_for_lineno(pops as usize, from_stack, vm); - self.object.set_pending_stack_pops(0); + self.set_pending_stack_pops(0); } arg_state.reset(); continue; @@ -1560,22 +3131,30 @@ impl ExecutingFrame<'_> { let mut do_extend_arg = false; let caches = op.cache_entries(); - // Update prev_line only when tracing or monitoring is active. - // When neither is enabled, prev_line is stale but unused. - if vm.use_tracing.get() { - if !matches!( - op.into(), - Opcode::Resume | Opcode::ExtendedArg | Opcode::InstrumentedLine - ) && let Some((loc, _)) = self.code.locations.get(idx) - { - *self.prev_line = loc.line.get() as u32; - } + // Always update prev_line so f_lineno returns the correct line + // even when the frame is observed mid-call (e.g. sys._getframe, + // warnings.warn). The lookup is a simple array index, so the + // cost is negligible. + // Update prev_line for f_lineno. Skip RESUME, ExtendedArg, + // and InstrumentedLine (it manages prev_line in its own handler; + // updating here first would defeat LINE de-duplication). + // Other instrumented opcodes update prev_line via + // execute_instrumented. + if !matches!( + op.into(), + Opcode::Resume | Opcode::ExtendedArg | Opcode::InstrumentedLine + ) && !op.is_instrumented() + && let Some((loc, _)) = self.code.locations.get(idx) + { + self.prev_line.set(loc.line.get() as u32); + } + if vm.use_tracing.get() { // Fire 'opcode' trace event for sys.settrace when f_trace_opcodes // is set. Skip RESUME and ExtendedArg // (_Py_call_instrumentation_instruction). - if !vm.is_none(&self.object.trace.lock()) - && *self.object.trace_opcodes.lock() + if self.trace_is_set(vm) + && self.trace_opcodes_is_set() && !matches!( op.into(), Opcode::Resume | Opcode::InstrumentedResume | Opcode::ExtendedArg @@ -1585,39 +3164,45 @@ impl ExecutingFrame<'_> { } } - if vm.eval_breaker_tripped() - && let Err(exception) = vm.check_signals() - { - #[cold] - fn handle_signal_exception( - frame: &mut ExecutingFrame<'_>, - exception: PyBaseExceptionRef, - idx: usize, - vm: &VirtualMachine, - ) -> FrameResult { - if let Some((loc, _end_loc)) = frame.code.locations.get(idx) { - let next = exception.__traceback__(); - let new_traceback = PyTraceback::new( - next, - frame.object.to_owned(), - idx as u32 * 2, - loc.line, - ); - exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); - } - vm.contextualize_exception(&exception); - frame.unwind_blocks(vm, UnwindReason::Raising { exception }) - } - match handle_signal_exception(self, exception, idx, vm) { - Ok(None) => {} - Ok(Some(value)) => { - break Ok(value); + #[cfg_attr(not(feature = "threading"), allow(clippy::collapsible_if))] + if vm.eval_breaker_tripped() { + if let Err(exception) = vm.check_signals() { + #[cold] + fn handle_signal_exception( + frame: &mut ExecutingFrame<'_>, + exception: PyBaseExceptionRef, + idx: usize, + vm: &VirtualMachine, + ) -> FrameResult { + if let Some((loc, _end_loc)) = frame.code.locations.get(idx) { + let next = exception.__traceback__(); + let new_traceback = PyTraceback::new( + next, + frame.frame_object(vm), + idx as u32 * 2, + loc.line, + ); + exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); + } + vm.contextualize_exception(&exception); + frame.unwind_blocks(vm, UnwindReason::Raising { exception }) } - Err(exception) => { - break Err(exception); + match handle_signal_exception(self, exception, idx, vm) { + Ok(None) => {} + Ok(Some(value)) => { + break Ok(value); + } + Err(exception) => { + break Err(exception); + } } + continue; } - continue; + // Run a scheduled automatic collection here — a safepoint with + // no interpreter locks held — instead of synchronously inside + // the allocation that tripped the threshold. + #[cfg(feature = "threading")] + vm.run_scheduled_gc(); } let lasti_before = self.lasti(); let result = self.execute_instruction(op, arg, &mut do_extend_arg, vm); @@ -1662,7 +3247,7 @@ impl ExecutingFrame<'_> { let new_traceback = PyTraceback::new( next, - frame.object.to_owned(), + frame.frame_object(vm), idx as u32 * 2, loc.line, ); @@ -1773,7 +3358,9 @@ impl ExecutingFrame<'_> { // The traceback was created with the correct lasti when exception // was first raised, but frame.lasti may have changed during cleanup if let Some(tb) = exception.__traceback__() - && core::ptr::eq::>(&*tb.frame, self.object) + && self.iframe().frame_obj().is_some_and(|fo| { + core::ptr::eq::>(&*tb.frame, fo) + }) { // This traceback entry is for this frame - restore its lasti // tb.lasti is in bytes (idx * 2), convert back to instruction index @@ -1862,12 +3449,8 @@ impl ExecutingFrame<'_> { if idx < self.code.locations.len() { let (loc, _end_loc) = self.code.locations[idx]; let next = err.__traceback__(); - let new_traceback = PyTraceback::new( - next, - self.object.to_owned(), - idx as u32 * 2, - loc.line, - ); + let new_traceback = + PyTraceback::new(next, self.frame_object(vm), idx as u32 * 2, loc.line); err.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); } @@ -1875,7 +3458,7 @@ impl ExecutingFrame<'_> { vm.contextualize_exception(&err); return match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) { Ok(None) => { - *self.prev_line = 0; + self.prev_line.set(0); self.run(vm) } Ok(Some(result)) => Ok(result), @@ -1909,7 +3492,7 @@ impl ExecutingFrame<'_> { let next = err.__traceback__(); let new_traceback = PyTraceback::new( next, - self.object.to_owned(), + self.frame_object(vm), idx as u32 * 2, loc.line, ); @@ -1920,7 +3503,7 @@ impl ExecutingFrame<'_> { vm.contextualize_exception(&err); match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) { Ok(None) => { - *self.prev_line = 0; + self.prev_line.set(0); self.run(vm) } Ok(Some(result)) => Ok(result), @@ -1951,7 +3534,7 @@ impl ExecutingFrame<'_> { let (loc, _end_loc) = self.code.locations[idx]; let next = exception.__traceback__(); let new_traceback = - PyTraceback::new(next, self.object.to_owned(), idx as u32 * 2, loc.line); + PyTraceback::new(next, self.frame_object(vm), idx as u32 * 2, loc.line); exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx))); } @@ -1994,7 +3577,7 @@ impl ExecutingFrame<'_> { // Reset prev_line so that the first instruction in the handler // fires a LINE event. In CPython, gen_send_ex re-enters the // eval loop which reinitializes its local prev_instr tracker. - *self.prev_line = 0; + self.prev_line.set(0); self.run(vm) } Ok(Some(result)) => Ok(result), @@ -2045,43 +3628,7 @@ impl ExecutingFrame<'_> { /// Get the variable name for a localsplus index. fn localsplus_name(&self, idx: usize) -> &'static PyStrInterned { - use rustpython_compiler_core::bytecode::{CO_FAST_CELL, CO_FAST_FREE, CO_FAST_LOCAL}; - let nlocals = self.code.varnames.len(); - let kind = self.code.localspluskinds.get(idx).copied().unwrap_or(0); - if kind & CO_FAST_LOCAL != 0 { - // Merged cell or regular local: name is in varnames - self.code.varnames[idx] - } else if kind & CO_FAST_FREE != 0 { - // Free var: slots are at the end of localsplus - let nlocalsplus = self.code.localspluskinds.len(); - let nfrees = self.code.freevars.len(); - let free_start = nlocalsplus - nfrees; - self.code.freevars[idx - free_start] - } else if kind & CO_FAST_CELL != 0 { - // Non-merged cell: count how many non-merged cell slots are before - // this index to find the corresponding cellvars entry. - // Non-merged cellvars appear in their original order (skipping merged ones). - let nonmerged_pos = self.code.localspluskinds[nlocals..idx] - .iter() - .filter(|&&k| k == CO_FAST_CELL) - .count(); - // Skip merged cellvars to find the right one - let mut cv_idx = 0; - let mut nonmerged_count = 0; - for (i, name) in self.code.cellvars.iter().enumerate() { - let is_merged = self.code.varnames.contains(name); - if !is_merged { - if nonmerged_count == nonmerged_pos { - cv_idx = i; - break; - } - nonmerged_count += 1; - } - } - self.code.cellvars[cv_idx] - } else { - self.code.varnames[idx] - } + localsplus_name(self.code, idx) } /// Execute a single instruction. @@ -2094,7 +3641,7 @@ impl ExecutingFrame<'_> { vm: &VirtualMachine, ) -> FrameResult { flame_guard!(format!( - "Frame::execute_instruction({instruction:?} {arg:?})" + "FrameObject::execute_instruction({instruction:?} {arg:?})" )); #[cfg(feature = "vm-tracing-logging")] @@ -2351,9 +3898,7 @@ impl ExecutingFrame<'_> { let n = n.get(arg) as usize; if n > 0 { let closure = self - .object .func_obj - .as_ref() .and_then(|f| f.downcast_ref::()) .and_then(|f| f.closure.as_ref()); let nlocalsplus = self.code.localspluskinds.len(); @@ -2858,9 +4403,10 @@ impl ExecutingFrame<'_> { Ok(None) } Instruction::LoadSmallInt { i: idx } => { - // Push small integer (-5..=256) directly without constant table lookup - let value = vm.ctx.new_int(idx.get(arg) as i32); - self.push_value(value.into()); + // Cached small integers live for the whole Context, so the value stack can + // borrow them without touching the refcount. + let value = vm.ctx.cached_int(idx.get(arg) as i32); + unsafe { self.push_borrowed(value.as_object()) }; Ok(None) } Instruction::LoadDeref { i } => { @@ -3050,9 +4596,17 @@ impl ExecutingFrame<'_> { let subject = self.pop_value(); let nargs_val = nargs.get(arg) as usize; + let Some(cls_type) = cls.downcast_ref::() else { + return Err(vm.new_type_error("called match pattern must be a class")); + }; + // Only the error paths need the class name; compute it lazily so a + // successful match does not take the name lock or allocate. + let type_name = || cls_type.name().to_string(); + // Check if subject is an instance of cls if subject.is_instance(cls.as_ref(), vm)? { let mut extracted = vec![]; + let seen_attrs = PySet::default().into_ref(&vm.ctx); // Get __match_args__ for positional arguments if nargs > 0 if nargs_val > 0 { @@ -3066,12 +4620,7 @@ impl ExecutingFrame<'_> { Ok(tuple) => tuple, Err(match_args) => { // __match_args__ must be a tuple - // Get type names for error message - let type_name = cls - .downcast::() - .ok() - .and_then(|t| t.__name__(vm).to_str().map(str::to_owned)) - .unwrap_or_else(|| String::from("?")); + let type_name = type_name(); let match_args_type_name = match_args.class().__name__(vm); return Err(vm.new_type_error(format!( "{type_name}.__match_args__ must be a tuple (got {match_args_type_name})" @@ -3081,9 +4630,12 @@ impl ExecutingFrame<'_> { // Check if we have enough match args if match_args.len() < nargs_val { + let type_name = type_name(); + let plural = if match_args.len() == 1 { "" } else { "s" }; return Err(vm.new_type_error(format!( - "class pattern accepts at most {} positional sub-patterns ({} given)", + "{type_name}() accepts {} positional sub-pattern{} ({} given)", match_args.len(), + plural, nargs_val ))); } @@ -3094,11 +4646,20 @@ impl ExecutingFrame<'_> { let attr_name_str = match attr_name.downcast_ref::() { Some(s) => s, None => { - return Err(vm.new_type_error( - "__match_args__ elements must be strings", - )); + let attr_type_name = attr_name.class().name(); + return Err(vm.new_type_error(format!( + "__match_args__ elements must be strings (got {attr_type_name})" + ))); } }; + if seen_attrs.__contains__(attr_name.as_object(), vm)? { + let type_name = type_name(); + let attr_repr = attr_name.as_object().repr(vm)?; + return Err(vm.new_type_error(format!( + "{type_name}() got multiple sub-patterns for attribute {attr_repr}" + ))); + } + seen_attrs.add(attr_name.clone(), vm)?; match subject.get_attr(attr_name_str, vm) { Ok(value) => extracted.push(value), Err(e) @@ -3115,9 +4676,8 @@ impl ExecutingFrame<'_> { // No __match_args__, check if this is a type with MATCH_SELF behavior // For built-in types like bool, int, str, list, tuple, dict, etc. // they match the subject itself as the single positional argument - let is_match_self_type = cls - .downcast::() - .is_ok_and(|t| t.slots.flags.contains(PyTypeFlags::_MATCH_SELF)); + let is_match_self_type = + cls_type.slots.flags.contains(PyTypeFlags::_MATCH_SELF); if is_match_self_type { if nargs_val == 1 { @@ -3125,16 +4685,18 @@ impl ExecutingFrame<'_> { extracted.push(subject.clone()); } else if nargs_val > 1 { // Too many positional arguments for MATCH_SELF - return Err(vm.new_type_error( - "class pattern accepts at most 1 positional sub-pattern for MATCH_SELF types", - )); + let type_name = type_name(); + return Err(vm.new_type_error(format!( + "{type_name}() accepts 1 positional sub-pattern ({nargs_val} given)" + ))); } } else { // No __match_args__ and not a MATCH_SELF type if nargs_val > 0 { - return Err(vm.new_type_error( - "class pattern defines no positional sub-patterns (__match_args__ missing)", - )); + let type_name = type_name(); + return Err(vm.new_type_error(format!( + "{type_name}() accepts 0 positional sub-patterns ({nargs_val} given)" + ))); } } } @@ -3143,6 +4705,14 @@ impl ExecutingFrame<'_> { // Extract keyword attributes for name in kwd_attrs { let name_str = name.downcast_ref::().unwrap(); + if seen_attrs.__contains__(name_str.as_object(), vm)? { + let type_name = type_name(); + let attr_repr = name.as_object().repr(vm)?; + return Err(vm.new_type_error(format!( + "{type_name}() got multiple sub-patterns for attribute {attr_repr}" + ))); + } + seen_attrs.add(name.clone(), vm)?; match subject.get_attr(name_str, vm) { Ok(value) => extracted.push(value), Err(e) if e.fast_isinstance(vm.ctx.exceptions.attribute_error) => { @@ -3166,10 +4736,14 @@ impl ExecutingFrame<'_> { let subject = self.nth_value(1); // stack[-2] // Check if subject is a mapping and extract values for keys - if subject.class().slots.flags.contains(PyTypeFlags::MAPPING) { + if subject + .class() + .has_patma_collection_flag(PyTypeFlags::MAPPING) + { let keys = keys_tuple.downcast_ref::().unwrap(); let mut values = Vec::new(); let mut all_match = true; + let seen_keys = PySet::default().into_ref(&vm.ctx); // We use the two argument form of map.get(key, default) for two reasons: // - Atomically check for a key and get its value without error handling. @@ -3186,22 +4760,35 @@ impl ExecutingFrame<'_> { .new_base_object(vm.ctx.types.object_type.to_owned(), None); for key in keys { + if seen_keys.__contains__(key.as_object(), vm)? { + return Err(vm.new_value_error(format!( + "mapping pattern checks duplicate key ({})", + key.as_object().repr(vm)? + ))); + } + seen_keys.add(key.as_object().to_owned(), vm)?; // value = map.get(key, dummy) - match get_method.call((key.as_object(), dummy.clone()), vm) { - Ok(value) => { - // if value == dummy: key not in map! - if value.is(&dummy) { - all_match = false; - break; - } - values.push(value); + { + let value = + get_method.call((key.as_object(), dummy.clone()), vm)?; + // if value == dummy: key not in map! + if value.is(&dummy) { + all_match = false; + break; } - Err(e) => return Err(e), + values.push(value); } } } else { // Fallback if .get() method is not available (shouldn't happen for mappings) for key in keys { + if seen_keys.__contains__(key.as_object(), vm)? { + return Err(vm.new_value_error(format!( + "mapping pattern checks duplicate key ({})", + key.as_object().repr(vm)? + ))); + } + seen_keys.add(key.as_object().to_owned(), vm)?; match subject.get_item(key.as_object(), vm) { Ok(value) => values.push(value), Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => { @@ -3231,7 +4818,9 @@ impl ExecutingFrame<'_> { let subject = self.pop_value(); // Check if the type has the MAPPING flag - let is_mapping = subject.class().slots.flags.contains(PyTypeFlags::MAPPING); + let is_mapping = subject + .class() + .has_patma_collection_flag(PyTypeFlags::MAPPING); self.push_value(subject); self.push_value(vm.ctx.new_bool(is_mapping).into()); @@ -3242,7 +4831,9 @@ impl ExecutingFrame<'_> { let subject = self.pop_value(); // Check if the type has the SEQUENCE flag - let is_sequence = subject.class().slots.flags.contains(PyTypeFlags::SEQUENCE); + let is_sequence = subject + .class() + .has_patma_collection_flag(PyTypeFlags::SEQUENCE); self.push_value(subject); self.push_value(vm.ctx.new_bool(is_sequence).into()); @@ -3275,7 +4866,7 @@ impl ExecutingFrame<'_> { // Python preserves exception tracebacks even after the exception is no longer // the "current exception". This is important for code that catches an exception, // stores it, and later inspects its traceback. - // Reference cycles (Exception → Traceback → Frame → locals) are handled by + // Reference cycles (Exception → Traceback → FrameObject → locals) are handled by // Python's garbage collector which can detect and break cycles. Ok(None) @@ -3318,22 +4909,15 @@ impl ExecutingFrame<'_> { } Instruction::RaiseVarargs { argc: kind } => self.execute_raise(vm, kind.get(arg)), Instruction::Resume { .. } | Instruction::ResumeCheck => { - // Lazy quickening: initialize adaptive counters on first execution - if !self.code.quickened.swap(true, atomic::Ordering::Relaxed) { + // Lazy quickening: initialize adaptive counters on first execution. + // Read before the swap so that the steady state — every call after + // the first — costs a load rather than a read-modify-write. + if !self.code.quickened.load(atomic::Ordering::Relaxed) + && !self.code.quickened.swap(true, atomic::Ordering::Relaxed) + { self.code.instructions.quicken(); atomic::fence(atomic::Ordering::Release); } - if self.monitoring_disabled_for_code(vm) { - let global_ver = vm - .state - .instrumentation_version - .load(atomic::Ordering::Acquire); - monitoring::instrument_code(self.code, 0); - self.code - .instrumentation_version - .store(global_ver, atomic::Ordering::Release); - return Ok(None); - } // Check if bytecode needs re-instrumentation let global_ver = vm .state @@ -3588,6 +5172,9 @@ impl ExecutingFrame<'_> { Ok(None) } Instruction::YieldValue { .. } => { + // The frame outlives this block from here on, so nothing it + // still holds may be a borrow of something else's slot. + self.localsplus.promote_stack(); debug_assert!( self.localsplus .stack_as_slice() @@ -3627,7 +5214,7 @@ impl ExecutingFrame<'_> { Ok(None) } PyIterReturn::StopIteration(value) => { - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value.clone()); self.fire_exception_trace(&stop_exc, vm)?; } @@ -3664,7 +5251,7 @@ impl ExecutingFrame<'_> { return Ok(None); } PyIterReturn::StopIteration(value) => { - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value.clone()); self.fire_exception_trace(&stop_exc, vm)?; } @@ -3682,7 +5269,7 @@ impl ExecutingFrame<'_> { Ok(None) } PyIterReturn::StopIteration(value) => { - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value.clone()); self.fire_exception_trace(&stop_exc, vm)?; } @@ -3706,7 +5293,7 @@ impl ExecutingFrame<'_> { let should_be_none = self.pop_value(); if !vm.is_none(&should_be_none) { return Err(vm.new_type_error(format!( - "__init__() should return None, not '{}'", + "__init__() should return None, not '{:.200}'", should_be_none.class().name() ))); } @@ -3797,7 +5384,7 @@ impl ExecutingFrame<'_> { if type_version != 0 && owner.class().tp_version_tag.load(Acquire) == type_version - && owner.dict().is_none() + && !owner.has_instance_dict() && let Some(func) = self.try_read_cached_descriptor(cache_base, type_version) { let owner = self.pop_value(); @@ -3817,18 +5404,11 @@ impl ExecutingFrame<'_> { let type_version = self.code.instructions.read_cache_u32(cache_base + 1); if type_version != 0 && owner.class().tp_version_tag.load(Acquire) == type_version { - // Check instance dict doesn't shadow the method - let shadowed = if let Some(dict) = owner.dict() { - match dict.get_item_opt(attr_name, vm) { - Ok(Some(_)) => true, - Ok(None) => false, - Err(_) => { - // Dict lookup error -> use safe path. - return self.load_attr_slow(vm, oparg); - } - } - } else { - false + // Check instance dict doesn't shadow the method. + let shadowed = match self.shadowing_instance_attr(cache_base, attr_name, vm) { + Ok(shadowed) => shadowed.is_some(), + // Dict lookup error -> use safe path. + Err(_) => return self.load_attr_slow(vm, oparg), }; if !shadowed @@ -3876,16 +5456,29 @@ impl ExecutingFrame<'_> { if type_version != 0 && owner.class().tp_version_tag.load(Acquire) == type_version && let Some(dict) = owner.dict() - && let Some(value) = dict.get_item_opt(attr_name, vm)? { - self.pop_value(); - if oparg.is_method() { - self.push_value(value); - self.push_value_opt(None); - } else { - self.push_value(value); + // Try the cached entry index first; a hit is an identity + // check on the entry key instead of a hash probe. + let hint = self.code.instructions.read_cache_u16(cache_base + 3); + if let Some((value, refreshed)) = + dict.get_item_opt_refresh_hint(attr_name, hint, vm)? + { + if let Some(new_hint) = refreshed { + unsafe { + self.code + .instructions + .write_cache_u16(cache_base + 3, new_hint); + } + } + self.pop_value(); + if oparg.is_method() { + self.push_value(value); + self.push_value_opt(None); + } else { + self.push_value(value); + } + return Ok(None); } - return Ok(None); } self.load_attr_slow(vm, oparg) @@ -3946,9 +5539,7 @@ impl ExecutingFrame<'_> { if type_version != 0 && owner.class().tp_version_tag.load(Acquire) == type_version { // Instance dict has priority — check if attr is shadowed - if let Some(dict) = owner.dict() - && let Some(value) = dict.get_item_opt(attr_name, vm)? - { + if let Some(value) = self.shadowing_instance_attr(cache_base, attr_name, vm)? { self.pop_value(); if oparg.is_method() { self.push_value(value); @@ -4044,7 +5635,8 @@ impl ExecutingFrame<'_> { debug_assert!(func.has_exact_argcount(2)); let owner = self.pop_value(); let attr_name = self.code.names[oparg.name_idx() as usize].to_owned().into(); - let result = func.invoke_exact_args(vec![owner, attr_name], vm)?; + let result = + func.invoke_exact_args_slots(&mut [Some(owner), Some(attr_name)], vm)?; self.push_value(result); return Ok(None); } @@ -4091,7 +5683,7 @@ impl ExecutingFrame<'_> { && self.specialization_has_datastack_space_for_func(vm, func) { let owner = self.pop_value(); - let result = func.invoke_exact_args(vec![owner], vm)?; + let result = func.invoke_exact_args_slots(&mut [Some(owner)], vm)?; self.push_value(result); return Ok(None); } @@ -4111,7 +5703,10 @@ impl ExecutingFrame<'_> { { self.pop_value(); // owner let value = self.pop_value(); - dict.set_item(attr_name, value, vm)?; + // The key was absent at specialization time, but this + // very store inserts it; hint learning makes later + // executions replace by entry index. + self.store_attr_dict_hinted(&dict, attr_name, value, cache_base, vm)?; return Ok(None); } self.store_attr(vm, attr_idx) @@ -4130,7 +5725,7 @@ impl ExecutingFrame<'_> { { self.pop_value(); // owner let value = self.pop_value(); - dict.set_item(attr_name, value, vm)?; + self.store_attr_dict_hinted(&dict, attr_name, value, cache_base, vm)?; return Ok(None); } self.store_attr(vm, attr_idx) @@ -4188,13 +5783,13 @@ impl ExecutingFrame<'_> { } // Specialized BINARY_OP opcodes Instruction::BinaryOpAddInt => { - self.execute_binary_op_int(vm, |a, b| a + b, bytecode::BinaryOperator::Add) + self.execute_binary_op_int(vm, Self::int_add, bytecode::BinaryOperator::Add) } Instruction::BinaryOpSubtractInt => { - self.execute_binary_op_int(vm, |a, b| a - b, bytecode::BinaryOperator::Subtract) + self.execute_binary_op_int(vm, Self::int_sub, bytecode::BinaryOperator::Subtract) } Instruction::BinaryOpMultiplyInt => { - self.execute_binary_op_int(vm, |a, b| a * b, bytecode::BinaryOperator::Multiply) + self.execute_binary_op_int(vm, Self::int_mul, bytecode::BinaryOperator::Multiply) } Instruction::BinaryOpAddFloat => { self.execute_binary_op_float(vm, |a, b| a + b, bytecode::BinaryOperator::Add) @@ -4213,8 +5808,8 @@ impl ExecutingFrame<'_> { b.downcast_ref_if_exact::(vm), ) { let result = a_str.as_wtf8().py_add(b_str.as_wtf8()); - self.pop_value(); - self.pop_value(); + self.pop_stackref(); + self.pop_stackref(); self.push_value(result.to_pyobject(vm)); Ok(None) } else { @@ -4222,8 +5817,12 @@ impl ExecutingFrame<'_> { } } Instruction::BinaryOpSubscrGetitem => { + let cache_base = self.lasti() as usize; + let type_version = self.code.instructions.read_cache_u32(cache_base + 1); let owner = self.nth_value(1); if !self.specialization_eval_frame_active(vm) + && type_version != 0 + && owner.class().tp_version_tag.load(Acquire) == type_version && let Some((func, func_version)) = owner.class().get_cached_getitem_for_specialization() && func.func_version() == func_version @@ -4232,7 +5831,7 @@ impl ExecutingFrame<'_> { debug_assert!(func.has_exact_argcount(2)); let sub = self.pop_value(); let owner = self.pop_value(); - let result = func.invoke_exact_args(vec![owner, sub], vm)?; + let result = func.invoke_exact_args_slots(&mut [Some(owner), Some(sub)], vm)?; self.push_value(result); return Ok(None); } @@ -4369,6 +5968,9 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_vectorcall(nargs, vm); + } let effective_nargs = nargs + u32::from(self_or_null_is_some); if !func.has_exact_argcount(effective_nargs) { return self.execute_call_vectorcall(nargs, vm); @@ -4379,19 +5981,28 @@ impl ExecutingFrame<'_> { if self.specialization_call_recursion_guard(vm) { return self.execute_call_vectorcall(nargs, vm); } - let pos_args: Vec = self.pop_multiple(nargs as usize).collect(); + if self.tailcall_enabled && !func.is_generator_like() { + self.tailcall_prepare_frame(nargs, self_or_null_is_some, vm); + return Ok(Some(ExecutionResult::TailCall)); + } + // Recursive path: pop args and call. + let base = usize::from(self_or_null_is_some); + let mut arg_buf = CallArgBuffer::new(nargs as usize + base); + let args = arg_buf.slots(); + for (slot, arg) in args[base..] + .iter_mut() + .zip(self.pop_multiple(nargs as usize)) + { + *slot = Some(arg); + } let self_or_null = self.pop_value_opt(); + debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some); + if self_or_null.is_some() { + args[0] = self_or_null; + } let callable = self.pop_value(); let func = callable.downcast_ref_if_exact::(vm).unwrap(); - let args = if let Some(self_val) = self_or_null { - let mut all_args = Vec::with_capacity(pos_args.len() + 1); - all_args.push(self_val); - all_args.extend(pos_args); - all_args - } else { - pos_args - }; - let result = func.invoke_exact_args(args, vm)?; + let result = func.invoke_exact_args_slots(args, vm)?; self.push_value(result); Ok(None) } else { @@ -4422,6 +6033,9 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_vectorcall(nargs, vm); + } if !func.has_exact_argcount(nargs + 1) { return self.execute_call_vectorcall(nargs, vm); } @@ -4431,14 +6045,28 @@ impl ExecutingFrame<'_> { if self.specialization_call_recursion_guard(vm) { return self.execute_call_vectorcall(nargs, vm); } - let pos_args: Vec = - self.pop_multiple(nargs as usize).collect(); + if self.tailcall_enabled && !func.is_generator_like() { + self.tailcall_prepare_bound_method_frame( + nargs, + bound_function, + bound_self, + vm, + ); + return Ok(Some(ExecutionResult::TailCall)); + } + // Recursive path: stage args without a per-call Vec. + // [bound_self, arg1, ..., argN] + let mut arg_buf = CallArgBuffer::new(nargs as usize + 1); + let args = arg_buf.slots(); + for (slot, arg) in + args[1..].iter_mut().zip(self.pop_multiple(nargs as usize)) + { + *slot = Some(arg); + } self.pop_value_opt(); // null (self_or_null) self.pop_value(); // callable (bound method) - let mut all_args = Vec::with_capacity(pos_args.len() + 1); - all_args.push(bound_self); - all_args.extend(pos_args); - let result = func.invoke_exact_args(all_args, vm)?; + args[0] = Some(bound_self); + let result = func.invoke_exact_args_slots(args, vm)?; self.push_value(result); return Ok(None); } @@ -4486,16 +6114,18 @@ impl ExecutingFrame<'_> { .as_ref() .is_some_and(|isinstance_callable| callable.is(isinstance_callable)) { - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); + // Stack: [callable, self_or_null, args...]; effective_nargs == 2, + // so the instance is either the first positional arg or self_or_null. + let cls = self.pop_value(); + let inst = if nargs == 2 { + let inst = self.pop_value(); + self.pop_value_opt(); // null + inst + } else { + self.pop_value() // self_or_null holds the instance + }; self.pop_value(); // callable - let mut all_args = Vec::with_capacity(2); - if let Some(self_val) = self_or_null { - all_args.push(self_val); - } - all_args.extend(pos_args); - let result = all_args[0].is_instance(&all_args[1], vm)?; + let result = inst.is_instance(&cls, vm)?; self.push_value(vm.ctx.new_bool(result).into()); return Ok(None); } @@ -4577,15 +6207,8 @@ impl ExecutingFrame<'_> { | PyMethodFlags::O | PyMethodFlags::KEYWORDS); if call_conv == PyMethodFlags::O && effective_nargs == 1 { - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let mut args_vec = Vec::with_capacity(effective_nargs as usize); - if let Some(self_val) = self_or_null { - args_vec.push(self_val); - } - args_vec.extend(pos_args); + let (callable, args_vec) = self.take_call_args(nargs as usize); + debug_assert_eq!(args_vec.len(), effective_nargs as usize); let result = callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?; self.push_value(result); @@ -4611,15 +6234,8 @@ impl ExecutingFrame<'_> { | PyMethodFlags::O | PyMethodFlags::KEYWORDS); if call_conv == PyMethodFlags::FASTCALL { - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let mut args_vec = Vec::with_capacity(effective_nargs as usize); - if let Some(self_val) = self_or_null { - args_vec.push(self_val); - } - args_vec.extend(pos_args); + let (callable, args_vec) = self.take_call_args(nargs as usize); + debug_assert_eq!(args_vec.len(), effective_nargs as usize); let result = callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?; self.push_value(result); @@ -4641,21 +6257,14 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_vectorcall(nargs, vm); + } if self.specialization_call_recursion_guard(vm) { return self.execute_call_vectorcall(nargs, vm); } - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let (args_vec, effective_nargs) = if let Some(self_val) = self_or_null { - let mut v = Vec::with_capacity(nargs_usize + 1); - v.push(self_val); - v.extend(pos_args); - (v, nargs_usize + 1) - } else { - (pos_args, nargs_usize) - }; + let (callable, args_vec) = self.take_call_args(nargs as usize); + let effective_nargs = args_vec.len(); let result = vectorcall_function(&callable, args_vec, effective_nargs, None, vm)?; self.push_value(result); @@ -4687,16 +6296,18 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_vectorcall(nargs, vm); + } if self.specialization_call_recursion_guard(vm) { return self.execute_call_vectorcall(nargs, vm); } let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - self.pop_value_opt(); // null (self_or_null) - self.pop_value(); // callable (bound method) let mut args_vec = Vec::with_capacity(nargs_usize + 1); args_vec.push(bound_self); - args_vec.extend(pos_args); + args_vec.extend(self.pop_multiple(nargs_usize)); + self.pop_value_opt(); // null (self_or_null) + self.pop_value(); // callable (bound method) let result = vectorcall_function( &bound_function, args_vec, @@ -4778,15 +6389,8 @@ impl ExecutingFrame<'_> { .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) { let func = descr.method.func; - let positional_args: Vec = - self.pop_multiple(nargs as usize).collect(); - let self_or_null = self.pop_value_opt(); - self.pop_value(); // callable - let mut all_args = Vec::with_capacity(total_nargs as usize); - if let Some(self_val) = self_or_null { - all_args.push(self_val); - } - all_args.extend(positional_args); + let (_callable, all_args) = self.take_call_args(nargs as usize); + debug_assert_eq!(all_args.len(), total_nargs as usize); let args = FuncArgs { args: all_args, kwargs: Default::default(), @@ -4825,15 +6429,8 @@ impl ExecutingFrame<'_> { .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) { let func = descr.method.func; - let positional_args: Vec = - self.pop_multiple(nargs as usize).collect(); - let self_or_null = self.pop_value_opt(); - self.pop_value(); // callable - let mut all_args = Vec::with_capacity(total_nargs as usize); - if let Some(self_val) = self_or_null { - all_args.push(self_val); - } - all_args.extend(positional_args); + let (_callable, all_args) = self.take_call_args(nargs as usize); + debug_assert_eq!(all_args.len(), total_nargs as usize); let args = FuncArgs { args: all_args, kwargs: Default::default(), @@ -4872,15 +6469,8 @@ impl ExecutingFrame<'_> { .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) { let func = descr.method.func; - let positional_args: Vec = - self.pop_multiple(nargs as usize).collect(); - let self_or_null = self.pop_value_opt(); - self.pop_value(); // callable - let mut all_args = Vec::with_capacity(total_nargs as usize); - if let Some(self_val) = self_or_null { - all_args.push(self_val); - } - all_args.extend(positional_args); + let (_callable, all_args) = self.take_call_args(nargs as usize); + debug_assert_eq!(all_args.len(), total_nargs as usize); let args = FuncArgs { args: all_args, kwargs: Default::default(), @@ -4897,22 +6487,9 @@ impl ExecutingFrame<'_> { if let Some(cls) = callable.downcast_ref::() && cls.slots.vectorcall.load().is_some() { - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let self_is_some = self_or_null.is_some(); - let mut args_vec = Vec::with_capacity(nargs_usize + usize::from(self_is_some)); - if let Some(self_val) = self_or_null { - args_vec.push(self_val); - } - args_vec.extend(pos_args); - let result = callable.vectorcall( - args_vec, - nargs_usize + usize::from(self_is_some), - None, - vm, - )?; + let (callable, args_vec) = self.take_call_args(nargs as usize); + let effective_nargs = args_vec.len(); + let result = callable.vectorcall(args_vec, effective_nargs, None, vm)?; self.push_value(result); return Ok(None); } @@ -4934,38 +6511,36 @@ impl ExecutingFrame<'_> { && cached_version != 0 && let Some(cls) = callable.downcast_ref::() && cls.tp_version_tag.load(Acquire) == cached_version - && let Some(init_func) = cls.get_cached_init_for_specialization(cached_version) + && let Some((init_func, init_func_version)) = + cls.get_cached_init_for_specialization(cached_version) + && init_func.func_version() == init_func_version + && init_func.has_exact_argcount(nargs + 1) && let Some(cls_alloc) = cls.slots.alloc.load() { - // Match CPython's `code->co_framesize + _Py_InitCleanup.co_framesize` - // shape, using RustPython's datastack-backed frame size - // equivalent for the extra shim frame. - let init_cleanup_stack_bytes = - datastack_frame_size_bytes_for_code(&vm.ctx.init_cleanup_code) - .expect("_Py_InitCleanup shim is not a generator/coroutine"); - if !self.specialization_has_datastack_space_for_func_with_extra( - vm, - &init_func, - init_cleanup_stack_bytes, - ) { + // The specialization runs `__init__` directly with no + // interpreter-visible trampoline frame. Deopt when the + // datastack or recursion budget for the `__init__` frame is + // unavailable. + if !self.specialization_has_datastack_space_for_func(vm, &init_func) { return self.execute_call_vectorcall(nargs, vm); } - // CPython creates `_Py_InitCleanup` + `__init__` frames here. - // Keep the guard conservative and deopt when the effective - // recursion budget for those two frames is not available. - if self.specialization_call_recursion_guard_with_extra_frames(vm, 1) { + if self.specialization_call_recursion_guard(vm) { return self.execute_call_vectorcall(nargs, vm); } // Allocate object directly (tp_new == object.__new__, tp_alloc == generic). let cls_ref = cls.to_owned(); let new_obj = cls_alloc(cls_ref, 0, vm)?; - // Build args: [new_obj, arg1, ..., argN] - let pos_args: Vec = self.pop_multiple(nargs as usize).collect(); + // Stage args as [new_obj, arg1, ..., argN]; slot 0 is + // filled by the init runner. + let mut arg_buf = CallArgBuffer::new(nargs as usize + 1); + let args = arg_buf.slots(); + for (slot, arg) in args[1..].iter_mut().zip(self.pop_multiple(nargs as usize)) { + *slot = Some(arg); + } let _null = self.pop_value_opt(); // self_or_null (None) let _callable = self.pop_value(); // callable (type) - let result = self - .specialization_run_init_cleanup_shim(new_obj, &init_func, pos_args, vm)?; + let result = self.specialization_run_init(new_obj, &init_func, args, vm)?; self.push_value(result); return Ok(None); } @@ -4999,15 +6574,8 @@ impl ExecutingFrame<'_> { .is_some_and(|self_obj| self_obj.class().is(descr.objclass)) { let func = descr.method.func; - let positional_args: Vec = - self.pop_multiple(nargs as usize).collect(); - let self_or_null = self.pop_value_opt(); - self.pop_value(); // callable - let mut all_args = Vec::with_capacity(total_nargs as usize); - if let Some(self_val) = self_or_null { - all_args.push(self_val); - } - all_args.extend(positional_args); + let (_callable, all_args) = self.take_call_args(nargs as usize); + debug_assert_eq!(all_args.len(), total_nargs as usize); let args = FuncArgs { args: all_args, kwargs: Default::default(), @@ -5036,15 +6604,8 @@ impl ExecutingFrame<'_> { | PyMethodFlags::O | PyMethodFlags::KEYWORDS); if call_conv == (PyMethodFlags::FASTCALL | PyMethodFlags::KEYWORDS) { - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let mut args_vec = Vec::with_capacity(effective_nargs as usize); - if let Some(self_val) = self_or_null { - args_vec.push(self_val); - } - args_vec.extend(pos_args); + let (callable, args_vec) = self.take_call_args(nargs as usize); + debug_assert_eq!(args_vec.len(), effective_nargs as usize); let result = callable.vectorcall(args_vec, effective_nargs as usize, None, vm)?; self.push_value(result); @@ -5068,22 +6629,13 @@ impl ExecutingFrame<'_> { { return self.execute_call_vectorcall(nargs, vm); } - let nargs_usize = nargs as usize; - let pos_args: Vec = self.pop_multiple(nargs_usize).collect(); - let self_or_null = self.pop_value_opt(); - let callable = self.pop_value(); - let mut args_vec = - Vec::with_capacity(nargs_usize + usize::from(self_or_null_is_some)); - if let Some(self_val) = self_or_null { - args_vec.push(self_val); - } - args_vec.extend(pos_args); - let result = callable.vectorcall( - args_vec, - nargs_usize + usize::from(self_or_null_is_some), - None, - vm, - )?; + let (callable, args_vec) = self.take_call_args(nargs as usize); + debug_assert_eq!( + args_vec.len(), + nargs as usize + usize::from(self_or_null_is_some) + ); + let effective_nargs = args_vec.len(); + let result = callable.vectorcall(args_vec, effective_nargs, None, vm)?; self.push_value(result); Ok(None) } @@ -5101,6 +6653,9 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_kw_vectorcall(nargs, vm); + } if self.specialization_call_recursion_guard(vm) { return self.execute_call_kw_vectorcall(nargs, vm); } @@ -5159,6 +6714,9 @@ impl ExecutingFrame<'_> { && func.func_version() == cached_version && cached_version != 0 { + if func.is_jitted() { + return self.execute_call_kw_vectorcall(nargs, vm); + } let nargs_usize = nargs as usize; let kwarg_names_obj = self.pop_value(); let kwarg_names_tuple = kwarg_names_obj @@ -5354,14 +6912,16 @@ impl ExecutingFrame<'_> { a.downcast_ref_if_exact::(vm), b.downcast_ref_if_exact::(vm), ) && let (Some(a_val), Some(b_val)) = ( - specialization_compact_int_value(a_int, vm), - specialization_compact_int_value(b_int, vm), + specialization_compact_int_value(a_int), + specialization_compact_int_value(b_int), ) { let op = self.compare_op_from_arg(arg); let result = op.eval_ord(a_val.cmp(&b_val)); - self.pop_value(); - self.pop_value(); - self.push_value(vm.ctx.new_bool(result).into()); + self.pop_stackref(); + self.pop_stackref(); + if !self.try_fused_compare_int_jump(result, vm) { + self.push_value(vm.ctx.new_bool(result).into()); + } Ok(None) } else { self.execute_compare(vm, arg) @@ -5397,10 +6957,13 @@ impl ExecutingFrame<'_> { b.downcast_ref_if_exact::(vm), ) { let op = self.compare_op_from_arg(arg); - if op != PyComparisonOp::Eq && op != PyComparisonOp::Ne { + // The same two shortcuts the unspecialized comparison takes: + // one object is equal to itself, and equality answers two + // strings of different length without reading either. + let Some(result) = op.eval_eq(|| a.is(b) || a_str.as_wtf8() == b_str.as_wtf8()) + else { return self.execute_compare(vm, arg); - } - let result = op.eval_ord(a_str.as_wtf8().cmp(b_str.as_wtf8())); + }; self.pop_value(); self.pop_value(); self.push_value(vm.ctx.new_bool(result).into()); @@ -5664,7 +7227,7 @@ impl ExecutingFrame<'_> { self.push_value(value); } Ok(PyIterReturn::StopIteration(value)) => { - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value); self.fire_exception_trace(&stop_exc, vm)?; } @@ -5684,17 +7247,16 @@ impl ExecutingFrame<'_> { // Keep specialized opcode on guard miss (JUMP_TO_PREDICTED behavior). let cached_version = self.code.instructions.read_cache_u16(cache_base + 1); let cached_index = self.code.instructions.read_cache_u16(cache_base + 3); - if let Ok(current_version) = u16::try_from(self.globals.version()) - && cached_version == current_version + if cached_version != 0 + && let Some(x) = self + .globals + .get_item_by_index_and_keys_version(cached_version, cached_index) { - let name = self.code.names[(oparg >> 1) as usize]; - if let Some(x) = self.globals.get_item_opt_hint(name, cached_index, vm)? { - self.push_value(x); - if (oparg & 1) != 0 { - self.push_value_opt(None); - } - return Ok(None); + self.push_value(x); + if (oparg & 1) != 0 { + self.push_value_opt(None); } + return Ok(None); } let name = self.code.names[(oparg >> 1) as usize]; let x = self.load_global_or_builtin(name, vm)?; @@ -5710,20 +7272,19 @@ impl ExecutingFrame<'_> { let cached_globals_ver = self.code.instructions.read_cache_u16(cache_base + 1); let cached_builtins_ver = self.code.instructions.read_cache_u16(cache_base + 2); let cached_index = self.code.instructions.read_cache_u16(cache_base + 3); - if let Ok(current_globals_ver) = u16::try_from(self.globals.version()) + if cached_globals_ver != 0 + && cached_builtins_ver != 0 + && let Ok(current_globals_ver) = u16::try_from(self.globals.keys_version()) && cached_globals_ver == current_globals_ver && let Some(builtins_dict) = self.builtins.downcast_ref_if_exact::(vm) - && let Ok(current_builtins_ver) = u16::try_from(builtins_dict.version()) - && cached_builtins_ver == current_builtins_ver + && let Some(x) = builtins_dict + .get_item_by_index_and_keys_version(cached_builtins_ver, cached_index) { - let name = self.code.names[(oparg >> 1) as usize]; - if let Some(x) = builtins_dict.get_item_opt_hint(name, cached_index, vm)? { - self.push_value(x); - if (oparg & 1) != 0 { - self.push_value_opt(None); - } - return Ok(None); + self.push_value(x); + if (oparg & 1) != 0 { + self.push_value_opt(None); } + return Ok(None); } let name = self.code.names[(oparg >> 1) as usize]; let x = self.load_global_or_builtin(name, vm)?; @@ -5752,17 +7313,19 @@ impl ExecutingFrame<'_> { instruction.is_instrumented(), "execute_instrumented called with non-instrumented opcode {instruction:?}" ); - if self.monitoring_disabled_for_code(vm) { - let global_ver = vm - .state - .instrumentation_version - .load(atomic::Ordering::Acquire); - monitoring::instrument_code(self.code, 0); - self.code - .instrumentation_version - .store(global_ver, atomic::Ordering::Release); - self.update_lasti(|i| *i -= 1); - return Ok(None); + // Update prev_line for f_lineno. The main bytecode loop skips + // instrumented opcodes to avoid interfering with LINE event + // de-duplication in InstrumentedLine. Update here instead, except + // for RESUME (prev_line must stay 0 for the first LINE event) and + // InstrumentedLine (manages prev_line in its own handler). + if !matches!( + instruction, + Instruction::InstrumentedResume | Instruction::InstrumentedLine + ) { + let idx = self.lasti() as usize - 1; + if let Some((loc, _)) = self.code.locations.get(idx) { + self.prev_line.set(loc.line.get() as u32); + } } self.monitoring_mask = vm.state.monitoring_events.load(); match instruction { @@ -5809,6 +7372,7 @@ impl ExecutingFrame<'_> { self.unwind_blocks(vm, UnwindReason::Returning { value }) } Instruction::InstrumentedYieldValue => { + self.localsplus.promote_stack(); debug_assert!( self.localsplus .stack_as_slice() @@ -6066,8 +7630,8 @@ impl ExecutingFrame<'_> { // Fire LINE event only if line changed if let Some((loc, _)) = self.code.locations.get(idx) { let line = loc.line.get() as u32; - if line != *self.prev_line && line > 0 { - *self.prev_line = line; + if line != self.prev_line.get() && line > 0 { + self.prev_line.set(line); monitoring::fire_line(vm, self.code, offset, line)?; } } @@ -6077,6 +7641,12 @@ impl ExecutingFrame<'_> { monitoring::fire_instruction(vm, self.code, offset)?; } + // Update prev_line for f_lineno since the bytecode loop's + // update skips all instrumented opcodes. + if let Some((loc, _)) = self.code.locations.get(idx) { + self.prev_line.set(loc.line.get() as u32); + } + // Re-dispatch to the real original opcode let original_op = Instruction::try_from(real_op_byte) .expect("invalid opcode in side-table chain"); @@ -6158,7 +7728,7 @@ impl ExecutingFrame<'_> { if let Some(builtins_dict) = self.builtins_dict { // Fast path: both globals and builtins are exact dicts // SAFETY: builtins_dict is only set when globals is also exact dict - let globals_exact = unsafe { PyExact::ref_unchecked(self.globals.as_ref()) }; + let globals_exact = unsafe { PyExact::ref_unchecked(self.globals) }; globals_exact .get_chain_exact(builtins_dict, name, vm)? .ok_or_else(|| { @@ -6179,7 +7749,7 @@ impl ExecutingFrame<'_> { } } - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn import(&mut self, vm: &VirtualMachine, module_name: Option<&Py>) -> PyResult<()> { let module_name = module_name.unwrap_or(vm.ctx.empty_str); let top = self.pop_value(); @@ -6195,7 +7765,7 @@ impl ExecutingFrame<'_> { Ok(()) } - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn import_from(&mut self, vm: &VirtualMachine, idx: bytecode::NameIdx) -> PyResult { let module = self.top_value(); let name = self.code.names[idx as usize]; @@ -6297,7 +7867,7 @@ impl ExecutingFrame<'_> { Err(err) } - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn import_star(&mut self, vm: &VirtualMachine) -> PyResult<()> { let module = self.pop_value(); @@ -6350,7 +7920,7 @@ impl ExecutingFrame<'_> { /// The reason for unwinding gives a hint on what to do when /// unwinding a block. /// Optionally returns an exception. - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn unwind_blocks(&mut self, vm: &VirtualMachine, reason: UnwindReason) -> FrameResult { // use exception table for exception handling match reason { @@ -6489,11 +8059,13 @@ impl ExecutingFrame<'_> { let func_str = Self::object_function_str(callable, vm); Self::iterate_mapping_keys(vm, &kw_obj, &func_str, |key| { + // `PyStr`, not `PyUtf8Str`: CPython only checks that the key is a + // `str`, not that it is valid UTF-8, so surrogate keys are accepted. let key_str = key - .downcast_ref::() + .downcast_ref::() .ok_or_else(|| vm.new_type_error("keywords must be strings"))?; let value = kw_obj.get_item(&*key, vm)?; - kwargs.insert(key_str.as_str().to_owned(), value); + kwargs.insert(key_str.as_wtf8().to_owned(), value); Ok(()) })? }; @@ -6845,7 +8417,7 @@ impl ExecutingFrame<'_> { } } - fn execute_unpack_ex(&mut self, vm: &VirtualMachine, before: u8, after: u8) -> FrameResult { + fn execute_unpack_ex(&mut self, vm: &VirtualMachine, before: u8, after: u32) -> FrameResult { let (before, after) = (before as usize, after as usize); let value = self.pop_value(); let not_iterable = value.class().slots.iter.load().is_none() @@ -6955,7 +8527,7 @@ impl ExecutingFrame<'_> { self.push_value(vm.ctx.new_int(value).into()); return Ok(true); } - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(None); self.fire_exception_trace(&stop_exc, vm)?; } @@ -6974,7 +8546,7 @@ impl ExecutingFrame<'_> { Ok(PyIterReturn::StopIteration(value)) => { // Fire 'exception' trace event for StopIteration, matching // FOR_ITER's inline call to _PyEval_MonitorRaise. - if vm.use_tracing.get() && !vm.is_none(&self.object.trace.lock()) { + if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value); self.fire_exception_trace(&stop_exc, vm)?; } @@ -7009,7 +8581,7 @@ impl ExecutingFrame<'_> { .expect("Stack value should be code object"); // Create function with minimal attributes - let func_obj = PyFunction::new(code_obj, self.globals.clone(), vm)?.into_pyobject(vm); + let func_obj = PyFunction::new(code_obj, self.globals.to_owned(), vm)?.into_pyobject(vm); self.push_value(func_obj); Ok(None) @@ -7044,20 +8616,20 @@ impl ExecutingFrame<'_> { Ok(None) } - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn execute_bin_op(&mut self, vm: &VirtualMachine, op: bytecode::BinaryOperator) -> FrameResult { let b_ref = &self.pop_value(); let a_ref = &self.pop_value(); let value = match op { - // BINARY_OP_ADD_INT / BINARY_OP_SUBTRACT_INT fast paths: - // bypass binary_op1 dispatch for exact int types, use i64 arithmetic - // when possible to avoid BigInt heap allocation. + // Exact-int fast paths for +, -, *, //, %: bypass binary_op1 + // dispatch and use i64 arithmetic when possible to avoid BigInt + // heap allocation, falling back to the slow path otherwise. bytecode::BinaryOperator::Add | bytecode::BinaryOperator::InplaceAdd => { if let (Some(a), Some(b)) = ( a_ref.downcast_ref_if_exact::(vm), b_ref.downcast_ref_if_exact::(vm), ) { - Ok(self.int_add(a.as_bigint(), b.as_bigint(), vm)) + Ok(Self::int_add(a, b, vm)) } else if matches!(op, bytecode::BinaryOperator::Add) { vm._add(a_ref, b_ref) } else { @@ -7069,32 +8641,65 @@ impl ExecutingFrame<'_> { a_ref.downcast_ref_if_exact::(vm), b_ref.downcast_ref_if_exact::(vm), ) { - Ok(self.int_sub(a.as_bigint(), b.as_bigint(), vm)) + Ok(Self::int_sub(a, b, vm)) } else if matches!(op, bytecode::BinaryOperator::Subtract) { vm._sub(a_ref, b_ref) } else { vm._isub(a_ref, b_ref) } } - bytecode::BinaryOperator::Multiply => vm._mul(a_ref, b_ref), + bytecode::BinaryOperator::Multiply | bytecode::BinaryOperator::InplaceMultiply => { + if let (Some(a), Some(b)) = ( + a_ref.downcast_ref_if_exact::(vm), + b_ref.downcast_ref_if_exact::(vm), + ) { + Ok(Self::int_mul(a, b, vm)) + } else if matches!(op, bytecode::BinaryOperator::Multiply) { + vm._mul(a_ref, b_ref) + } else { + vm._imul(a_ref, b_ref) + } + } bytecode::BinaryOperator::MatrixMultiply => vm._matmul(a_ref, b_ref), bytecode::BinaryOperator::Power => vm._pow(a_ref, b_ref, vm.ctx.none.as_object()), bytecode::BinaryOperator::TrueDivide => vm._truediv(a_ref, b_ref), - bytecode::BinaryOperator::FloorDivide => vm._floordiv(a_ref, b_ref), - bytecode::BinaryOperator::Remainder => vm._mod(a_ref, b_ref), + bytecode::BinaryOperator::FloorDivide + | bytecode::BinaryOperator::InplaceFloorDivide => { + if let (Some(a), Some(b)) = ( + a_ref.downcast_ref_if_exact::(vm), + b_ref.downcast_ref_if_exact::(vm), + ) && let Some(result) = Self::int_floordiv(a.as_bigint(), b.as_bigint(), vm) + { + Ok(result) + } else if matches!(op, bytecode::BinaryOperator::FloorDivide) { + vm._floordiv(a_ref, b_ref) + } else { + vm._ifloordiv(a_ref, b_ref) + } + } + bytecode::BinaryOperator::Remainder | bytecode::BinaryOperator::InplaceRemainder => { + if let (Some(a), Some(b)) = ( + a_ref.downcast_ref_if_exact::(vm), + b_ref.downcast_ref_if_exact::(vm), + ) && let Some(result) = Self::int_mod(a.as_bigint(), b.as_bigint(), vm) + { + Ok(result) + } else if matches!(op, bytecode::BinaryOperator::Remainder) { + vm._mod(a_ref, b_ref) + } else { + vm._imod(a_ref, b_ref) + } + } bytecode::BinaryOperator::Lshift => vm._lshift(a_ref, b_ref), bytecode::BinaryOperator::Rshift => vm._rshift(a_ref, b_ref), bytecode::BinaryOperator::Xor => vm._xor(a_ref, b_ref), bytecode::BinaryOperator::Or => vm._or(a_ref, b_ref), bytecode::BinaryOperator::And => vm._and(a_ref, b_ref), - bytecode::BinaryOperator::InplaceMultiply => vm._imul(a_ref, b_ref), bytecode::BinaryOperator::InplaceMatrixMultiply => vm._imatmul(a_ref, b_ref), bytecode::BinaryOperator::InplacePower => { vm._ipow(a_ref, b_ref, vm.ctx.none.as_object()) } bytecode::BinaryOperator::InplaceTrueDivide => vm._itruediv(a_ref, b_ref), - bytecode::BinaryOperator::InplaceFloorDivide => vm._ifloordiv(a_ref, b_ref), - bytecode::BinaryOperator::InplaceRemainder => vm._imod(a_ref, b_ref), bytecode::BinaryOperator::InplaceLshift => vm._ilshift(a_ref, b_ref), bytecode::BinaryOperator::InplaceRshift => vm._irshift(a_ref, b_ref), bytecode::BinaryOperator::InplaceXor => vm._ixor(a_ref, b_ref), @@ -7107,28 +8712,108 @@ impl ExecutingFrame<'_> { Ok(None) } - /// Int addition with i64 fast path to avoid BigInt heap allocation. + /// Int binary op with an i64 fast path to avoid BigInt heap allocation. + /// `checked` computes the i64 result; on `None` (either operand does not + /// fit i64, or the op overflows i64) it falls through to `fallback` on the + /// full BigInt values. Result boxing always goes through `new_int` so the + /// small-int cache is consulted identically. + #[inline] + fn int_fast_op( + a: &PyInt, + b: &PyInt, + vm: &VirtualMachine, + checked: fn(i64, i64) -> Option, + fallback: impl FnOnce(&BigInt, &BigInt) -> BigInt, + ) -> PyObjectRef { + if let (Some(av), Some(bv)) = (a.try_to_i64_fast(), b.try_to_i64_fast()) + && let Some(result) = checked(av, bv) + { + return vm.ctx.new_int(result).into(); + } + vm.ctx + .new_int(fallback(a.as_bigint(), b.as_bigint())) + .into() + } + + /// Int addition with i64 fast path to avoid BigInt heap allocation. + #[inline] + fn int_add(a: &PyInt, b: &PyInt, vm: &VirtualMachine) -> PyObjectRef { + Self::int_fast_op(a, b, vm, i64::checked_add, |a, b| a + b) + } + + /// Int subtraction with i64 fast path to avoid BigInt heap allocation. + #[inline] + fn int_sub(a: &PyInt, b: &PyInt, vm: &VirtualMachine) -> PyObjectRef { + Self::int_fast_op(a, b, vm, i64::checked_sub, |a, b| a - b) + } + + /// Int multiplication with i64 fast path to avoid BigInt heap allocation. + #[inline] + fn int_mul(a: &PyInt, b: &PyInt, vm: &VirtualMachine) -> PyObjectRef { + Self::int_fast_op(a, b, vm, i64::checked_mul, |a, b| a * b) + } + + /// Int divide/remainder i64 fast path. Returns `None` to signal the caller + /// to fall through to the slow path when either operand does not fit i64 + /// or `compute` reports a case it cannot handle (zero divisor or i64 + /// overflow). Result boxing goes through `new_int` so the small-int cache + /// is consulted identically. #[inline] - fn int_add(&self, a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> PyObjectRef { + fn int_div_fast_op( + a: &BigInt, + b: &BigInt, + vm: &VirtualMachine, + compute: fn(i64, i64) -> Option, + ) -> Option { use num_traits::ToPrimitive; - if let (Some(av), Some(bv)) = (a.to_i64(), b.to_i64()) - && let Some(result) = av.checked_add(bv) - { - return vm.ctx.new_int(result).into(); + let (av, bv) = (a.to_i64()?, b.to_i64()?); + compute(av, bv).map(|r| vm.ctx.new_int(r).into()) + } + + /// Floor division of two i64 values with floor (toward negative infinity) + /// semantics. `None` when `b == 0` or the quotient overflows i64 + /// (`i64::MIN / -1`). + #[inline] + fn floordiv_i64(a: i64, b: i64) -> Option { + if b == 0 { + return None; } - vm.ctx.new_int(a + b).into() + let q = a.checked_div(b)?; + let r = a % b; + Some(if r != 0 && (r < 0) != (b < 0) { + q - 1 + } else { + q + }) } - /// Int subtraction with i64 fast path to avoid BigInt heap allocation. + /// Remainder of two i64 values, taking the sign of the divisor. `None` + /// when `b == 0` or the operation overflows i64 (`i64::MIN % -1`). #[inline] - fn int_sub(&self, a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> PyObjectRef { - use num_traits::ToPrimitive; - if let (Some(av), Some(bv)) = (a.to_i64(), b.to_i64()) - && let Some(result) = av.checked_sub(bv) - { - return vm.ctx.new_int(result).into(); + fn mod_i64(a: i64, b: i64) -> Option { + if b == 0 { + return None; } - vm.ctx.new_int(a - b).into() + let r = a.checked_rem(b)?; + Some(if r != 0 && (r < 0) != (b < 0) { + r + b + } else { + r + }) + } + + /// Int floor division with i64 fast path. `None` falls through to the + /// slow path (bigint operands, zero divisor, or i64 overflow). + #[inline] + fn int_floordiv(a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> Option { + Self::int_div_fast_op(a, b, vm, Self::floordiv_i64) + } + + /// Int remainder with i64 fast path. `None` falls through to the slow + /// path (bigint operands, zero divisor, or i64 overflow). + #[inline] + fn int_mod(a: &BigInt, b: &BigInt, vm: &VirtualMachine) -> Option { + Self::int_div_fast_op(a, b, vm, Self::mod_i64) } #[cold] @@ -7286,7 +8971,7 @@ impl ExecutingFrame<'_> { Ok(!self._in(vm, needle, haystack)?) } - #[cfg_attr(feature = "flame-it", flame("Frame"))] + #[cfg_attr(feature = "flame-it", flame("FrameObject"))] fn execute_compare(&mut self, vm: &VirtualMachine, arg: bytecode::OpArg) -> FrameResult { let op = bytecode::ComparisonOperator::try_from(u32::from(arg)) .unwrap_or(bytecode::ComparisonOperator::Equal); @@ -7327,6 +9012,67 @@ impl ExecutingFrame<'_> { Ok(None) } + /// Store an instance attribute through the cached entry index at + /// `cache_base + 3`, refreshing the cache when the hint missed. + fn store_attr_dict_hinted( + &mut self, + dict: &Py, + attr_name: &'static PyStrInterned, + value: PyObjectRef, + cache_base: usize, + vm: &VirtualMachine, + ) -> PyResult<()> { + let hint = self.code.instructions.read_cache_u16(cache_base + 3); + if let Some(new_hint) = dict.set_item_with_hint(attr_name, hint, value, vm)? { + unsafe { + self.code + .instructions + .write_cache_u16(cache_base + 3, new_hint); + } + } + Ok(()) + } + + /// Shadow check for method/nondescriptor loads: return the instance + /// attribute shadowing the cached class attr, or `None` if not shadowed. + /// + /// A keys-version stamp of the instance dict is kept in the pointer cache + /// at `cache_base + 3`. While the dict reports the same stamp, its key + /// set is unchanged since the name was last verified absent, so the probe + /// is skipped. On a verified-absent probe the current stamp is recorded + /// for the next execution. + fn shadowing_instance_attr( + &self, + cache_base: usize, + attr_name: &'static PyStrInterned, + vm: &VirtualMachine, + ) -> PyResult> { + let stamp = self.code.instructions.read_cache_ptr(cache_base + 3); + // Take the stamp check first, on a borrowed dict: a hit is the whole + // fast path, and cloning the dict for it would cost more than the + // comparison it exists to make. + let stamped = self.top_value().with_instance_dict(|dict| { + dict.is_some_and(|d| stamp != 0 && stamp == d.keys_version() as usize) + }); + if stamped { + return Ok(None); + } + let Some(dict) = self.top_value().dict() else { + return Ok(None); + }; + // Take the stamp before probing so it attests the probed key set. + let stamp = dict.assign_keys_version(vm); + if let Some(value) = dict.get_item_opt(attr_name, vm)? { + return Ok(Some(value)); + } + unsafe { + self.code + .instructions + .write_cache_ptr(cache_base + 3, stamp as usize); + } + Ok(None) + } + /// Read a cached descriptor pointer and validate it against the expected /// type version, using a lock-free double-check pattern: /// 1. read pointer → incref (try_to_owned) @@ -7477,22 +9223,33 @@ impl ExecutingFrame<'_> { return; } + // Capture the version before inspecting getattro and the MRO so a + // concurrently installed __getattribute__/__getattr__ invalidates the + // version this specialization is cached against. + let type_version = cls.version_for_specialization(_vm); + if type_version == 0 { + unsafe { + self.code.instructions.write_adaptive_counter( + cache_base, + bytecode::adaptive_counter_backoff( + self.code.instructions.read_adaptive_counter(cache_base), + ), + ); + } + return; + } + // Only specialize if getattro is the default (PyBaseObject::getattro) - let is_default_getattro = cls - .slots - .getattro - .load() - .is_some_and(|f| f as usize == PyBaseObject::getattro as *const () as usize); + let is_default_getattro = cls.slots.getattro.load().is_some_and(|f| { + crate::types::fn_addr(f) + == crate::types::fn_addr(PyBaseObject::getattro as crate::types::GetattroFunc) + }); if !is_default_getattro { - let mut type_version = cls.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = cls.assign_version_tag(); - } - if type_version != 0 - && !oparg.is_method() + let getattribute = cls.get_attr(identifier!(_vm, __getattribute__)); + if !oparg.is_method() && !self.specialization_eval_frame_active(_vm) && cls.get_attr(identifier!(_vm, __getattr__)).is_none() - && let Some(getattribute) = cls.get_attr(identifier!(_vm, __getattribute__)) + && let Some(getattribute) = getattribute && let Some(func) = getattribute.downcast_ref_if_exact::(_vm) && func.can_specialize_call(2) { @@ -7524,24 +9281,6 @@ impl ExecutingFrame<'_> { return; } - // Get or assign type version - let mut type_version = cls.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = cls.assign_version_tag(); - } - if type_version == 0 { - // Version counter overflow — backoff to avoid re-attempting every execution - unsafe { - self.code.instructions.write_adaptive_counter( - cache_base, - bytecode::adaptive_counter_backoff( - self.code.instructions.read_adaptive_counter(cache_base), - ), - ); - } - return; - } - let attr_name = self.code.names[oparg.name_idx() as usize]; // Match CPython: only specialize module attribute loads when the @@ -7574,7 +9313,6 @@ impl ExecutingFrame<'_> { return; } - // Look up attr in class via MRO let cls_attr = cls.get_attr(attr_name); let class_has_dict = cls.slots.flags.has_feature(PyTypeFlags::HAS_DICT); @@ -7624,9 +9362,14 @@ impl ExecutingFrame<'_> { if has_data_descr { // Check for member descriptor (slot access) + // The slot offset only means anything on the layout the + // descriptor was defined for; the specialized instruction + // guards on the type version alone, so what descr_get() + // checks on every access has to be checked here instead. if let Some(ref descr) = cls_attr && let Some(member_descr) = descr.downcast_ref::() && let MemberGetter::Offset(offset) = member_descr.member.getter + && cls.fast_issubclass(&member_descr.common.typ) { unsafe { self.code @@ -7687,10 +9430,12 @@ impl ExecutingFrame<'_> { // attribute is missing on both the class and the current // instance, keep the generic opcode and just enter // cooldown instead of specializing a repeated miss path. - let has_instance_attr = if let Some(dict) = obj.dict() { - match dict.get_item_opt(attr_name, _vm) { - Ok(Some(_)) => true, - Ok(None) => false, + // A present attribute always specializes; when no entry + // index is representable the hint degrades to 0 and the + // handler simply keeps taking its full-probe fallback. + let instance_attr_hint = if let Some(dict) = obj.dict() { + match dict.get_item_opt_refresh_hint(attr_name, 0, _vm) { + Ok(present) => present.map(|(_, refreshed)| refreshed.unwrap_or(0)), Err(_) => { unsafe { self.code.instructions.write_adaptive_counter( @@ -7706,13 +9451,14 @@ impl ExecutingFrame<'_> { } } } else { - false + None }; - if has_instance_attr { + if let Some(hint) = instance_attr_hint { unsafe { self.code .instructions .write_cache_u32(cache_base + 1, type_version); + self.code.instructions.write_cache_u16(cache_base + 3, hint); } self.specialize_at(instr_idx, cache_base, Instruction::LoadAttrWithHint); } else { @@ -7746,29 +9492,11 @@ impl ExecutingFrame<'_> { ) { let obj = self.top_value(); let owner_type = obj.downcast_ref::().unwrap(); - - // Get or assign type version for the type object itself - let mut type_version = owner_type.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = owner_type.assign_version_tag(); - } - if type_version == 0 { - unsafe { - self.code.instructions.write_adaptive_counter( - cache_base, - bytecode::adaptive_counter_backoff( - self.code.instructions.read_adaptive_counter(cache_base), - ), - ); - } - return; - } - let attr_name = self.code.names[oparg.name_idx() as usize]; // Check metaclass: ensure no data descriptor on metaclass for this name let mcl = obj.class(); - let mcl_attr = mcl.get_attr(attr_name); + let (mcl_attr, mut metaclass_version) = mcl.lookup_ref_and_version_interned(attr_name, _vm); if let Some(ref attr) = mcl_attr { let attr_class = attr.class(); if attr_class.slots.descr_set.load().is_some() { @@ -7784,12 +9512,7 @@ impl ExecutingFrame<'_> { return; } } - let mut metaclass_version = 0; if !mcl.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) { - metaclass_version = mcl.tp_version_tag.load(Acquire); - if metaclass_version == 0 { - metaclass_version = mcl.assign_version_tag(); - } if metaclass_version == 0 { unsafe { self.code.instructions.write_adaptive_counter( @@ -7801,10 +9524,22 @@ impl ExecutingFrame<'_> { } return; } + } else { + metaclass_version = 0; } - // Look up attr in the type's own MRO - let cls_attr = owner_type.get_attr(attr_name); + let (cls_attr, type_version) = owner_type.lookup_ref_and_version_interned(attr_name, _vm); + if type_version == 0 { + unsafe { + self.code.instructions.write_adaptive_counter( + cache_base, + bytecode::adaptive_counter_backoff( + self.code.instructions.read_adaptive_counter(cache_base), + ), + ); + } + return; + } if let Some(ref descr) = cls_attr { let descr_class = descr.class(); let has_descr_get = descr_class.slots.descr_get.load().is_some(); @@ -7976,26 +9711,31 @@ impl ExecutingFrame<'_> { Some(Instruction::BinaryOpSubscrListSlice) } else { let cls = a.class(); + // Check the cheap gates before the __getitem__ lookup, which + // takes the global type lock and may allocate a version tag. if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) && !self.specialization_eval_frame_active(vm) - && let Some(_getitem) = cls.get_attr(identifier!(vm, __getitem__)) - && let Some(func) = _getitem.downcast_ref_if_exact::(vm) - && func.can_specialize_call(2) { - let mut type_version = cls.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = cls.assign_version_tag(); - } - if type_version != 0 { - if cls.cache_getitem_for_specialization( + let (getitem, type_version) = + cls.lookup_ref_and_version_interned(identifier!(vm, __getitem__), vm); + if type_version != 0 + && let Some(getitem) = getitem + && let Some(func) = getitem.downcast_ref_if_exact::(vm) + && func.can_specialize_call(2) + && cls.cache_getitem_for_specialization( func.to_owned(), type_version, vm, - ) { - Some(Instruction::BinaryOpSubscrGetitem) - } else { - None + ) + { + // Record the type version so the specialized handler + // can revalidate before using the cached __getitem__. + unsafe { + self.code + .instructions + .write_cache_u32(cache_base + 1, type_version); } + Some(Instruction::BinaryOpSubscrGetitem) } else { None } @@ -8165,7 +9905,7 @@ impl ExecutingFrame<'_> { fn execute_binary_op_int( &mut self, vm: &VirtualMachine, - op: impl FnOnce(&BigInt, &BigInt) -> BigInt, + op: impl FnOnce(&PyInt, &PyInt, &VirtualMachine) -> PyObjectRef, deopt_op: bytecode::BinaryOperator, ) -> FrameResult { let b = self.top_value(); @@ -8174,10 +9914,10 @@ impl ExecutingFrame<'_> { a.downcast_ref_if_exact::(vm), b.downcast_ref_if_exact::(vm), ) { - let result = op(a_int.as_bigint(), b_int.as_bigint()); - self.pop_value(); - self.pop_value(); - self.push_value(vm.ctx.new_bigint(&result).into()); + let result = op(a_int, b_int, vm); + self.pop_stackref(); + self.pop_stackref(); + self.push_value(result); Ok(None) } else { self.execute_bin_op(vm, deopt_op) @@ -8233,7 +9973,7 @@ impl ExecutingFrame<'_> { let callable = self.nth_value(nargs + 1); if let Some(func) = callable.downcast_ref_if_exact::(vm) { - if self.specialization_eval_frame_active(vm) { + if self.specialization_eval_frame_active(vm) || func.is_jitted() { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -8296,7 +10036,7 @@ impl ExecutingFrame<'_> { .function_obj() .downcast_ref_if_exact::(vm) { - if self.specialization_eval_frame_active(vm) { + if self.specialization_eval_frame_active(vm) || func.is_jitted() { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -8515,21 +10255,20 @@ impl ExecutingFrame<'_> { // CallAllocAndEnterInit: heap type with default __new__ if !self_or_null_is_some && cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) { + // Capture the version before inspecting tp_new/tp_alloc so a + // concurrently installed __new__ invalidates the version this + // specialization is cached against. + let type_version = cls.version_for_specialization(vm); let object_new = vm.ctx.types.object_type.slots.new.load(); let cls_new = cls.slots.new.load(); let object_alloc = vm.ctx.types.object_type.slots.alloc.load(); let cls_alloc = cls.slots.alloc.load(); if let (Some(cls_new_fn), Some(obj_new_fn), Some(cls_alloc_fn), Some(obj_alloc_fn)) = (cls_new, object_new, cls_alloc, object_alloc) - && cls_new_fn as usize == obj_new_fn as usize - && cls_alloc_fn as usize == obj_alloc_fn as usize + && crate::types::fn_addr(cls_new_fn) == crate::types::fn_addr(obj_new_fn) + && crate::types::fn_addr(cls_alloc_fn) == crate::types::fn_addr(obj_alloc_fn) { - let init = cls.get_attr(identifier!(vm, __init__)); - let mut version = cls.tp_version_tag.load(Acquire); - if version == 0 { - version = cls.assign_version_tag(); - } - if version == 0 { + if type_version == 0 { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -8540,15 +10279,17 @@ impl ExecutingFrame<'_> { } return; } + let init = cls.get_attr(identifier!(vm, __init__)); if let Some(init) = init && let Some(init_func) = init.downcast_ref_if_exact::(vm) - && init_func.is_simple_for_call_specialization() - && cls.cache_init_for_specialization(init_func.to_owned(), version, vm) + && init_func.can_specialize_call(nargs + 1) + && !init_func.is_generator_like() + && cls.cache_init_for_specialization(init_func.to_owned(), type_version, vm) { unsafe { self.code .instructions - .write_cache_u32(cache_base + 1, version); + .write_cache_u32(cache_base + 1, type_version); } self.specialize_at( instr_idx, @@ -8590,7 +10331,7 @@ impl ExecutingFrame<'_> { let callable = self.nth_value(nargs + 2); if let Some(func) = callable.downcast_ref_if_exact::(vm) { - if self.specialization_eval_frame_active(vm) { + if self.specialization_eval_frame_active(vm) || func.is_jitted() { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -8641,7 +10382,7 @@ impl ExecutingFrame<'_> { .function_obj() .downcast_ref_if_exact::(vm) { - if self.specialization_eval_frame_active(vm) { + if self.specialization_eval_frame_active(vm) || func.is_jitted() { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -8784,8 +10525,8 @@ impl ExecutingFrame<'_> { a.downcast_ref_if_exact::(vm), b.downcast_ref_if_exact::(vm), ) { - if specialization_compact_int_value(a_int, vm).is_some() - && specialization_compact_int_value(b_int, vm).is_some() + if specialization_compact_int_value(a_int).is_some() + && specialization_compact_int_value(b_int).is_some() { Some(Instruction::CompareOpInt) } else { @@ -8816,6 +10557,37 @@ impl ExecutingFrame<'_> { .into() } + /// Execute an immediately following conditional jump without materializing + /// the comparison result as a Python bool. This is the adaptive interpreter + /// equivalent of keeping the result virtual across the two-opcode trace. + #[inline] + fn try_fused_compare_int_jump(&mut self, result: bool, vm: &VirtualMachine) -> bool { + if self.specialization_eval_frame_active(vm) { + return false; + } + + let jump_idx = self.lasti() as usize + Instruction::CompareOpInt.cache_entries(); + if jump_idx >= self.code.instructions.len() { + return false; + } + + let jump_op = self.code.instructions.read_op(jump_idx); + let jump_on = match jump_op { + Instruction::PopJumpIfFalse { .. } => false, + Instruction::PopJumpIfTrue { .. } => true, + _ => return false, + }; + let jump_delta = self.code.instructions.read_arg(jump_idx).as_u32(); + let after_jump = jump_idx as u32 + 1 + jump_op.cache_entries() as u32; + let target = if result == jump_on { + after_jump + jump_delta + } else { + after_jump + }; + self.update_lasti(|i| *i = target); + true + } + /// Recover the BinaryOperator from the instruction arg byte. /// `replace_op` preserves the arg byte, so the original op remains accessible. fn binary_op_from_arg(&self, arg: bytecode::OpArg) -> bytecode::BinaryOperator { @@ -8842,34 +10614,35 @@ impl ExecutingFrame<'_> { Some(Instruction::ToBoolList) } else if cls.is(PyStr::class(&vm.ctx)) { Some(Instruction::ToBoolStr) - } else if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) - && cls.slots.as_number.boolean.load().is_none() - && cls.slots.as_mapping.length.load().is_none() - && cls.slots.as_sequence.length.load().is_none() - { - // Cache type version for ToBoolAlwaysTrue guard - let mut type_version = cls.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = cls.assign_version_tag(); - } - if type_version != 0 { - unsafe { - self.code - .instructions - .write_cache_u32(cache_base + 1, type_version); - } - self.specialize_at(instr_idx, cache_base, Instruction::ToBoolAlwaysTrue); - } else { - unsafe { - self.code.instructions.write_adaptive_counter( - cache_base, - bytecode::adaptive_counter_backoff( - self.code.instructions.read_adaptive_counter(cache_base), - ), - ); + } else if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) { + // Capture the version before inspecting the bool/len slots so a + // concurrently installed __bool__/__len__ invalidates the version + // the ToBoolAlwaysTrue guard is cached against. + let type_version = cls.version_for_specialization(vm); + let has_bool_or_len = cls.slots.as_number.boolean.load().is_some() + || cls.slots.as_mapping.length.load().is_some() + || cls.slots.as_sequence.length.load().is_some(); + if !has_bool_or_len { + if type_version != 0 { + unsafe { + self.code + .instructions + .write_cache_u32(cache_base + 1, type_version); + } + self.specialize_at(instr_idx, cache_base, Instruction::ToBoolAlwaysTrue); + } else { + unsafe { + self.code.instructions.write_adaptive_counter( + cache_base, + bytecode::adaptive_counter_backoff( + self.code.instructions.read_adaptive_counter(cache_base), + ), + ); + } } + return; } - return; + None } else { None }; @@ -8957,6 +10730,128 @@ impl ExecutingFrame<'_> { >= vm.recursion_limit.get() } + /// Prepare a callee frame on the datastack for a TailCall. + /// Pops args, self_or_null, and callable from the caller's stack, + /// builds the callee InterpreterFrame, and stores its pointer in + /// `vm.pending_tailcall_frame`. + /// + /// The callable must be at stack position `nargs + 1` (already validated). + fn tailcall_prepare_frame( + &mut self, + nargs: u32, + self_or_null_is_some: bool, + vm: &VirtualMachine, + ) { + let base = usize::from(self_or_null_is_some); + let effective_nargs = nargs as usize + base; + + // Peek at the callable (still on the stack) to build the callee + // frame. The callable stays on the caller's stack until we're done + // constructing the callee. + let callable = self.nth_value(nargs + 1); + let func = callable.downcast_ref_if_exact::(vm).unwrap(); + + let code: &Py = &func.code; + + let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { + FrameLocals::lazy() + } else { + FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact( + func.globals.clone(), + )) + }; + + let callee_iframe = InterpreterFrame::new_on_datastack( + code, + &func.globals, + &func.builtins, + Some(func.as_object()), + locals, + func.closure.as_ref().map_or(&[], |c| c.as_slice()), + vm, + ); + + // Move args directly from the caller's stack into callee fastlocals, + // avoiding an intermediate buffer. + { + let fastlocals = callee_iframe.localsplus.fastlocals_mut(); + for (dst, arg) in fastlocals[base..effective_nargs] + .iter_mut() + .zip(self.pop_multiple(nargs as usize)) + { + *dst = Some(arg); + } + let self_or_null = self.pop_value_opt(); + debug_assert_eq!(self_or_null.is_some(), self_or_null_is_some); + if self_or_null.is_some() { + fastlocals[0] = self_or_null; + } + } + + // Pop the callable and transfer ownership to the trampoline. This one + // reference keeps every field borrowed by the callee frame alive. + let callable = self.pop_value(); + vm.set_pending_tailcall_owner(callable); + + vm.set_pending_tailcall(callee_iframe); + } + + /// Prepare a callee frame for a bound method TailCall. + /// Pops args, self_or_null (null), and callable from the caller's stack, + /// builds the callee InterpreterFrame with bound_self prepended, and + /// stores its pointer in `vm.pending_tailcall_frame`. + fn tailcall_prepare_bound_method_frame( + &mut self, + nargs: u32, + bound_function: PyObjectRef, + bound_self: PyObjectRef, + vm: &VirtualMachine, + ) { + let effective_nargs = nargs as usize + 1; // +1 for bound_self + + let func = bound_function + .downcast_ref_if_exact::(vm) + .unwrap(); + let code: &Py = &func.code; + + let locals = if code.flags.contains(bytecode::CodeFlags::NEWLOCALS) { + FrameLocals::lazy() + } else { + FrameLocals::with_locals(crate::function::ArgMapping::from_dict_exact( + func.globals.clone(), + )) + }; + + let callee_iframe = InterpreterFrame::new_on_datastack( + code, + &func.globals, + &func.builtins, + Some(func.as_object()), + locals, + func.closure.as_ref().map_or(&[], |c| c.as_slice()), + vm, + ); + + // Move args directly from the caller's stack into callee fastlocals. + let fastlocals = callee_iframe.localsplus.fastlocals_mut(); + for (dst, arg) in fastlocals[1..effective_nargs] + .iter_mut() + .zip(self.pop_multiple(nargs as usize)) + { + *dst = Some(arg); + } + self.pop_value_opt(); // null (self_or_null) + self.pop_value(); // callable (bound method) + fastlocals[0] = Some(bound_self); + + // The function owns every field borrowed by the callee frame. + // bound_self is owned by fastlocals; the bound-method object itself is + // no longer needed and was dropped above, matching the recursive path. + vm.set_pending_tailcall_owner(bound_function); + + vm.set_pending_tailcall(callee_iframe); + } + #[inline] fn for_iter_has_end_for_shape(&self, instr_idx: usize, jump_delta: u32) -> bool { let target_idx = instr_idx @@ -9004,7 +10899,7 @@ impl ExecutingFrame<'_> { return; } let name = self.code.names[(oparg >> 1) as usize]; - let Ok(globals_version) = u16::try_from(self.globals.version()) else { + let Ok(globals_version @ 1..) = u16::try_from(self.globals.assign_keys_version(vm)) else { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -9032,7 +10927,7 @@ impl ExecutingFrame<'_> { if let Some(builtins_dict) = self.builtins.downcast_ref_if_exact::(vm) && let Ok(Some(builtins_hint)) = builtins_dict.hint_for_key(name, vm) - && let Ok(builtins_version) = u16::try_from(builtins_dict.version()) + && let Ok(builtins_version @ 1..) = u16::try_from(builtins_dict.assign_keys_version(vm)) { unsafe { self.code @@ -9167,13 +11062,11 @@ impl ExecutingFrame<'_> { let owner = self.top_value(); let cls = owner.class(); - // Only specialize if setattr is the default (generic_setattr) - let is_default_setattr = cls - .slots - .setattro - .load() - .is_some_and(|f| f as usize == PyBaseObject::slot_setattro as *const () as usize); - if !is_default_setattr { + // Capture the version before inspecting the setattro slot so a + // concurrently installed __setattr__ invalidates the version this + // specialization is cached against. + let type_version = cls.version_for_specialization(vm); + if type_version == 0 { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -9185,12 +11078,12 @@ impl ExecutingFrame<'_> { return; } - // Get or assign type version - let mut type_version = cls.tp_version_tag.load(Acquire); - if type_version == 0 { - type_version = cls.assign_version_tag(); - } - if type_version == 0 { + // Only specialize if setattr is the default (generic_setattr) + let is_default_setattr = cls.slots.setattro.load().is_some_and(|f| { + crate::types::fn_addr(f) + == crate::types::fn_addr(PyBaseObject::slot_setattro as crate::types::SetattroFunc) + }); + if !is_default_setattr { unsafe { self.code.instructions.write_adaptive_counter( cache_base, @@ -9202,7 +11095,6 @@ impl ExecutingFrame<'_> { return; } - // Check for data descriptor let attr_name = self.code.names[attr_idx as usize]; let cls_attr = cls.get_attr(attr_name); let has_data_descr = cls_attr.as_ref().is_some_and(|descr| { @@ -9212,9 +11104,12 @@ impl ExecutingFrame<'_> { if has_data_descr { // Check for member descriptor (slot access) + // As in the load specialization, the offset is only valid for + // instances of the type the descriptor belongs to. if let Some(ref descr) = cls_attr && let Some(member_descr) = descr.downcast_ref::() && let MemberGetter::Offset(offset) = member_descr.member.getter + && cls.fast_issubclass(&member_descr.common.typ) { unsafe { self.code @@ -9236,9 +11131,8 @@ impl ExecutingFrame<'_> { } } } else if let Some(dict) = owner.dict() { - let use_hint = match dict.get_item_opt(attr_name, vm) { - Ok(Some(_)) => true, - Ok(None) => false, + let hint = match dict.hint_for_key(attr_name, vm) { + Ok(hint) => hint, Err(_) => { unsafe { self.code.instructions.write_adaptive_counter( @@ -9255,11 +11149,14 @@ impl ExecutingFrame<'_> { self.code .instructions .write_cache_u32(cache_base + 1, type_version); + self.code + .instructions + .write_cache_u16(cache_base + 3, hint.unwrap_or(0)); } self.specialize_at( instr_idx, cache_base, - if use_hint { + if hint.is_some() { Instruction::StoreAttrWithHint } else { Instruction::StoreAttrInstanceValue @@ -9562,6 +11459,49 @@ impl ExecutingFrame<'_> { } } + /// Take a call's `[self_or_null, arg1, ..., argN]` off the stack as one + /// vectorcall argument list, along with the callable underneath them. + /// + /// The stack already holds the arguments in vectorcall order, so filling a + /// single vector by index costs one allocation — collecting the positional + /// arguments first and then pushing `self` in front of them costs two plus + /// a copy. + fn take_call_args(&mut self, nargs: usize) -> (PyObjectRef, Vec) { + let stack_len = self.localsplus.stack_len(); + debug_assert!( + stack_len >= nargs + 2, + "CALL stack underflow: need callable + self_or_null + {nargs} args, have {stack_len}" + ); + let callable_idx = stack_len - nargs - 2; + let self_or_null_idx = callable_idx + 1; + + let self_or_null = self + .localsplus + .stack_index_mut(self_or_null_idx) + .take() + .map(|sr| sr.to_pyobj()); + let mut args = Vec::with_capacity(nargs + usize::from(self_or_null.is_some())); + args.extend(self_or_null); + for stack_idx in self_or_null_idx + 1..stack_len { + let val = self + .localsplus + .stack_index_mut(stack_idx) + .take() + .unwrap() + .to_pyobj(); + args.push(val); + } + + let callable = self + .localsplus + .stack_index_mut(callable_idx) + .take() + .unwrap() + .to_pyobj(); + self.localsplus.stack_truncate(callable_idx); + (callable, args) + } + /// Pop multiple values from the stack. Panics if any slot is NULL. fn pop_multiple(&mut self, count: usize) -> impl ExactSizeIterator + '_ { let stack_len = self.localsplus.stack_len(); @@ -9620,11 +11560,13 @@ impl ExecutingFrame<'_> { } } -impl fmt::Debug for Frame { +impl fmt::Debug for FrameObject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // SAFETY: Debug is best-effort; concurrent mutation is unlikely // and would only affect debug output. - let iframe = unsafe { &*self.iframe.get() }; + let Some(iframe) = (unsafe { &*self.iframe.get() }) else { + return f.write_str("FrameObject Object { cleared }"); + }; let stack_str = iframe .localsplus @@ -9647,9 +11589,9 @@ impl fmt::Debug for Frame { // TODO: fix this up write!( f, - "Frame Object {{ \n Stack:{}\n Locals initialized:{}\n}}", + "FrameObject Object {{ \n Stack:{}\n Locals initialized:{}\n}}", stack_str, - self.locals.get().is_some() + self.iframe().locals.get().is_some() ) } } diff --git a/crates/vm/src/function/argument.rs b/crates/vm/src/function/argument.rs index c37475ba48d..6bf4ae2107b 100644 --- a/crates/vm/src/function/argument.rs +++ b/crates/vm/src/function/argument.rs @@ -1,18 +1,24 @@ use crate::{ AsObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, builtins::{PyBaseExceptionRef, PyTupleRef, PyTypeRef}, + common::wtf8::{Wtf8, Wtf8Buf}, convert::ToPyObject, object::{Traverse, TraverseFn}, }; use core::ops::{Deref, DerefMut, RangeInclusive}; use indexmap::IndexMap; use itertools::Itertools; +use std::hash::DefaultHasher; pub trait IntoFuncArgs: Sized { fn into_args(self, vm: &VirtualMachine) -> FuncArgs; fn into_method_args(self, obj: PyObjectRef, vm: &VirtualMachine) -> FuncArgs { let mut args = self.into_args(vm); - args.prepend_arg(obj); + // Build the final vec once instead of prepending (realloc + memmove). + let mut with_obj = Vec::with_capacity(args.args.len() + 1); + with_obj.push(obj); + with_obj.append(&mut args.args); + args.args = with_obj; args } } @@ -151,10 +157,12 @@ impl FuncArgs { .iter() .zip(&args[nargs..nargs + names.len()]) .map(|(name, val)| { + // `PyStr`, not `PyUtf8Str`: a surrogate key is a valid str and + // must survive as WTF-8 rather than panic. let key = name - .downcast_ref::() + .downcast_ref::() .expect("kwnames must be strings") - .as_str() + .as_wtf8() .to_owned(); (key, val.clone()) }) @@ -183,9 +191,9 @@ impl FuncArgs { .zip(args.drain(nargs..nargs + kw_count)) .map(|(name, val)| { let key = name - .downcast_ref::() + .downcast_ref::() .expect("kwnames must be strings") - .as_str() + .as_wtf8() .to_owned(); (key, val) }) @@ -202,7 +210,9 @@ impl FuncArgs { } pub fn prepend_arg(&mut self, item: PyObjectRef) { - self.args.reserve_exact(1); + // reserve (not reserve_exact): incoming vectors are usually built with + // exact capacity, so exact growth would realloc on every prepend. + self.args.reserve(1); self.args.insert(0, item) } @@ -262,7 +272,7 @@ impl FuncArgs { self.kwargs.swap_remove(name) } - pub fn remaining_keywords(&mut self) -> impl Iterator + '_ { + pub fn remaining_keywords(&mut self) -> impl Iterator + '_ { self.kwargs.drain(..) } @@ -400,17 +410,29 @@ impl FromArgOptional for T { /// KwArgs is only for functions that accept arbitrary keyword arguments. For /// functions that accept only *specific* named arguments, a rust struct with /// an appropriate FromArgs implementation must be created. +// Keys are stored as `Wtf8Buf`, not `String`, so that a lone-surrogate keyword +// name coming through `f(**d)` is preserved instead of being rejected (see +// issue #8228). `PyStr` is WTF-8 backed, and CPython only requires that a +// keyword key be a `str`, not that it be valid UTF-8. #[derive(Clone, Debug)] -pub struct KwArgs(IndexMap); +pub struct KwArgs(KwArgsMap); + +/// The map behind [`KwArgs`]. +/// +/// The hasher is zero-sized rather than the randomly seeded default: a +/// `KwArgs` is built for every call, including the far more common +/// keyword-less one, and seeding reads a thread-local. Keyword names come +/// from the program text, so per-process hash randomization buys nothing. +pub type KwArgsMap = IndexMap>; impl Default for KwArgs { fn default() -> Self { - Self(IndexMap::new()) + Self(KwArgsMap::default()) } } impl Deref for KwArgs { - type Target = IndexMap; + type Target = KwArgsMap; fn deref(&self) -> &Self::Target { &self.0 @@ -434,24 +456,47 @@ where impl KwArgs { #[must_use] - pub const fn new(map: IndexMap) -> Self { + pub const fn new(map: KwArgsMap) -> Self { Self(map) } + // `String` keys accepted `&str` lookups for free via `Borrow`; `Wtf8Buf` + // borrows only as `Wtf8`, so these inherent methods restore the `&str` interface + // via the zero-cost `Wtf8::new` cast, keeping every call site unchanged. + #[must_use] + pub fn get(&self, name: &str) -> Option<&T> { + self.0.get(Wtf8::new(name)) + } + + #[must_use] + pub fn contains_key(&self, name: &str) -> bool { + self.0.contains_key(Wtf8::new(name)) + } + + pub fn swap_remove(&mut self, name: &str) -> Option { + self.0.swap_remove(Wtf8::new(name)) + } + + pub fn shift_remove(&mut self, name: &str) -> Option { + self.0.shift_remove(Wtf8::new(name)) + } + pub fn pop_kwarg(&mut self, name: &str) -> Option { self.swap_remove(name) } } -impl FromIterator<(String, T)> for KwArgs { - fn from_iter>(iter: I) -> Self { - Self(iter.into_iter().collect()) +// Accept any key that converts into `Wtf8Buf` (notably `String`), so existing +// call sites that build kwargs from string literals keep compiling unchanged. +impl, T> FromIterator<(K, T)> for KwArgs { + fn from_iter>(iter: I) -> Self { + Self(iter.into_iter().map(|(k, v)| (k.into(), v)).collect()) } } impl<'a, T> IntoIterator for &'a KwArgs { - type Item = (&'a String, &'a T); - type IntoIter = indexmap::map::Iter<'a, String, T>; + type Item = (&'a Wtf8Buf, &'a T); + type IntoIter = indexmap::map::Iter<'a, Wtf8Buf, T>; fn into_iter(self) -> Self::IntoIter { self.0.iter() @@ -459,8 +504,8 @@ impl<'a, T> IntoIterator for &'a KwArgs { } impl IntoIterator for KwArgs { - type Item = (String, T); - type IntoIter = indexmap::map::IntoIter; + type Item = (Wtf8Buf, T); + type IntoIter = indexmap::map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() @@ -472,7 +517,7 @@ where T: TryFromObject, { fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result { - let mut kwargs = IndexMap::new(); + let mut kwargs = KwArgsMap::default(); for (name, value) in args.remaining_keywords() { kwargs.insert(name, value.try_into_value(vm)?); } diff --git a/crates/vm/src/function/buffer.rs b/crates/vm/src/function/buffer.rs index 028f3633d06..dba97e9c77f 100644 --- a/crates/vm/src/function/buffer.rs +++ b/crates/vm/src/function/buffer.rs @@ -3,7 +3,7 @@ use crate::{ VirtualMachine, builtins::{PyStr, PyStrRef}, common::borrow::{BorrowedValue, BorrowedValueMut}, - protocol::PyBuffer, + protocol::{BufferFlags, PyBuffer}, }; // Python/getargs.c @@ -13,24 +13,22 @@ use crate::{ pub struct ArgBytesLike(PyBuffer); impl PyObject { - pub fn try_bytes_like( - &self, - vm: &VirtualMachine, - f: impl FnOnce(&[u8]) -> R, - ) -> PyResult { - let buffer = PyBuffer::try_from_borrowed_object(vm, self)?; + pub fn try_bytes_like(&self, vm: &VirtualMachine, f: F) -> PyResult + where + F: FnOnce(&[u8]) -> R, + { + let buffer = PyBuffer::from_object(vm, self, BufferFlags::SIMPLE)?; buffer .as_contiguous() .map(|x| f(&x)) .ok_or_else(|| vm.new_buffer_error("non-contiguous buffer is not a bytes-like object")) } - pub fn try_rw_bytes_like( - &self, - vm: &VirtualMachine, - f: impl FnOnce(&mut [u8]) -> R, - ) -> PyResult { - let buffer = PyBuffer::try_from_borrowed_object(vm, self)?; + pub fn try_rw_bytes_like(&self, vm: &VirtualMachine, f: F) -> PyResult + where + F: FnOnce(&mut [u8]) -> R, + { + let buffer = PyBuffer::from_object(vm, self, BufferFlags::WRITABLE)?; buffer .as_contiguous_mut() .map(|mut x| f(&mut x)) @@ -51,11 +49,41 @@ impl ArgBytesLike { f(&self.borrow_buf()) } + /// The bytes to hand to an operation that may wait, and whatever keeps + /// them readable while it does. + /// + /// `borrow_buf` may answer with a lock that every other thread writing to + /// the same object waits on, and a thread waiting on a lock never reaches + /// a safepoint, so keeping one across a wait for a peer, a pipe or a + /// signal stops the world from being stopped at all. Bytes reached that + /// way are copied out first. Bytes that lock nothing -- an immutable + /// object's -- are borrowed where they lie, which is all CPython holds in + /// either case. + pub fn borrow_buf_unlocked(&self, vm: &VirtualMachine) -> PyResult> { + let borrowed = self.borrow_buf(); + if !borrowed.is_locked() { + return Ok(UnlockedBuf::Borrowed(borrowed)); + } + let mut copy = Vec::new(); + copy.try_reserve_exact(borrowed.len()) + .map_err(|_| vm.new_memory_error(""))?; + copy.extend_from_slice(&borrowed); + Ok(UnlockedBuf::Copied(copy)) + } + #[must_use] pub const fn len(&self) -> usize { self.0.desc.len } + /// The width of one item. Callers that read the buffer as bytes rather + /// than as whatever it holds have to ask, since a contiguous buffer of + /// wider items is contiguous all the same. + #[must_use] + pub const fn itemsize(&self) -> usize { + self.0.desc.itemsize + } + #[must_use] pub const fn is_empty(&self) -> bool { self.len() == 0 @@ -65,6 +93,16 @@ impl ArgBytesLike { pub fn as_object(&self) -> &PyObject { &self.0.obj } + + /// The object whose storage is borrowed while this buffer is read: a view + /// borrows the object it looks at, not itself. + #[must_use] + pub fn source_object(&self) -> &PyObject { + self.0 + .obj + .downcast_ref::() + .map_or(&self.0.obj, |view| view.viewed_object()) + } } impl From for PyBuffer { @@ -79,9 +117,9 @@ impl From for PyObjectRef { } } -impl<'a> TryFromBorrowedObject<'a> for ArgBytesLike { - fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { - let buffer = PyBuffer::try_from_borrowed_object(vm, obj)?; +impl ArgBytesLike { + fn from_request(vm: &VirtualMachine, obj: &PyObject, flags: BufferFlags) -> PyResult { + let buffer = PyBuffer::from_object(vm, obj, flags)?; if buffer.desc.is_contiguous() { Ok(Self(buffer)) } else { @@ -90,6 +128,49 @@ impl<'a> TryFromBorrowedObject<'a> for ArgBytesLike { } } +impl<'a> TryFromBorrowedObject<'a> for ArgBytesLike { + fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { + Self::from_request(vm, obj, BufferFlags::SIMPLE) + } +} + +/// A bytes-like object asked for as `PyBUF_CONTIG_RO`, which is what a shape is +/// requested with rather than assumed. +#[derive(Debug, Traverse)] +pub struct ArgContiguousBytesLike(ArgBytesLike); + +impl core::ops::Deref for ArgContiguousBytesLike { + type Target = ArgBytesLike; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> TryFromBorrowedObject<'a> for ArgContiguousBytesLike { + fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { + ArgBytesLike::from_request(vm, obj, BufferFlags::CONTIG_RO).map(Self) + } +} + +/// Bytes that stay readable across a wait, from [`ArgBytesLike::borrow_buf_unlocked`]. +#[derive(Debug)] +pub enum UnlockedBuf<'a> { + Borrowed(BorrowedValue<'a, [u8]>), + Copied(Vec), +} + +impl core::ops::Deref for UnlockedBuf<'_> { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + match self { + Self::Borrowed(b) => b, + Self::Copied(v) => v, + } + } +} + /// A memory buffer, read-write access. Like the `w*` format code for `PyArg_Parse` in CPython. #[derive(Debug, Traverse)] pub struct ArgMemoryBuffer(PyBuffer); @@ -116,6 +197,16 @@ impl ArgMemoryBuffer { pub const fn is_empty(&self) -> bool { self.len() == 0 } + + /// The object whose storage is borrowed while this buffer is written: a + /// view borrows the object it looks at, not itself. + #[must_use] + pub fn source_object(&self) -> &PyObject { + self.0 + .obj + .downcast_ref::() + .map_or(&self.0.obj, |view| view.viewed_object()) + } } impl From for PyBuffer { @@ -126,7 +217,15 @@ impl From for PyBuffer { impl<'a> TryFromBorrowedObject<'a> for ArgMemoryBuffer { fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { - let buffer = PyBuffer::try_from_borrowed_object(vm, obj)?; + let buffer = PyBuffer::from_object(vm, obj, BufferFlags::WRITABLE).map_err(|exc| { + if obj.check_buffer() { + // An exporter that cannot serve the request leaves the argument + // simply the wrong kind of object, as `PyArg_Parse` reports it. + vm.new_type_error("buffer is not a read-write bytes-like object") + } else { + exc + } + })?; if !buffer.desc.is_contiguous() { Err(vm.new_buffer_error("non-contiguous buffer is not a bytes-like object")) } else if buffer.desc.readonly { @@ -207,7 +306,10 @@ impl ArgAsciiBuffer { } #[inline] - pub fn with_ref(&self, f: impl FnOnce(&[u8]) -> R) -> R { + pub fn with_ref(&self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { match self { Self::String(s) => f(s.as_bytes()), Self::Buffer(buffer) => buffer.with_ref(f), diff --git a/crates/vm/src/function/builtin.rs b/crates/vm/src/function/builtin.rs index 4fed5e4cf23..a2753bdf145 100644 --- a/crates/vm/src/function/builtin.rs +++ b/crates/vm/src/function/builtin.rs @@ -11,8 +11,9 @@ pub trait PyNativeFn: Fn(&VirtualMachine, FuncArgs) -> PyResult + PyThreadingConstraint + 'static { } -impl PyResult + PyThreadingConstraint + 'static> PyNativeFn - for F + +impl PyNativeFn for F where + F: Fn(&VirtualMachine, FuncArgs) -> PyResult + PyThreadingConstraint + 'static { } @@ -56,9 +57,10 @@ const fn zst_ref_out_of_thin_air(x: T) -> &'static T { // operation. if T isn't zero-sized, we don't have to worry about it because we'll fail to compile. core::mem::forget(x); const { - if core::mem::size_of::() != 0 { - panic!("can't use a non-zero-sized type here") - } + assert!( + core::mem::size_of::() == 0, + "can't use a non-zero-sized type here" + ); // SAFETY: we just confirmed that T is zero-sized, so we can // pull a value of it out of thin air. unsafe { core::ptr::NonNull::::dangling().as_ref() } @@ -103,8 +105,10 @@ use sealed::PyNativeFnInternal; #[doc(hidden)] pub struct OwnedParam(PhantomData); + #[doc(hidden)] pub struct BorrowedParam(PhantomData); + #[doc(hidden)] pub struct RefParam(PhantomData); diff --git a/crates/vm/src/function/either.rs b/crates/vm/src/function/either.rs index 9ee7f028bd2..e7f6091b200 100644 --- a/crates/vm/src/function/either.rs +++ b/crates/vm/src/function/either.rs @@ -8,7 +8,11 @@ pub enum Either { B(B), } -impl, B: Borrow> Borrow for Either { +impl Borrow for Either +where + A: Borrow, + B: Borrow, +{ #[inline(always)] fn borrow(&self) -> &PyObject { match self { @@ -18,7 +22,11 @@ impl, B: Borrow> Borrow for Either } } -impl, B: AsRef> AsRef for Either { +impl AsRef for Either +where + A: AsRef, + B: AsRef, +{ #[inline(always)] fn as_ref(&self) -> &PyObject { match self { @@ -28,7 +36,11 @@ impl, B: AsRef> AsRef for Either { } } -impl, B: Into> From> for PyObjectRef { +impl From> for PyObjectRef +where + A: Into, + B: Into, +{ #[inline(always)] fn from(value: Either) -> Self { match value { @@ -38,7 +50,11 @@ impl, B: Into> From> for PyObjectRef { } } -impl ToPyObject for Either { +impl ToPyObject for Either +where + A: ToPyObject, + B: ToPyObject, +{ #[inline(always)] fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { match self { diff --git a/crates/vm/src/function/fspath.rs b/crates/vm/src/function/fspath.rs index 5ec81ba63a6..f0ab2909059 100644 --- a/crates/vm/src/function/fspath.rs +++ b/crates/vm/src/function/fspath.rs @@ -3,9 +3,9 @@ use crate::{ builtins::{PyBytes, PyBytesRef, PyStrRef}, convert::{IntoPyException, ToPyObject}, function::PyStr, - protocol::PyBuffer, }; use alloc::borrow::Cow; +use core::hint::cold_path; use std::{ffi::OsStr, path::PathBuf}; /// Helper to implement os.fspath() @@ -36,21 +36,20 @@ impl FsPath { msg: &'static str, vm: &VirtualMachine, ) -> PyResult { - let check_nul = |b: &[u8]| { - if !check_for_nul || memchr::memchr(b'\0', b).is_none() { - Ok(()) - } else { - Err(crate::exceptions::cstring_error(vm)) - } - }; let match1 = |obj: PyObjectRef| { let pathlike = match_class!(match obj { s @ PyStr => { - check_nul(s.as_bytes())?; + if check_for_nul && s.contains_nuls() { + cold_path(); + return Err(crate::exceptions::nul_char_error(vm)); + } Self::Str(s) } b @ PyBytes => { - check_nul(&b)?; + if check_for_nul && b.contains_nuls() { + cold_path(); + return Err(crate::exceptions::nul_char_error(vm)); + } Self::Bytes(b) } obj => return Ok(Err(obj)), @@ -125,8 +124,15 @@ impl FsPath { } pub fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> { - rustpython_host_env::os::bytes_as_os_str(b) - .map_err(|_| vm.new_unicode_decode_error("can't decode path for utf-8")) + rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(b.to_vec()), + e.valid_up_to(), + e.error_len().map_or(b.len(), |n| e.valid_up_to() + n), + vm.ctx.new_str("can't decode path for utf-8"), + ) + }) } } @@ -140,16 +146,9 @@ impl ToPyObject for FsPath { } impl TryFromObject for FsPath { - // PyUnicode_FSDecoder in CPython + // PyUnicode_FSDecoder, which takes what PyOS_FSPath takes: str, bytes, or an + // object with __fspath__, and nothing that merely exports a buffer. fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - let obj = match obj.try_to_value::(vm) { - Ok(buffer) => { - let mut bytes = vec![]; - buffer.append_to(&mut bytes); - vm.ctx.new_bytes(bytes).into() - } - Err(_) => obj, - }; Self::try_from_path_like(obj, true, vm) } } diff --git a/crates/vm/src/function/getset.rs b/crates/vm/src/function/getset.rs index bcd745f561b..a1c003dbca6 100644 --- a/crates/vm/src/function/getset.rs +++ b/crates/vm/src/function/getset.rs @@ -1,6 +1,4 @@ -/*! Python `attribute` descriptor class. (PyGetSet) - -*/ +//! Python `attribute` descriptor class. (PyGetSet) use crate::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, convert::ToPyResult, diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index 12eda50c9d5..f0497fb384e 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -254,6 +254,7 @@ impl PyMethodDef { all_methods } + #[must_use] const fn const_copy(&self) -> Self { Self { name: self.name, diff --git a/crates/vm/src/function/mod.rs b/crates/vm/src/function/mod.rs index 7eb87fea3ed..2b37c9fc8de 100644 --- a/crates/vm/src/function/mod.rs +++ b/crates/vm/src/function/mod.rs @@ -11,11 +11,13 @@ mod protocol; mod time; pub use argument::{ - ArgumentError, FromArgOptional, FromArgs, FuncArgs, IntoFuncArgs, KwArgs, OptionalArg, - OptionalOption, PosArgs, + ArgumentError, FromArgOptional, FromArgs, FuncArgs, IntoFuncArgs, KwArgs, KwArgsMap, + OptionalArg, OptionalOption, PosArgs, }; pub use arithmetic::{PyArithmeticValue, PyComparisonValue}; -pub use buffer::{ArgAsciiBuffer, ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike}; +pub use buffer::{ + ArgAsciiBuffer, ArgBytesLike, ArgContiguousBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike, +}; pub use builtin::{IntoPyNativeFn, PyNativeFn, static_func, static_raw_func}; pub use either::Either; pub use fspath::FsPath; diff --git a/crates/vm/src/function/protocol.rs b/crates/vm/src/function/protocol.rs index 25ef62b458d..d503fabaca8 100644 --- a/crates/vm/src/function/protocol.rs +++ b/crates/vm/src/function/protocol.rs @@ -86,6 +86,11 @@ unsafe impl Traverse for ArgIterable { } impl ArgIterable { + #[must_use] + pub(crate) fn as_object(&self) -> &PyObject { + &self.iterable + } + /// Returns an iterator over this sequence of objects. /// /// This operation may fail if an exception is raised while invoking the diff --git a/crates/vm/src/gc_state.rs b/crates/vm/src/gc_state.rs index ecb34fcc869..e5eb3758950 100644 --- a/crates/vm/src/gc_state.rs +++ b/crates/vm/src/gc_state.rs @@ -4,20 +4,19 @@ use crate::common::linked_list::LinkedList; use crate::common::lock::{PyMutex, PyRwLock}; -use crate::object::{GC_PERMANENT, GC_UNTRACKED, GcLink}; +use crate::object::{GC_NO_OWNER, GC_PERMANENT, GC_UNTRACKED, GcLink, GcOwner}; use crate::{AsObject, PyObject, PyObjectRef}; use core::ptr::NonNull; -use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; -use std::collections::HashSet; - -#[cfg(not(target_arch = "wasm32"))] -fn elapsed_secs(start: &std::time::Instant) -> f64 { - start.elapsed().as_secs_f64() -} - -#[cfg(target_arch = "wasm32")] -fn elapsed_secs(_start: &()) -> f64 { - 0.0 +use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicUsize, Ordering}; + +fn elapsed_secs( + #[cfg(target_arch = "wasm32")] _start: (), + #[cfg(not(target_arch = "wasm32"))] start: std::time::Instant, +) -> f64 { + cfg_select! { + target_arch = "wasm32" => 0.0, + _ => start.elapsed().as_secs_f64(), + } } bitflags::bitflags! { @@ -38,7 +37,7 @@ bitflags::bitflags! { } /// Result from a single collection run -#[derive(Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct CollectResult { pub collected: usize, pub uncollectable: usize, @@ -47,7 +46,7 @@ pub struct CollectResult { } /// Statistics for a single generation (gc_generation_stats) -#[derive(Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct GcStats { pub collections: usize, pub collected: usize, @@ -56,10 +55,12 @@ pub struct GcStats { pub duration: f64, } -/// A single GC generation with intrusive linked list +/// One generation's collection policy and statistics, per interpreter. +/// +/// The objects themselves live in the process-wide lists on [`GcState`], so the +/// occupancy count sits there; what an interpreter owns is when to collect and +/// what its own collections have done. pub struct GcGeneration { - /// Number of objects in this generation - count: AtomicUsize, /// Threshold for triggering collection threshold: AtomicU32, /// Collection statistics @@ -70,7 +71,6 @@ impl GcGeneration { #[must_use] pub const fn new(threshold: u32) -> Self { Self { - count: AtomicUsize::new(0), threshold: AtomicU32::new(threshold), stats: PyMutex::new(GcStats { collections: 0, @@ -82,16 +82,14 @@ impl GcGeneration { } } - pub fn count(&self) -> usize { - self.count.load(Ordering::SeqCst) - } - + /// Relaxed: this is policy read once per allocation, and a collection + /// racing `gc.set_threshold()` may use either value. pub fn threshold(&self) -> u32 { - self.threshold.load(Ordering::SeqCst) + self.threshold.load(Ordering::Relaxed) } pub fn set_threshold(&self, value: u32) { - self.threshold.store(value, Ordering::SeqCst); + self.threshold.store(value, Ordering::Relaxed); } pub fn stats(&self) -> GcStats { @@ -131,34 +129,196 @@ impl GcGeneration { } } +/// Drop one from a generation's occupancy. +/// +/// A collection resets the counts of the generations it emptied, but it only +/// empties its own interpreter's objects; another interpreter's stay behind with +/// the count already zeroed, and untracking one of those must not wrap. +fn release_count(count: &AtomicUsize) { + if count.load(Ordering::Relaxed) > 0 { + count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// Whether `owner`'s collections act on `obj`. +/// +/// Objects with no owner — everything the shared context allocates, and anything +/// allocated with no interpreter current — belong to all of them. +fn is_owned_by(obj: &PyObject, owner: GcOwner) -> bool { + let obj_owner = obj.gc_owner(); + obj_owner == owner || obj_owner == GC_NO_OWNER +} + /// Wrapper for NonNull to impl Hash/Eq for use in temporary collection sets. /// Only used within collect_inner, never shared across threads. #[derive(Clone, Copy, PartialEq, Eq, Hash)] struct GcPtr(NonNull); -/// Global GC state +/// Hashing for the tables a collection keys by an object's address. +/// +/// The default hasher is SipHash, which buys resistance against a caller +/// choosing keys that collide. Nothing chooses these keys: they are addresses +/// this process handed out, and the tables live and die inside one collection. +/// What a collection needs from them is speed -- it hashes every tracked +/// object and every edge between them -- so this runs the address through a +/// handful of multiplies and shifts instead. The shifts are what earns the +/// speed: a table picks its bucket from the low bits, and an address arrives +/// with its low bits zeroed by alignment, so entropy has to be carried +/// downward or every object lands in the same few buckets. +#[derive(Default)] +struct GcPtrHasher(u64); + +impl core::hash::Hasher for GcPtrHasher { + fn finish(&self) -> u64 { + self.0 + } + + fn write_usize(&mut self, value: usize) { + let mut z = (value as u64).wrapping_add(0x9E37_79B9_7F4A_7C15); + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + self.0 = z ^ (z >> 31); + } + + fn write(&mut self, bytes: &[u8]) { + // Addresses reach this hasher through `write_usize`; a key hashed any + // other way still has to land somewhere sensible. + for &byte in bytes { + self.0 = (self.0 ^ u64::from(byte)).wrapping_mul(0x0100_0000_01B3); + } + } +} + +type GcBuildHasher = core::hash::BuildHasherDefault; +type GcSet = std::collections::HashSet; +type GcMap = std::collections::HashMap; + +/// RAII barrier that parks every other thread for the pointer-reading phases +/// of a collection and lets them run again before finalizers execute. +/// +/// Reference subtraction, the reachability walk and the strong-reference +/// snapshot dereference the interpreter state of every tracked object, +/// including the `localsplus` of frames that other threads are actively +/// executing. Those writes carry no synchronization, so the reads are only +/// well-defined while all other threads are parked at a safepoint. Restarting +/// happens explicitly once the snapshot has pinned every object; `Drop` is a +/// backstop that also restarts on the early-return paths. +/// +/// A collection acts on one interpreter's objects, but its candidates include +/// the ones no interpreter owns, which every interpreter can reference and so +/// incref. Reading a refcount that another interpreter is changing is what +/// makes an object look unreachable when it is not, so every live interpreter +/// is stopped, not just the collecting one. Stopping in `runtime` id order +/// keeps exclusion acquisition ordered; the `collecting` mutex additionally +/// serializes collections process-wide, so no second collector can take these +/// exclusions in another order. +#[cfg(feature = "threading")] +struct CollectStopTheWorld { + /// Stopped interpreter states, in stop order. Held as strong references so + /// an interpreter cannot be dropped between stop and restart, and kept past + /// the restart so that releasing the last one — which frees that + /// interpreter's objects, and so removes them from these lists — happens + /// after the collection has let go of the generation locks. + stopped: Vec>, + /// Keeps interpreters from registering between the snapshot below and the + /// restart. One registered in that window would be missing from `stopped`, + /// so its bootstrap would keep running — and mutating the shared generation + /// lists — while this collection reads them. + admission: Option>, + restarted: bool, +} + +#[cfg(feature = "threading")] +impl CollectStopTheWorld { + /// Request stop-the-world on every live interpreter when the current thread + /// has an attached VM. Falls back to no barrier when no VM is attached (the + /// tracked-object reads then run without other threads only if the caller + /// guarantees it). + fn new() -> Self { + // No attached VM means no interpreter is running Python on this thread; + // keep the historical no-barrier fallback. + if !crate::vm::thread::current_vm_is_set() { + return Self { + stopped: Vec::new(), + admission: None, + restarted: true, + }; + } + + // Accumulate into a live `Self` rather than a bare Vec: if a later + // `stop_the_world` unwinds, dropping this guard restarts the + // interpreters already stopped, instead of leaving their threads parked + // and their exclusion held forever. + let mut guard = Self { + stopped: Vec::new(), + admission: Some(crate::vm::runtime::lock_admission_for_stop()), + restarted: false, + }; + for state in crate::vm::runtime::live_interpreter_states() { + state.stop_the_world.stop_the_world(&state); + guard.stopped.push(state); + } + guard + } + + /// Restart the world. Idempotent. + fn restart(&mut self) { + if self.restarted { + return; + } + self.restarted = true; + // Reverse of the stop order. The references stay until this guard is + // dropped; see the field comment. + for state in self.stopped.iter().rev() { + state.stop_the_world.start_the_world(state); + } + // Nothing is parked any more, so registration may resume. + self.admission = None; + } + + /// Whether this collection actually stopped the world. + #[cfg(all(unix, debug_assertions))] + fn is_stopped(&self) -> bool { + !self.stopped.is_empty() + } +} + +#[cfg(feature = "threading")] +impl Drop for CollectStopTheWorld { + fn drop(&mut self) { + self.restart(); + } +} + +/// The process-wide object lists every interpreter's collections walk. +/// +/// Interpreter-owned policy and results live in [`GcInterpreterState`]; what is +/// here is shared because the lists are: an object is untracked from +/// `default_dealloc`, where no interpreter is in scope, so it has to be findable +/// without one. pub struct GcState { - /// 3 generations (0 = youngest, 2 = oldest) - pub generations: [GcGeneration; 3], - /// Permanent generation (frozen objects) - pub permanent: GcGeneration, - /// GC enabled flag - pub enabled: AtomicBool, /// Per-generation intrusive linked lists for object tracking. /// Objects start in gen0, survivors are promoted to gen1, then gen2. generation_lists: [PyRwLock>; 3], /// Frozen/permanent objects (excluded from normal GC) permanent_list: PyRwLock>, - /// Debug flags - pub debug: AtomicU32, - /// gc.garbage list (uncollectable objects with __del__) - pub garbage: PyMutex>, - /// gc.callbacks list - pub callbacks: PyMutex>, + /// Number of tracked objects per generation, across all interpreters. + /// + /// Advisory: they drive the collection threshold and `gc.get_count()`, and + /// the generation locks — not these counters — order the list changes they + /// describe. Every access is therefore relaxed, which keeps the tracking and + /// untracking of every object off the barrier path. + counts: [AtomicUsize; 3], + /// Number of frozen objects. Advisory, like `counts`. + permanent_count: AtomicUsize, /// Mutex for collection (prevents concurrent collections) collecting: PyMutex<()>, - /// Allocation counter for gen0 - alloc_count: AtomicUsize, + /// Next `gc_owner` tag to hand to an interpreter. + next_owner: AtomicU16, + /// Tags of interpreters that are gone. Their objects outlived them, so a + /// collection adopts them — tags them `GC_NO_OWNER` again — as it walks, + /// rather than leaving them for a collector that will never come. + retired: PyMutex>, } // SAFETY: All fields are either inherently Send/Sync (atomics, RwLock, Mutex) or protected by PyMutex. @@ -176,105 +336,72 @@ impl Default for GcState { impl GcState { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { - generations: [ - GcGeneration::new(2000), // young - GcGeneration::new(10), // old[0] - GcGeneration::new(0), // old[1] - ], - permanent: GcGeneration::new(0), - enabled: AtomicBool::new(true), generation_lists: [ PyRwLock::new(LinkedList::new()), PyRwLock::new(LinkedList::new()), PyRwLock::new(LinkedList::new()), ], permanent_list: PyRwLock::new(LinkedList::new()), - debug: AtomicU32::new(0), - garbage: PyMutex::new(Vec::new()), - callbacks: PyMutex::new(Vec::new()), + counts: [ + AtomicUsize::new(0), + AtomicUsize::new(0), + AtomicUsize::new(0), + ], + permanent_count: AtomicUsize::new(0), collecting: PyMutex::new(()), - alloc_count: AtomicUsize::new(0), + next_owner: AtomicU16::new(GC_NO_OWNER + 1), + retired: PyMutex::new(Vec::new()), } } - /// Check if GC is enabled - pub fn is_enabled(&self) -> bool { - self.enabled.load(Ordering::SeqCst) - } - - /// Enable GC - pub fn enable(&self) { - self.enabled.store(true, Ordering::SeqCst); - } - - /// Disable GC - pub fn disable(&self) { - self.enabled.store(false, Ordering::SeqCst); - } - - /// Get debug flags - pub fn get_debug(&self) -> GcDebugFlags { - GcDebugFlags::from_bits_truncate(self.debug.load(Ordering::SeqCst)) - } - - /// Set debug flags - pub fn set_debug(&self, flags: GcDebugFlags) { - self.debug.store(flags.bits(), Ordering::SeqCst); - } - - /// Get thresholds for all generations - pub fn get_threshold(&self) -> (u32, u32, u32) { - ( - self.generations[0].threshold(), - self.generations[1].threshold(), - self.generations[2].threshold(), - ) + /// Reserve a tag for a new interpreter. Tags are never reused; exhausting + /// the tag space falls back to `GC_NO_OWNER`, which costs isolation but + /// stays correct, rather than aliasing a live interpreter. + fn alloc_owner(&self) -> GcOwner { + self.next_owner + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + next.checked_add(1) + }) + .unwrap_or(GC_NO_OWNER) } - /// Set thresholds - pub fn set_threshold(&self, t0: u32, t1: Option, t2: Option) { - self.generations[0].set_threshold(t0); - if let Some(t1) = t1 { - self.generations[1].set_threshold(t1); - } - if let Some(t2) = t2 { - self.generations[2].set_threshold(t2); + /// Record that `owner`'s interpreter is gone, so the next collection adopts + /// whatever it left behind. Retagging the objects here would mean walking + /// every list under an interpreter drop, which happens while a collection + /// holds the collecting lock. + fn retire_owner(&self, owner: GcOwner) { + if owner == GC_NO_OWNER { + return; } + self.retired.lock().push(owner); } - /// Get counts for all generations + /// Get counts for all generations. Tracked objects are shared, so these are + /// process-wide even though the thresholds they are compared against are + /// per interpreter. pub fn get_count(&self) -> (usize, usize, usize) { ( - self.generations[0].count(), - self.generations[1].count(), - self.generations[2].count(), + self.counts[0].load(Ordering::Relaxed), + self.counts[1].load(Ordering::Relaxed), + self.counts[2].load(Ordering::Relaxed), ) } - /// Get statistics for all generations - pub fn get_stats(&self) -> [GcStats; 3] { - [ - self.generations[0].stats(), - self.generations[1].stats(), - self.generations[2].stats(), - ] - } - - /// Track a new object (add to gen0). + /// Track a new object (add to gen0) as owned by `owner`. /// O(1) — intrusive linked list push_front, no hashing. /// /// # Safety /// obj must be a valid pointer to a PyObject - pub unsafe fn track_object(&self, obj: NonNull) { + pub unsafe fn track_object(&self, obj: NonNull, owner: GcOwner) { let obj_ref = unsafe { obj.as_ref() }; obj_ref.set_gc_tracked(); obj_ref.set_gc_generation(0); + obj_ref.set_gc_owner(owner); self.generation_lists[0].write().push_front(obj); - self.generations[0].count.fetch_add(1, Ordering::SeqCst); - self.alloc_count.fetch_add(1, Ordering::SeqCst); + self.counts[0].fetch_add(1, Ordering::Relaxed); } /// Untrack an object (remove from GC lists). @@ -293,10 +420,10 @@ impl GcState { ( &self.generation_lists[obj_gen as usize] as &PyRwLock>, - &self.generations[obj_gen as usize].count, + &self.counts[obj_gen as usize], ) } else if obj_gen == GC_PERMANENT { - (&self.permanent_list, &self.permanent.count) + (&self.permanent_list, &self.permanent_count) } else { return; // GC_UNTRACKED or unknown — already untracked }; @@ -308,7 +435,7 @@ impl GcState { continue; // Retry with the updated generation } if unsafe { list.remove(obj) }.is_some() { - count.fetch_sub(1, Ordering::SeqCst); + release_count(count); obj_ref.clear_gc_tracked(); obj_ref.set_gc_generation(GC_UNTRACKED); } else { @@ -326,14 +453,18 @@ impl GcState { } } - /// Get tracked objects (for gc.get_objects) - /// If generation is None, returns all tracked objects. - /// If generation is Some(n), returns objects in generation n only. - pub fn get_objects(&self, generation: Option) -> Vec { + /// Get the objects `owner` tracks (for gc.get_objects), plus the ones no + /// interpreter owns. + /// If generation is None, returns all such objects. + /// If generation is Some(n), returns those in generation n only. + pub fn get_objects(&self, generation: Option, owner: GcOwner) -> Vec { fn collect_from_list( list: &LinkedList, + owner: GcOwner, ) -> impl Iterator + '_ { - list.iter().filter_map(|obj| obj.try_to_owned()) + list.iter() + .filter(move |obj| is_owned_by(obj, owner)) + .filter_map(|obj| obj.try_to_owned()) } match generation { @@ -341,14 +472,14 @@ impl GcState { // Return all tracked objects from all generations + permanent let mut result = Vec::new(); for gen_list in &self.generation_lists { - result.extend(collect_from_list(&gen_list.read())); + result.extend(collect_from_list(&gen_list.read(), owner)); } - result.extend(collect_from_list(&self.permanent_list.read())); + result.extend(collect_from_list(&self.permanent_list.read(), owner)); result } Some(g) if (0..=2).contains(&g) => { let guard = self.generation_lists[g as usize].read(); - collect_from_list(&guard).collect() + collect_from_list(&guard, owner).collect() } _ => Vec::new(), } @@ -357,34 +488,44 @@ impl GcState { /// Check if automatic GC should run and run it if needed. /// Called after object allocation. /// Returns true if GC was run, false otherwise. - pub fn maybe_collect(&self) -> bool { - if !self.is_enabled() { + fn maybe_collect(&self, gc: &GcInterpreterState) -> bool { + if !gc.is_enabled() { return false; } // Check gen0 threshold - let count0 = self.generations[0].count.load(Ordering::SeqCst) as u32; - let threshold0 = self.generations[0].threshold(); + let count0 = self.counts[0].load(Ordering::Relaxed) as u32; + let threshold0 = gc.generations[0].threshold(); if threshold0 > 0 && count0 >= threshold0 { - self.collect(0); - return true; + #[cfg(feature = "threading")] + { + // Defer to the next bytecode safepoint. Collecting here would + // stop the world while this thread may hold an internal lock + // (e.g. a lazily-initialized frame locals cell) that another + // thread is blocked on with no way to reach a safepoint — + // a deadlock. At a safepoint no such lock is held. + crate::signal::schedule_gc(); + return false; + } + // Without threading there is no safepoint to defer to and no other + // thread whose frames could be read mid-mutation, so collect inline. + #[cfg(not(feature = "threading"))] + { + self.collect_inner(gc, 0, false); + return true; + } } false } - /// Perform garbage collection on the given generation - pub fn collect(&self, generation: usize) -> CollectResult { - self.collect_inner(generation, false) - } - - /// Force collection even if GC is disabled (for manual gc.collect() calls) - pub fn collect_force(&self, generation: usize) -> CollectResult { - self.collect_inner(generation, true) - } - - fn collect_inner(&self, generation: usize, force: bool) -> CollectResult { - if !force && !self.is_enabled() { + fn collect_inner( + &self, + gc: &GcInterpreterState, + generation: usize, + force: bool, + ) -> CollectResult { + if !force && !gc.is_enabled() { return CollectResult::default(); } @@ -393,46 +534,115 @@ impl GcState { return CollectResult::default(); }; - #[cfg(not(target_arch = "wasm32"))] - let start_time = std::time::Instant::now(); - #[cfg(target_arch = "wasm32")] - let start_time = (); + let start_time = cfg_select! { + target_arch = "wasm32" => (), + _ => std::time::Instant::now(), + }; // Memory barrier to ensure visibility of all reference count updates // from other threads before we start analyzing the object graph. core::sync::atomic::fence(Ordering::SeqCst); let generation = generation.min(2); - let debug = self.get_debug(); + let debug = gc.get_debug(); // Clear the method cache to release strong references that // might prevent cycle collection (_PyType_ClearCache). crate::builtins::type_::type_cache_clear(); + // Backstop for QSBR reclamation (threads may have missed requests). + #[cfg(feature = "threading")] + crate::object::qsbr::QSBR.process(); + + // Stop the world before reading any tracked object's interpreter + // state. Requested *before* the generation read locks are taken: a + // thread parking at a safepoint may still hold a generation write lock + // (track/untrack/promote) and must be able to release it to reach the + // safepoint. It could not do so if this thread already held a read + // lock it was waiting behind — hence the ordering. + // + // Auto-collection is deferred to a bytecode safepoint (see + // `maybe_collect`), where no internal lock is held, so it never stops + // the world under a lock. Explicit `gc.collect()` runs synchronously + // here; a re-entrant call from a finalizer during an in-progress + // collection is turned into a no-op by the `collecting` try_lock above. + // The one residual is an explicit `gc.collect()` reached from a + // finalizer/`__del__` that runs inline while a non-generation internal + // lock is still held (e.g. a container write lock during element + // replacement) with another thread blocked on that same lock: stopping + // the world then waits for a thread that cannot reach a safepoint. + // Closing it fully would require making those locks stop-the-world + // aware; the exclusion above only serializes the fork/GC requesters. + #[cfg(feature = "threading")] + let mut stw = CollectStopTheWorld::new(); + // Step 1: Gather objects from generations 0..=generation // Hold read locks for the entire scan to prevent concurrent modifications. let gen_locks: Vec<_> = (0..=generation) .map(|i| self.generation_lists[i].read()) .collect(); - let mut collecting: HashSet = HashSet::new(); + // Only this interpreter's objects, plus the ones no interpreter owns. + // Another interpreter's objects stay out of the candidate set, so they + // act as external roots: anything they reference survives this pass. + let owner = gc.owner; + // Sorted so that the test below, which every scanned object pays for, + // stays logarithmic in the number of interpreters that have been + // dropped instead of linear. + let retired = { + let mut retired = self.retired.lock().clone(); + retired.sort_unstable(); + retired + }; + // The candidates and their reference counts go in one table, not a set + // beside a map: every edge in the heap is looked up here, and the two + // held the same keys, so a second table only bought a second hash of + // the same address. `candidate_ptrs` keeps them in a walkable order, + // since the counts are written while the candidates are read. + let mut gc_refs: GcMap = GcMap::default(); + let mut candidate_ptrs: Vec = Vec::new(); for gen_list in &gen_locks { for obj in gen_list.iter() { - if obj.strong_count() > 0 { - collecting.insert(GcPtr(NonNull::from(obj))); + if retired.binary_search(&obj.gc_owner()).is_ok() { + obj.set_gc_owner(GC_NO_OWNER); + } + let strong_count = obj.strong_count(); + let ptr = GcPtr(NonNull::from(obj)); + if strong_count > 0 + && is_owned_by(obj, owner) + && gc_refs.insert(ptr, strong_count).is_none() + { + candidate_ptrs.push(ptr); + } + } + } + + // A full collection is the only one that sees every generation, so it + // is where adoption finishes and the tags stop being tracked. + if generation == 2 && !retired.is_empty() { + for obj in self.permanent_list.read().iter() { + if retired.binary_search(&obj.gc_owner()).is_ok() { + obj.set_gc_owner(GC_NO_OWNER); } } + // Only the tags this scan saw: one retired while it ran still has + // objects nobody has adopted. + self.retired + .lock() + .retain(|tag| retired.binary_search(tag).is_err()); } - if collecting.is_empty() { + if candidate_ptrs.is_empty() { // Reset counts for generations whose objects were promoted away. // For gen2 (oldest), survivors stay in-place so don't reset gen2 count. let reset_end = if generation >= 2 { 2 } else { generation + 1 }; for i in 0..reset_end { - self.generations[i].count.store(0, Ordering::SeqCst); + self.counts[i].store(0, Ordering::Relaxed); } - let duration = elapsed_secs(&start_time); - self.generations[generation].update_stats(0, 0, 0, duration); + + let duration = elapsed_secs(start_time); + + gc.generations[generation].update_stats(0, 0, 0, duration); return CollectResult { collected: 0, uncollectable: 0, @@ -441,21 +651,10 @@ impl GcState { }; } - let candidates = collecting.len(); + let candidates = candidate_ptrs.len(); if debug.contains(GcDebugFlags::STATS) { - eprintln!( - "gc: collecting {} objects from generations 0..={}", - collecting.len(), - generation - ); - } - - // Step 2: Build gc_refs map (copy reference counts) - let mut gc_refs: std::collections::HashMap = std::collections::HashMap::new(); - for &ptr in &collecting { - let obj = unsafe { ptr.0.as_ref() }; - gc_refs.insert(ptr, obj.strong_count()); + eprintln!("gc: collecting {candidates} objects from generations 0..={generation}"); } // Step 3: Subtract internal references @@ -464,29 +663,37 @@ impl GcState { // of each object's children. Without this, a dict whose write lock is // held during one traversal but not the other can yield inconsistent // results, causing live objects to be incorrectly collected. - let mut referents_map: std::collections::HashMap>> = - std::collections::HashMap::new(); - for &ptr in &collecting { + // + // Every object's referents go in one buffer, with each object holding + // the range that is its own: a vector each would be an allocation per + // tracked object, and the collection wants them all at once anyway. + let mut referent_ptrs: Vec> = Vec::new(); + let mut referent_ranges: GcMap = GcMap::default(); + + for &ptr in &candidate_ptrs { let obj = unsafe { ptr.0.as_ref() }; if obj.strong_count() == 0 { continue; } - let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() }; - referents_map.insert(ptr, referent_ptrs.clone()); - for child_ptr in referent_ptrs { - let gc_ptr = GcPtr(child_ptr); - if collecting.contains(&gc_ptr) - && let Some(refs) = gc_refs.get_mut(&gc_ptr) - { + let start = referent_ptrs.len(); + unsafe { obj.gc_extend_referent_ptrs(&mut referent_ptrs) }; + let end = referent_ptrs.len(); + for &child_ptr in &referent_ptrs[start..end] { + if let Some(refs) = gc_refs.get_mut(&GcPtr(child_ptr)) { *refs = refs.saturating_sub(1); } } + referent_ranges.insert(ptr, (start, end)); } // Step 4: Find reachable objects (gc_refs > 0) and traverse from them - let mut reachable: HashSet = HashSet::new(); + let mut reachable: GcSet = GcSet::default(); let mut worklist: Vec = Vec::new(); + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&ptr, &refs) in &gc_refs { if refs > 0 { reachable.insert(ptr); @@ -497,16 +704,21 @@ impl GcState { while let Some(ptr) = worklist.pop() { let obj = unsafe { ptr.0.as_ref() }; if obj.is_gc_tracked() { - // Reuse the pre-computed referent pointers from step 3. - // For objects that were skipped in step 3 (strong_count was 0), - // compute them now as a fallback. - let referent_ptrs = referents_map - .get(&ptr) - .cloned() - .unwrap_or_else(|| unsafe { obj.gc_get_referent_ptrs() }); - for child_ptr in referent_ptrs { + // Reuse the pre-computed referent pointers from step 3, in + // place: copying them out again costs a second pass over every + // edge in the heap. Objects skipped in step 3 (strong_count was + // 0) have none stored and are traversed here instead. + let computed; + let children: &[NonNull] = match referent_ranges.get(&ptr) { + Some(&(start, end)) => &referent_ptrs[start..end], + None => { + computed = unsafe { obj.gc_get_referent_ptrs() }; + &computed + } + }; + for &child_ptr in children { let gc_ptr = GcPtr(child_ptr); - if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) { + if gc_refs.contains_key(&gc_ptr) && reachable.insert(gc_ptr) { worklist.push(gc_ptr); } } @@ -514,7 +726,38 @@ impl GcState { } // Step 5: Find unreachable objects - let unreachable: Vec = collecting.difference(&reachable).copied().collect(); + let unreachable: Vec = candidate_ptrs + .iter() + .filter(|ptr| !reachable.contains(ptr)) + .copied() + .collect(); + + // With the world stopped, every frame on any thread's call stack is a + // live root that is externally referenced and must have been + // classified reachable. A running frame appearing in `unreachable` + // would mean the reachability analysis observed its interpreter state + // as garbage — the exact hazard the barrier exists to prevent. + // Verify no running frame is classified unreachable. + // Walk the TLS frame chain (CURRENT_FRAME) instead of top_frame, + // because stack-allocated frames update only CURRENT_FRAME (via + // set_current_frame_nosave), not top_frame. + #[cfg(all(unix, feature = "threading", debug_assertions))] + if stw.is_stopped() { + let unreachable_set: GcSet = unreachable.iter().copied().collect(); + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let iframe = unsafe { &*cur }; + if let Some(fo) = iframe.frame_obj() { + let obj = fo.as_object(); + let ptr = GcPtr(NonNull::from(obj)); + debug_assert!( + !unreachable_set.contains(&ptr), + "running frame {obj:p} classified unreachable during GC" + ); + } + cur = iframe.previous(); + } + } if debug.contains(GcDebugFlags::STATS) { eprintln!( @@ -549,15 +792,25 @@ impl GcState { }) .collect(); + // The pointer-reading phases are done: strong references now pin every + // survivor and unreachable object, so the remaining phases can run with + // the world restarted. Finalizers and tp_clear must not run under + // stop-the-world — they execute arbitrary Python — and they only touch + // dead/husk objects, never a running frame. + #[cfg(feature = "threading")] + stw.restart(); + if unreachable.is_empty() { drop(gen_locks); self.promote_survivors(generation, &survivor_refs); let reset_end = if generation >= 2 { 2 } else { generation + 1 }; for i in 0..reset_end { - self.generations[i].count.store(0, Ordering::SeqCst); + self.counts[i].store(0, Ordering::Relaxed); } - let duration = elapsed_secs(&start_time); - self.generations[generation].update_stats(0, 0, candidates, duration); + + let duration = elapsed_secs(start_time); + + gc.generations[generation].update_stats(0, 0, candidates, duration); return CollectResult { collected: 0, uncollectable: 0, @@ -575,10 +828,12 @@ impl GcState { self.promote_survivors(generation, &survivor_refs); let reset_end = if generation >= 2 { 2 } else { generation + 1 }; for i in 0..reset_end { - self.generations[i].count.store(0, Ordering::SeqCst); + self.counts[i].store(0, Ordering::Relaxed); } - let duration = elapsed_secs(&start_time); - self.generations[generation].update_stats(0, 0, candidates, duration); + + let duration = elapsed_secs(start_time); + + gc.generations[generation].update_stats(0, 0, candidates, duration); return CollectResult { collected: 0, uncollectable: 0, @@ -588,7 +843,7 @@ impl GcState { } // 6b: Record initial strong counts (for resurrection detection) - let initial_counts: std::collections::HashMap = unreachable_refs + let initial_counts: GcMap = unreachable_refs .iter() .map(|obj| { let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref())); @@ -619,8 +874,8 @@ impl GcState { } // Detect resurrection - let mut resurrected_set: HashSet = HashSet::new(); - let unreachable_set: HashSet = unreachable.iter().copied().collect(); + let mut resurrected_set: GcSet = GcSet::default(); + let unreachable_set: GcSet = unreachable.iter().copied().collect(); for obj in &unreachable_refs { let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref())); @@ -660,7 +915,7 @@ impl GcState { // Compute collected count (exclude instance dicts in truly_dead) let collected = { - let dead_ptrs: HashSet = truly_dead + let dead_ptrs: GcSet = truly_dead .iter() .map(|obj| obj.as_ref() as *const PyObject as usize) .collect(); @@ -698,7 +953,7 @@ impl GcState { } if debug.contains(GcDebugFlags::SAVEALL) { - let mut garbage_guard = self.garbage.lock(); + let mut garbage_guard = gc.garbage.lock(); for obj_ref in &truly_dead { garbage_guard.push(obj_ref.clone()); } @@ -707,11 +962,89 @@ impl GcState { if !truly_dead.is_empty() { // Break cycles by clearing references (tp_clear) // Use deferred drop context to prevent stack overflow. - rustpython_common::refcount::with_deferred_drops(|| { + // With DEBUG_SAVEALL the objects stay reachable through + // gc.garbage, so they must not be cleared (delete_garbage + // skips tp_clear for saved objects). + let save_all = debug.contains(GcDebugFlags::SAVEALL); + + // Untrack dead objects BEFORE clearing them, mirroring the + // untrack-then-clear ordering of the refcount dealloc path. + // A cleared object (e.g. a frame husk with iframe == None) must + // never be observable through the generation lists, or another + // thread could obtain a strong reference via gc.get_objects() + // and access the cleared payload. + let mut late_resurrected: GcSet = GcSet::default(); + if !save_all { + let mut expected_counts: GcMap = GcMap::default(); + for obj_ref in &truly_dead { + let obj = obj_ref.as_ref(); + if obj.is_gc_tracked() { + unsafe { self.untrack_object(NonNull::from(obj)) }; + } + // One strong reference held by the `truly_dead` vec itself. + expected_counts.insert(GcPtr(NonNull::from(obj)), 1); + } + // With the objects out of the generation lists, no new external + // reference can appear. Count the references coming from within + // the dead set; any surplus in strong_count means another thread + // grabbed a reference before untracking (late resurrection) and + // the object must not be cleared. + let mut referents: GcMap>> = GcMap::default(); for obj_ref in &truly_dead { - if obj_ref.gc_has_clear() { - let edges = unsafe { obj_ref.gc_clear() }; - drop(edges); + let referent_ptrs = unsafe { obj_ref.gc_get_referent_ptrs() }; + for child_ptr in &referent_ptrs { + if let Some(n) = expected_counts.get_mut(&GcPtr(*child_ptr)) { + *n += 1; + } + } + referents.insert(GcPtr(NonNull::from(obj_ref.as_ref())), referent_ptrs); + } + let mut worklist: Vec = Vec::new(); + for obj_ref in &truly_dead { + let ptr = GcPtr(NonNull::from(obj_ref.as_ref())); + if obj_ref.strong_count() > expected_counts[&ptr] + && late_resurrected.insert(ptr) + { + worklist.push(ptr); + } + } + // A holder of a late-resurrected object can reach its referents, + // so everything reachable from it must stay intact as well. + while let Some(ptr) = worklist.pop() { + let Some(referent_ptrs) = referents.get(&ptr) else { + continue; + }; + for child_ptr in referent_ptrs { + let child = GcPtr(*child_ptr); + if expected_counts.contains_key(&child) && late_resurrected.insert(child) { + worklist.push(child); + } + } + } + // Re-track late-resurrected objects so a future collection can + // retry once the external references are released. + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for &ptr in &late_resurrected { + // Re-tracking a resurrected object: it keeps the owner it + // was allocated under. + let owner = unsafe { ptr.0.as_ref() }.gc_owner(); + unsafe { self.track_object(ptr.0, owner) }; + } + } + rustpython_common::refcount::with_deferred_drops(|| { + if !save_all { + for obj_ref in &truly_dead { + let obj = obj_ref.as_ref(); + if late_resurrected.contains(&GcPtr(NonNull::from(obj))) { + continue; + } + if obj.gc_has_clear() { + let edges = unsafe { obj.gc_clear() }; + drop(edges); + } } } drop(truly_dead); @@ -722,11 +1055,12 @@ impl GcState { // For gen2 (oldest), survivors stay in-place so don't reset gen2 count. let reset_end = if generation >= 2 { 2 } else { generation + 1 }; for i in 0..reset_end { - self.generations[i].count.store(0, Ordering::SeqCst); + self.counts[i].store(0, Ordering::Relaxed); } - let duration = elapsed_secs(&start_time); - self.generations[generation].update_stats(collected, 0, candidates, duration); + let duration = elapsed_secs(start_time); + + gc.generations[generation].update_stats(collected, 0, candidates, duration); CollectResult { collected, @@ -769,14 +1103,10 @@ impl GcState { } if unsafe { src.remove(ptr) }.is_some() { - self.generations[src_gen] - .count - .fetch_sub(1, Ordering::SeqCst); + release_count(&self.counts[src_gen]); dst.push_front(ptr); - self.generations[next_gen] - .count - .fetch_add(1, Ordering::SeqCst); + self.counts[next_gen].fetch_add(1, Ordering::Relaxed); obj.set_gc_generation(next_gen as u8); } @@ -786,45 +1116,66 @@ impl GcState { /// Get count of frozen objects pub fn get_freeze_count(&self) -> usize { - self.permanent.count() + self.permanent_count.load(Ordering::Relaxed) } - /// Freeze all tracked objects (move to permanent generation). + /// Freeze the objects `owner` could collect (move them to the permanent + /// generation). /// Lock order: generation_lists[i] → permanent_list (consistent with unfreeze). - pub fn freeze(&self) { + fn freeze(&self, owner: GcOwner) { let mut count = 0usize; for (gen_idx, gen_list) in self.generation_lists.iter().enumerate() { let mut list = gen_list.write(); let mut perm = self.permanent_list.write(); - while let Some(ptr) = list.pop_front() { + let moving: Vec<_> = list + .iter() + .filter(|obj| is_owned_by(obj, owner)) + .map(NonNull::from) + .collect(); + for ptr in moving { + if unsafe { list.remove(ptr) }.is_none() { + continue; + } perm.push_front(ptr); unsafe { ptr.as_ref().set_gc_generation(GC_PERMANENT) }; count += 1; + release_count(&self.counts[gen_idx]); } - self.generations[gen_idx].count.store(0, Ordering::SeqCst); } - self.permanent.count.fetch_add(count, Ordering::SeqCst); + self.permanent_count.fetch_add(count, Ordering::Relaxed); } - /// Unfreeze all objects (move from permanent to gen2). + /// Unfreeze the objects `owner` froze (move them from permanent to gen2). /// Lock order: generation_lists[2] → permanent_list (consistent with freeze). - pub fn unfreeze(&self) { + fn unfreeze(&self, owner: GcOwner) { let mut count = 0usize; { let mut gen2 = self.generation_lists[2].write(); let mut perm_list = self.permanent_list.write(); - while let Some(ptr) = perm_list.pop_front() { + let moving: Vec<_> = perm_list + .iter() + .filter(|obj| is_owned_by(obj, owner)) + .map(NonNull::from) + .collect(); + for ptr in moving { + if unsafe { perm_list.remove(ptr) }.is_none() { + continue; + } gen2.push_front(ptr); unsafe { ptr.as_ref().set_gc_generation(2) }; count += 1; } - self.permanent.count.store(0, Ordering::SeqCst); + let _ = self.permanent_count.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |permanent| Some(permanent.saturating_sub(count)), + ); } - self.generations[2].count.fetch_add(count, Ordering::SeqCst); + self.counts[2].fetch_add(count, Ordering::Relaxed); } /// Reset all locks to unlocked state after fork(). @@ -841,13 +1192,7 @@ impl GcState { unsafe { reinit_mutex_after_fork(&self.collecting); - reinit_mutex_after_fork(&self.garbage); - reinit_mutex_after_fork(&self.callbacks); - - for generation in &self.generations { - generation.reinit_stats_after_fork(); - } - self.permanent.reinit_stats_after_fork(); + reinit_mutex_after_fork(&self.retired); for rw in &self.generation_lists { reinit_rwlock_after_fork(rw); @@ -857,11 +1202,196 @@ impl GcState { } } +/// Per-interpreter garbage collector state (≈ `PyInterpreterState.gc`). +/// +/// The generation lists are process-wide (see [`GcState`]); what an interpreter +/// owns is the policy applied to them and the results — which objects its +/// collections consider, whether they run automatically, and where uncollectable +/// objects end up. +pub struct GcInterpreterState { + /// Tag written into every object this interpreter tracks. + owner: GcOwner, + /// Per-generation thresholds and statistics. + pub generations: [GcGeneration; 3], + /// GC enabled flag + enabled: AtomicBool, + /// Debug flags + debug: AtomicU32, + /// Uncollectable objects saved by this interpreter's collections, drained + /// into `py_garbage` by `gc.collect()`. + pub garbage: PyMutex>, + /// `gc.garbage` + pub py_garbage: crate::builtins::PyListRef, + /// `gc.callbacks` + pub py_callbacks: crate::builtins::PyListRef, +} + +impl GcInterpreterState { + pub fn new(ctx: &crate::vm::Context) -> Self { + Self { + owner: gc_state().alloc_owner(), + generations: [ + GcGeneration::new(2000), // young + GcGeneration::new(10), // old[0] + GcGeneration::new(0), // old[1] + ], + enabled: AtomicBool::new(true), + debug: AtomicU32::new(0), + garbage: PyMutex::new(Vec::new()), + py_garbage: ctx.new_list(Vec::new()), + py_callbacks: ctx.new_list(Vec::new()), + } + } + + /// Check if GC is enabled. + /// + /// Relaxed, like [`GcGeneration::threshold`]: it is read once per + /// allocation, and an allocation racing `gc.disable()` may use either value. + pub fn is_enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } + + /// Enable GC + pub fn enable(&self) { + self.enabled.store(true, Ordering::Relaxed); + } + + /// Disable GC + pub fn disable(&self) { + self.enabled.store(false, Ordering::Relaxed); + } + + /// Get debug flags + pub fn get_debug(&self) -> GcDebugFlags { + GcDebugFlags::from_bits_truncate(self.debug.load(Ordering::SeqCst)) + } + + /// Set debug flags + pub fn set_debug(&self, flags: GcDebugFlags) { + self.debug.store(flags.bits(), Ordering::SeqCst); + } + + /// Get thresholds for all generations + pub fn get_threshold(&self) -> (u32, u32, u32) { + ( + self.generations[0].threshold(), + self.generations[1].threshold(), + self.generations[2].threshold(), + ) + } + + /// Set thresholds + pub fn set_threshold(&self, t0: u32, t1: Option, t2: Option) { + self.generations[0].set_threshold(t0); + if let Some(t1) = t1 { + self.generations[1].set_threshold(t1); + } + if let Some(t2) = t2 { + self.generations[2].set_threshold(t2); + } + } + + /// Get statistics for all generations + pub fn get_stats(&self) -> [GcStats; 3] { + [ + self.generations[0].stats(), + self.generations[1].stats(), + self.generations[2].stats(), + ] + } + + /// Perform garbage collection on the given generation + pub fn collect(&self, generation: usize) -> CollectResult { + gc_state().collect_inner(self, generation, false) + } + + /// Force collection even if GC is disabled (for manual gc.collect() calls) + pub fn collect_force(&self, generation: usize) -> CollectResult { + gc_state().collect_inner(self, generation, true) + } + + /// The tracked objects this interpreter can reach (for gc.get_objects). + pub fn get_objects(&self, generation: Option) -> Vec { + gc_state().get_objects(generation, self.owner) + } + + /// Move the objects this interpreter could collect into the permanent + /// generation. + pub fn freeze(&self) { + gc_state().freeze(self.owner); + } + + /// Move them back out of it. + pub fn unfreeze(&self) { + gc_state().unfreeze(self.owner); + } + + /// Reset this interpreter's GC locks to unlocked state after fork(). + /// + /// # Safety + /// Must only be called after fork() in the child process when no other + /// threads exist. The calling thread must NOT hold any of these locks. + #[cfg(all(unix, feature = "threading"))] + pub unsafe fn reinit_after_fork(&self) { + unsafe { + crate::common::lock::reinit_mutex_after_fork(&self.garbage); + for generation in &self.generations { + generation.reinit_stats_after_fork(); + } + } + } +} + +impl Drop for GcInterpreterState { + fn drop(&mut self) { + // Objects this interpreter tracked can outlive it (another interpreter + // may still hold one). Clearing the tag hands them to every collection + // instead of stranding them. The tag itself is not handed back: it stays + // retired so that a later interpreter cannot inherit these objects. + gc_state().retire_owner(self.owner); + } +} + +/// The tag `track_object` should write for the interpreter running now. +#[must_use] +pub fn current_owner() -> GcOwner { + // SAFETY: the pointee is owned by the `PyGlobalState` of the VM on top of + // this thread's VM stack, which outlives the section this call runs in. + crate::vm::thread::current_gc_state().map_or(GC_NO_OWNER, |gc| unsafe { gc.as_ref() }.owner) +} + +/// Track a freshly allocated object under the interpreter running now, and let +/// it collect if the allocation pushed gen0 past its threshold. +/// +/// # Safety +/// obj must be a valid pointer to a PyObject that is not already tracked. +pub(crate) unsafe fn track_new_object(obj: NonNull) { + let state = gc_state(); + let Some(gc) = crate::vm::thread::current_gc_state() else { + // No interpreter is running: the shared context builds its own objects + // this way. They are left unowned, so every interpreter collects them. + unsafe { state.track_object(obj, GC_NO_OWNER) }; + return; + }; + // SAFETY: as in `current_owner`. + let gc = unsafe { gc.as_ref() }; + unsafe { state.track_object(obj, gc.owner) }; + state.maybe_collect(gc); +} + /// Get a reference to the GC state. /// /// In threading mode this is a true global (OnceLock). /// In non-threading mode this is thread-local, because PyRwLock/PyMutex /// use Cell-based locks that are not Sync. +/// +/// Every interpreter's tracked objects live in these lists, because untracking +/// happens in `default_dealloc`, where no interpreter is in scope to route to. +/// What a collection *acts on* is still one interpreter's own objects, selected +/// by the `gc_owner` tag; [`GcInterpreterState`] holds the rest of the state +/// that goes with that. The counts here, and so `gc.get_count()` and +/// `gc.get_freeze_count()`, stay process-wide: they measure how full these +/// lists are. pub fn gc_state() -> &'static GcState { rustpython_common::static_cell! { static GC_STATE: GcState; @@ -873,18 +1403,21 @@ pub fn gc_state() -> &'static GcState { mod tests { use super::*; + fn interpreter_state() -> GcInterpreterState { + GcInterpreterState::new(crate::vm::Context::genesis()) + } + #[test] fn gc_state_default() { - let state = GcState::new(); + let state = interpreter_state(); assert!(state.is_enabled()); assert_eq!(state.get_debug(), GcDebugFlags::empty()); assert_eq!(state.get_threshold(), (2000, 10, 0)); - assert_eq!(state.get_count(), (0, 0, 0)); } #[test] fn gc_enable_disable() { - let state = GcState::new(); + let state = interpreter_state(); assert!(state.is_enabled()); state.disable(); assert!(!state.is_enabled()); @@ -894,18 +1427,29 @@ mod tests { #[test] fn gc_threshold() { - let state = GcState::new(); + let state = interpreter_state(); state.set_threshold(100, Some(20), Some(30)); assert_eq!(state.get_threshold(), (100, 20, 30)); } #[test] fn gc_debug_flags() { - let state = GcState::new(); + let state = interpreter_state(); state.set_debug(GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE); assert_eq!( state.get_debug(), GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE ); } + + /// Live interpreters never share an owner tag, or their collections would + /// reach each other's objects. + #[test] + fn gc_owner_tags_are_distinct_while_live() { + let first = interpreter_state(); + let second = interpreter_state(); + assert_ne!(first.owner, second.owner); + assert_ne!(first.owner, GC_NO_OWNER); + assert_ne!(second.owner, GC_NO_OWNER); + } } diff --git a/crates/vm/src/getpath.rs b/crates/vm/src/getpath.rs index 38aa122db49..bd90a3d2725 100644 --- a/crates/vm/src/getpath.rs +++ b/crates/vm/src/getpath.rs @@ -121,7 +121,7 @@ pub fn init_path_config(settings: &Settings) -> Paths { // - sys.executable should be the launcher path (where user invoked Python) // - sys._base_executable should be the real Python executable let exe_dir = if let Ok(launcher) = crate::host_env::os::var("__PYVENV_LAUNCHER__") { - paths.executable = launcher.clone(); + paths.executable.clone_from(&launcher); paths.base_executable = real_executable; PathBuf::from(&launcher).parent().map(PathBuf::from) } else { @@ -152,7 +152,7 @@ pub fn init_path_config(settings: &Settings) -> Paths { paths.base_prefix = calculated_prefix; } else { // Not in venv: prefix == base_prefix - paths.prefix = calculated_prefix.clone(); + paths.prefix.clone_from(&calculated_prefix); paths.base_prefix = calculated_prefix; } @@ -163,7 +163,7 @@ pub fn init_path_config(settings: &Settings) -> Paths { } else { calculate_exec_prefix(search_dir.as_ref(), paths.prefix.as_ref()) }; - paths.base_exec_prefix = paths.base_prefix.clone(); + paths.base_exec_prefix.clone_from(&paths.base_prefix); // Step 7: Calculate base_executable (if not already set by __PYVENV_LAUNCHER__) if paths.base_executable.is_empty() { @@ -180,17 +180,30 @@ pub fn init_path_config(settings: &Settings) -> Paths { paths } -/// Get default prefix value -fn default_prefix() -> String { - std::option_env!("RUSTPYTHON_PREFIX") - .map(String::from) - .unwrap_or_else(|| { - if cfg!(windows) { - "C:".to_owned() - } else { - "/usr/local".to_owned() - } - }) +/// Get default prefix value used when landmark search fails. +/// +/// A compile-time `RUSTPYTHON_PREFIX` always wins. Otherwise POSIX uses the +/// conventional install prefix, while Windows has no meaningful compile-time +/// prefix and falls back to the executable's directory (ref: getpath.py). +/// +/// A bare drive root must never be returned on Windows: pip walks up from +/// `/Lib/site-packages` and would otherwise probe the drive root for +/// writability, which fails for standard users (see issue #8246). +fn default_prefix(exe_dir: Option<&PathBuf>) -> String { + if let Some(prefix) = std::option_env!("RUSTPYTHON_PREFIX") { + return prefix.to_owned(); + } + + if cfg!(windows) { + if let Some(dir) = exe_dir { + return dir.to_string_lossy().into_owned(); + } + // Executable directory is unknown; use a valid absolute root as a last + // resort rather than a drive-relative bare "C:". + "C:\\".to_owned() + } else { + "/usr/local".to_owned() + } } /// Detect virtual environment by looking for pyvenv.cfg @@ -262,7 +275,7 @@ fn calculate_prefix(exe_dir: Option<&PathBuf>, build_prefix: Option<&PathBuf>) - } // 4. Fallback to default - default_prefix() + default_prefix(exe_dir) } /// Calculate exec_prefix @@ -359,7 +372,7 @@ fn get_executable_path() -> Option { #[cfg(not(target_arch = "wasm32"))] { let exec_arg = env::args_os().next()?; - which::which(exec_arg).ok() + crate::host_env::fs::which(exec_arg) } #[cfg(target_arch = "wasm32")] { @@ -410,7 +423,7 @@ mod tests { #[test] fn default_prefix_basic() { - let prefix = default_prefix(); + let prefix = default_prefix(None); assert!(!prefix.is_empty()); } } diff --git a/crates/vm/src/import.rs b/crates/vm/src/import.rs index 5c418b35d67..eb07c76a201 100644 --- a/crates/vm/src/import.rs +++ b/crates/vm/src/import.rs @@ -141,7 +141,7 @@ pub fn import_file( file_path, vm.compile_opts(), ) - .map_err(|err| vm.new_syntax_error(&err, Some(content)))?; + .map_err(|err| err.into_pyexception(vm, Some(content)))?; import_code_obj(vm, module_name, code, true) } @@ -154,7 +154,7 @@ pub fn import_source(vm: &VirtualMachine, module_name: &str, content: &str) -> P "", 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) } @@ -221,12 +221,12 @@ fn remove_importlib_frames_inner( return (None, false); }; - let file_name = traceback.frame.code.source_path().as_str(); + let file_name = traceback.frame.iframe().code().source_path().as_str(); let (inner_tb, mut now_in_importlib) = remove_importlib_frames_inner(vm, traceback.next.lock().clone(), always_trim); if file_name == "_frozen_importlib" || file_name == "_frozen_importlib_external" { - if traceback.frame.code.obj_name.as_str() == "_call_with_frames_removed" { + if traceback.frame.iframe().code().obj_name.as_str() == "_call_with_frames_removed" { now_in_importlib = true; } if always_trim || now_in_importlib { diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index cca5b43457c..a15e30c34a3 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -86,12 +86,13 @@ pub mod py_io; pub mod py_serde; pub mod gc_state; -pub mod readline; +pub use rustpython_host_env::readline; pub mod recursion; pub mod scope; pub mod sequence; pub mod signal; pub mod sliceable; +pub mod sorting; pub mod stdlib; pub mod suggestion; pub mod types; @@ -108,7 +109,11 @@ pub use self::object::{ AsObject, Py, PyAtomicRef, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, PyStackRef, PyWeakRef, }; -pub use self::vm::{Context, Interpreter, InterpreterBuilder, Settings, VirtualMachine}; +pub use self::vm::runtime; +pub use self::vm::{ + Context, Interpreter, InterpreterBuilder, InterpreterInfo, InterpreterWhence, + MAIN_INTERPRETER_ID, Settings, VirtualMachine, +}; pub use rustpython_common as common; pub use rustpython_compiler_core::{bytecode, frozen}; diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 36bc0df0c74..bdacb7c5b83 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -95,9 +95,23 @@ mod trashcan { type DeallocFn = unsafe fn(*mut super::PyObject); type DeallocQueue = Vec<(*mut super::PyObject, DeallocFn)>; + /// Per-thread trashcan state. Depth and deferral queue live in one + /// thread-local so a single access reaches both fields (one `_tlv_get_addr` + /// on platforms where thread-local access is a function call). Both fields + /// are `Cell`-based so reentrant deallocation (nested `begin`/`end` triggered + /// by draining deferred objects) never holds an outstanding borrow. + struct Trashcan { + depth: Cell, + queue: Cell, + } + thread_local! { - static DEALLOC_DEPTH: Cell = const { Cell::new(0) }; - static DEALLOC_QUEUE: Cell = const { Cell::new(Vec::new()) }; + static TRASHCAN: Trashcan = const { + Trashcan { + depth: Cell::new(0), + queue: Cell::new(Vec::new()), + } + }; } /// Try to begin deallocation. Returns true if we should proceed, @@ -107,18 +121,16 @@ mod trashcan { obj: *mut super::PyObject, dealloc: unsafe fn(*mut super::PyObject), ) -> bool { - DEALLOC_DEPTH.with(|d| { - let depth = d.get(); + TRASHCAN.with(|t| { + let depth = t.depth.get(); if depth >= TRASHCAN_LIMIT { // Depth exceeded: defer this deallocation - DEALLOC_QUEUE.with(|q| { - let mut queue = q.take(); - queue.push((obj, dealloc)); - q.set(queue); - }); + let mut queue = t.queue.take(); + queue.push((obj, dealloc)); + t.queue.set(queue); false } else { - d.set(depth + 1); + t.depth.set(depth + 1); true } }) @@ -127,29 +139,30 @@ mod trashcan { /// End deallocation and process any deferred objects if at outermost level. #[inline] pub(super) unsafe fn end() { - let depth = DEALLOC_DEPTH.with(|d| { - let depth = d.get(); + TRASHCAN.with(|t| { + let depth = t.depth.get(); debug_assert!(depth > 0, "trashcan::end called without matching begin"); let depth = depth - 1; - d.set(depth); - depth - }); - if depth == 0 { - // Process deferred deallocations iteratively + t.depth.set(depth); + if depth != 0 { + return; + } + // Process deferred deallocations iteratively. The queue is set back + // before each `dealloc` call so a reentrant `begin` can push freely. loop { - let next = DEALLOC_QUEUE.with(|q| { - let mut queue = q.take(); + let next = { + let mut queue = t.queue.take(); let item = queue.pop(); - q.set(queue); + t.queue.set(queue); item - }); + }; if let Some((obj, dealloc)) = next { unsafe { dealloc(obj) }; } else { break; } } - } + }) } } @@ -161,8 +174,17 @@ pub(super) unsafe fn default_dealloc(obj: *mut PyObject) { return; // resurrected by __del__ } + // Only tracked objects take the trashcan recursion guard and untrack path. + // Untracked objects either own no children (int, float, str, ...) or, like + // non-escaped frames, are released at interpreter depth with at most one + // unguarded link before their tracked children (dicts, functions, code) + // re-enter guarded deallocation, so recursion stays bounded. A frame stored + // in an object graph is forced to escape, becoming tracked and guarded here. + // Read once and reuse for both gates below. + let tracked = obj_ref.is_gc_tracked(); + // Trashcan: limit recursive deallocation depth to prevent stack overflow - if !unsafe { trashcan::begin(obj, default_dealloc::) } { + if tracked && !unsafe { trashcan::begin(obj, default_dealloc::) } { return; // deferred to queue } @@ -171,7 +193,7 @@ pub(super) unsafe fn default_dealloc(obj: *mut PyObject) { // Untrack from GC BEFORE deallocation. // Must happen before memory is freed because intrusive list removal // reads the object's gc_pointers (prev/next). - if obj_ref.is_gc_tracked() { + if tracked { let ptr = unsafe { NonNull::new_unchecked(obj) }; unsafe { crate::gc_state::gc_state().untrack_object(ptr); @@ -188,36 +210,49 @@ pub(super) unsafe fn default_dealloc(obj: *mut PyObject) { ); } - // Try to store in freelist for reuse BEFORE tp_clear, so that - // size-based freelists (e.g. PyTuple) can read the payload directly. + // Extract child references to break circular refs (tp_clear), then drop + // them. Some payloads (e.g. FrameObject) drop children in place inside clear_fn + // instead of extracting them, so user code (`__del__`) may run here. + let mut edges = Vec::new(); + if let Some(clear_fn) = vtable.clear { + unsafe { clear_fn(obj, &mut edges) }; + } + // Drop extracted child references - may trigger recursive destruction. + drop(edges); + + // Try to store in freelist for reuse. This must happen AFTER clear_fn and + // after the extracted-children drop: both can run user code (`__del__`) + // that allocates, and `PyRef::new_ref` pops from the same thread-local + // freelist. If the husk were already in the freelist, a reentrant + // allocation could pop it and write a fresh payload into it while clear_fn + // still holds a `&mut` borrow of that payload (aliasing UB). Pushing only + // once no borrows into the payload can be live closes that window. // Only exact base types (not heaptype or structseq subtypes) go into the freelist. + // Published objects (e.g. a tuple stored as a type attribute) must skip the + // freelist: `PyRef::new_ref` would reuse the slot and overwrite the refcount + // word with a non-atomic write, racing a reader's atomic try-incref. Route + // them through `PyInner::dealloc` instead, whose QSBR hook defers the actual + // memory free until readers can no longer observe it. let typ = obj_ref.class(); let pushed = if T::HAS_FREELIST && typ.heaptype_ext.is_none() && core::ptr::eq(typ, T::class(crate::vm::Context::genesis())) + && !obj_ref.0.ref_count.is_published() { unsafe { T::freelist_push(obj) } } else { false }; - // Extract child references to break circular refs (tp_clear). - // This runs regardless of freelist push — the object's children must be released. - let mut edges = Vec::new(); - if let Some(clear_fn) = vtable.clear { - unsafe { clear_fn(obj, &mut edges) }; - } - if !pushed { // Deallocate the object memory (handles ObjExt prefix if present) unsafe { PyInner::dealloc(obj as *mut PyInner) }; } - // Drop child references - may trigger recursive destruction. - drop(edges); - // Trashcan: decrement depth and process deferred objects at outermost level - unsafe { trashcan::end() }; + if tracked { + unsafe { trashcan::end() }; + } } pub(super) unsafe fn debug_obj( x: &PyObject, @@ -268,6 +303,18 @@ bitflags::bitflags! { /// GC generation constants pub(crate) const GC_UNTRACKED: u8 = 0xFF; pub(crate) const GC_PERMANENT: u8 = 3; +/// Width of an interpreter's `gc_owner` tag. +/// +/// Sized to the padding the header alignment already forces, so the tag costs +/// no space on either pointer width. Running out of tags is not an error: an +/// interpreter that gets none uses [`GC_NO_OWNER`] and its objects stay +/// collectable by every interpreter, which is how they behaved before tagging. +pub(crate) type GcOwner = u16; + +/// `gc_owner` of an object that belongs to no single interpreter: everything +/// the shared context allocates, and anything allocated with no interpreter +/// current. Every interpreter collects these. +pub(crate) const GC_NO_OWNER: GcOwner = 0; /// Link implementation for GC intrusive linked list tracking pub(crate) struct GcLink; @@ -354,6 +401,10 @@ pub(super) struct PyInner { /// GC generation index (0-2=gen, GC_PERMANENT=permanent, GC_UNTRACKED=not tracked). /// Uses PyAtomic for interior mutability (writes happen through &self under list locks). pub(super) gc_generation: PyAtomic, + /// Interpreter that tracked this object, or `GC_NO_OWNER`. Written by + /// `track_object`; read to scope a collection to one interpreter. + /// Sits in what would otherwise be padding, so it costs no space. + pub(super) gc_owner: PyAtomic, /// Intrusive linked list pointers for GC generational tracking pub(super) gc_pointers: Pointers, @@ -363,6 +414,11 @@ pub(super) struct PyInner { } pub(crate) const SIZEOF_PYOBJECT_HEAD: usize = core::mem::size_of::>(); +// ref_count, vtable, gc_pointers (two) and typ are one word each; the gc bits, +// generation and owner share the word of padding their alignment forces. Adding +// to that group is free only while this holds. +const _: () = assert!(SIZEOF_PYOBJECT_HEAD == 6 * core::mem::size_of::()); + impl PyInner { /// Read type flags and member_count via raw pointers to avoid Stacked Borrows /// violations during bootstrap, where type objects have self-referential typ pointers. @@ -419,7 +475,7 @@ impl PyInner { impl fmt::Debug for PyInner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[PyObject {:?}]", &self.payload) + write!(f, "[PyObject {:?}]", self.payload) } } @@ -547,6 +603,18 @@ unsafe fn unlink_weakref(wrl: &WeakRefList, node: NonNull>) { } } +// try_reuse_basic_ref +unsafe fn try_reuse_weakref(ptr: *mut Py) -> Option> { + if ptr.is_null() { + return None; + } + let node = unsafe { &*ptr }; + node.0 + .ref_count + .safe_inc() + .then(|| unsafe { PyRef::from_raw(ptr) }) +} + impl WeakRefList { pub(super) fn new() -> Self { Self { @@ -561,22 +629,25 @@ impl WeakRefList { obj: &PyObject, cls: PyTypeRef, cls_is_weakref: bool, + cls_is_weakproxy: bool, callback: Option, dict: Option, ) -> PyRef { let is_generic = cls_is_weakref && callback.is_none(); + let is_generic_proxy = cls_is_weakproxy && callback.is_none(); // Try reuse under lock first (fast path, no allocation) { let _lock = weakref_lock::lock(obj as *const PyObject as usize); - if is_generic { - let generic_ptr = self.generic.load(Ordering::Relaxed); - if !generic_ptr.is_null() { - let generic = unsafe { &*generic_ptr }; - if generic.0.ref_count.safe_inc() { - return unsafe { PyRef::from_raw(generic_ptr) }; - } - } + let existing = if is_generic { + unsafe { try_reuse_weakref(self.generic.load(Ordering::Relaxed)) } + } else if is_generic_proxy { + unsafe { try_reuse_weakref(self.find_generic_proxy_ptr()) } + } else { + None + }; + if let Some(existing) = existing { + return existing; } } @@ -594,64 +665,98 @@ impl WeakRefList { // Re-acquire lock for linked list insertion let _lock = weakref_lock::lock(obj as *const PyObject as usize); - // Re-check: another thread may have inserted a generic ref while we - // were allocating outside the lock. If so, reuse it and drop ours. - if is_generic { - let generic_ptr = self.generic.load(Ordering::Relaxed); - if !generic_ptr.is_null() { - let generic = unsafe { &*generic_ptr }; - if generic.0.ref_count.safe_inc() { - // Nullify wr_object so drop_inner won't unlink an - // un-inserted node (which would corrupt the list head). - weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed); - return unsafe { PyRef::from_raw(generic_ptr) }; - } - } + // Re-check: another thread may have inserted a generic ref/proxy + // while we were allocating outside the lock. If so, reuse it and + // drop ours. + let existing = if is_generic { + unsafe { try_reuse_weakref(self.generic.load(Ordering::Relaxed)) } + } else if is_generic_proxy { + unsafe { try_reuse_weakref(self.find_generic_proxy_ptr()) } + } else { + None + }; + if let Some(existing) = existing { + // Nullify wr_object so drop_inner won't unlink an + // un-inserted node (which would corrupt the list head). + weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed); + return existing; } // Insert into linked list under stripe lock + // (insert_weakref: generic ref at head, generic proxy right after it) let node_ptr = NonNull::from(&*weak); + let after = if is_generic { + None + } else if is_generic_proxy { + NonNull::new(self.generic.load(Ordering::Relaxed)) + } else { + NonNull::new(self.find_generic_proxy_ptr()) + .or_else(|| NonNull::new(self.generic.load(Ordering::Relaxed))) + }; + match after { + Some(after) => unsafe { self.insert_after(after, node_ptr) }, + None => unsafe { self.insert_at_head(node_ptr) }, + } + if is_generic { + self.generic.store(node_ptr.as_ptr(), Ordering::Relaxed); + } + + weak + } + + unsafe fn insert_at_head(&self, node_ptr: NonNull>) { unsafe { let mut ptrs = WeakLink::pointers(node_ptr); - if is_generic { - // Generic ref goes to head (insert_head for basic ref) - let old_head = self.head.load(Ordering::Relaxed); - ptrs.as_mut().set_next(NonNull::new(old_head)); - ptrs.as_mut().set_prev(None); - if let Some(old_head) = NonNull::new(old_head) { - WeakLink::pointers(old_head) - .as_mut() - .set_prev(Some(node_ptr)); - } - self.head.store(node_ptr.as_ptr(), Ordering::Relaxed); - self.generic.store(node_ptr.as_ptr(), Ordering::Relaxed); - } else { - // Non-generic refs go after generic ref (insert_after) - let generic_ptr = self.generic.load(Ordering::Relaxed); - if let Some(after) = NonNull::new(generic_ptr) { - let after_next = WeakLink::pointers(after).as_ref().get_next(); - ptrs.as_mut().set_prev(Some(after)); - ptrs.as_mut().set_next(after_next); - WeakLink::pointers(after).as_mut().set_next(Some(node_ptr)); - if let Some(next) = after_next { - WeakLink::pointers(next).as_mut().set_prev(Some(node_ptr)); - } + let old_head = self.head.load(Ordering::Relaxed); + ptrs.as_mut().set_next(NonNull::new(old_head)); + ptrs.as_mut().set_prev(None); + if let Some(old_head) = NonNull::new(old_head) { + WeakLink::pointers(old_head) + .as_mut() + .set_prev(Some(node_ptr)); + } + self.head.store(node_ptr.as_ptr(), Ordering::Relaxed); + } + } + + unsafe fn insert_after(&self, after: NonNull>, node_ptr: NonNull>) { + unsafe { + let mut ptrs = WeakLink::pointers(node_ptr); + let after_next = WeakLink::pointers(after).as_ref().get_next(); + ptrs.as_mut().set_prev(Some(after)); + ptrs.as_mut().set_next(after_next); + WeakLink::pointers(after).as_mut().set_next(Some(node_ptr)); + if let Some(next) = after_next { + WeakLink::pointers(next).as_mut().set_prev(Some(node_ptr)); + } + } + } + + // get_basic_refs + fn find_generic_proxy_ptr(&self) -> *mut Py { + let generic_ptr = self.generic.load(Ordering::Relaxed); + let candidate_ptr = if let Some(generic_node) = NonNull::new(generic_ptr) { + unsafe { WeakLink::pointers(generic_node).as_ref().get_next() } + .map_or(ptr::null_mut(), |n| n.as_ptr()) + } else { + self.head.load(Ordering::Relaxed) + }; + match NonNull::new(candidate_ptr) { + Some(candidate) => { + let node = unsafe { candidate.as_ref() }; + let has_callback = unsafe { (&*node.0.payload.callback.get()).is_some() }; + // PyWeakref_CheckProxy: the basic-proxy slot is reserved for + // the canonical proxy type; subclasses and callback-less ref + // subclasses must not be mistaken for it. + let is_proxy = node.class().is(crate::builtins::PyWeakProxy::static_type()); + if has_callback || !is_proxy { + ptr::null_mut() } else { - // No generic ref; insert at head - let old_head = self.head.load(Ordering::Relaxed); - ptrs.as_mut().set_next(NonNull::new(old_head)); - ptrs.as_mut().set_prev(None); - if let Some(old_head) = NonNull::new(old_head) { - WeakLink::pointers(old_head) - .as_mut() - .set_prev(Some(node_ptr)); - } - self.head.store(node_ptr.as_ptr(), Ordering::Relaxed); + candidate_ptr } } + None => ptr::null_mut(), } - - weak } /// Clear all weakrefs and call their callbacks. @@ -968,6 +1073,16 @@ impl InstanceDict { self.d.read().clone() } + /// Run `f` on the dict without cloning it. + /// + /// For callers that only need to look at the dict — a predicate, a version + /// stamp — this drops the refcount round-trip [`Self::get`] pays. `f` runs + /// under the read guard, so it must not run Python or take this lock again. + #[inline] + pub(crate) fn with(&self, f: impl FnOnce(Option<&Py>) -> R) -> R { + f(self.d.read().as_deref()) + } + #[inline] pub(crate) fn set(&self, d: Option) { self.replace(d); @@ -1005,6 +1120,9 @@ impl PyInner { let has_ext = flags.has_feature(crate::types::PyTypeFlags::HAS_DICT) || member_count > 0; let has_weakref = flags.has_feature(crate::types::PyTypeFlags::HAS_WEAKREF); + // Objects published to lock-free caches keep their memory mapped + // until a QSBR grace period passes; destructors still run now. + let published = (*ptr).ref_count.is_published(); if has_ext || has_weakref { // Reconstruct the same layout used in new() @@ -1037,7 +1155,15 @@ impl PyInner { } // WeakRefList has no Drop (just raw pointers), no drop_in_place needed - alloc::alloc::dealloc(alloc_ptr, combined); + if published { + crate::object::qsbr::free_delayed(alloc_ptr, combined); + } else { + alloc::alloc::dealloc(alloc_ptr, combined); + } + } else if published { + let layout = core::alloc::Layout::new::(); + core::ptr::drop_in_place(ptr); + crate::object::qsbr::free_delayed(ptr as *mut u8, layout); } else { drop(Box::from_raw(ptr)); } @@ -1121,6 +1247,7 @@ impl PyInner { vtable: PyObjVTable::of::(), gc_bits: Radium::new(0), gc_generation: Radium::new(GC_UNTRACKED), + gc_owner: Radium::new(GC_NO_OWNER), gc_pointers: Pointers::new(), typ: PyAtomicRef::from(typ), payload, @@ -1133,6 +1260,7 @@ impl PyInner { vtable: PyObjVTable::of::(), gc_bits: Radium::new(0), gc_generation: Radium::new(GC_UNTRACKED), + gc_owner: Radium::new(GC_NO_OWNER), gc_pointers: Pointers::new(), typ: PyAtomicRef::from(typ), payload, @@ -1141,11 +1269,6 @@ impl PyInner { } } -/// Returns the allocation layout for `PyInner`, for use in freelist Drop impls. -pub(crate) const fn pyinner_layout() -> core::alloc::Layout { - core::alloc::Layout::new::>() -} - /// Thread-local freelist storage for reusing object allocations. /// /// Wraps a `Vec<*mut PyObject>`. On thread teardown, `Drop` frees raw @@ -1287,6 +1410,13 @@ impl PyObject { None } } + + /// Mark this object as published to a lock-free cache. Its memory + /// reclamation is deferred through QSBR (see `object::qsbr`) so that + /// concurrent try-incref readers never touch freed memory. + pub(crate) fn mark_cache_published(&self) { + self.0.ref_count.mark_published(); + } } impl PyObjectRef { @@ -1397,7 +1527,7 @@ impl PyObject { typ: PyTypeRef, ) -> Option> { self.weak_ref_list() - .map(|wrl| wrl.add(self, typ, true, callback, None)) + .map(|wrl| wrl.add(self, typ, true, false, callback, None)) } pub(crate) fn downgrade_with_typ( @@ -1428,13 +1558,14 @@ impl PyObject { None }; let cls_is_weakref = typ.is(vm.ctx.types.weakref_type); + let cls_is_weakproxy = typ.is(vm.ctx.types.weakproxy_type); let wrl = self.weak_ref_list().ok_or_else(|| { vm.new_type_error(format!( "cannot create weak reference to '{}' object", self.class().name() )) })?; - Ok(wrl.add(self, typ, cls_is_weakref, callback, dict)) + Ok(wrl.add(self, typ, cls_is_weakref, cls_is_weakproxy, callback, dict)) } pub fn downgrade( @@ -1517,6 +1648,28 @@ impl PyObject { self.instance_dict().and_then(|d| d.get()) } + /// Whether this object currently has an instance dict, without cloning it. + /// + /// `false` both for an object with no dict slot and for one whose slot is + /// still empty, which is what `dict().is_none()` reports. + #[inline(always)] + pub fn has_instance_dict(&self) -> bool { + self.instance_dict() + .is_some_and(|d| d.with(|dict| dict.is_some())) + } + + /// Run `f` on the instance dict without cloning it; see [`InstanceDict::with`]. + #[inline(always)] + pub(crate) fn with_instance_dict( + &self, + f: impl FnOnce(Option<&Py>) -> R, + ) -> R { + match self.instance_dict() { + Some(d) => d.with(f), + None => f(None), + } + } + /// Set the dict field. Returns `Err(dict)` if this object does not have a dict field /// in the first place. pub fn set_dict(&self, dict: Option) -> Result<(), Option> { @@ -1606,7 +1759,7 @@ impl PyObject { /// Check if the object has been finalized (__del__ already called). /// _PyGC_FINALIZED in Py_GIL_DISABLED mode. #[inline] - pub(crate) fn gc_finalized(&self) -> bool { + pub fn gc_finalized(&self) -> bool { GcBits::from_bits_retain(self.0.gc_bits.load(Ordering::Relaxed)).contains(GcBits::FINALIZED) } @@ -1636,6 +1789,20 @@ impl PyObject { self.0.gc_generation.store(generation, Ordering::Relaxed); } + /// The interpreter whose collections consider this object. + #[inline] + pub(crate) fn gc_owner(&self) -> GcOwner { + self.0.gc_owner.load(Ordering::Relaxed) + } + + /// Set the owning interpreter. Written by `track_object` before the object + /// enters a generation list, and reset to `GC_NO_OWNER` when the owning + /// interpreter goes away. + #[inline] + pub(crate) fn set_gc_owner(&self, owner: GcOwner) { + self.0.gc_owner.store(owner, Ordering::Relaxed); + } + /// _PyObject_GC_TRACK #[inline] pub(crate) fn set_gc_tracked(&self) { @@ -1790,11 +1957,20 @@ impl PyObject { /// and its contents haven't been modified. pub unsafe fn gc_get_referent_ptrs(&self) -> Vec> { let mut result = Vec::new(); + unsafe { self.gc_extend_referent_ptrs(&mut result) }; + result + } + + /// Append this object's referents to `out`, for a caller that holds many + /// objects' referents in one buffer rather than one buffer each. + /// + /// # Safety + /// Same as [`Self::gc_get_referent_ptrs`]. + pub unsafe fn gc_extend_referent_ptrs(&self, out: &mut Vec>) { // Traverse the entire object including dict and slots self.0.traverse(&mut |child: &Self| { - result.push(NonNull::from(child)); + out.push(NonNull::from(child)); }); - result } /// Pop edges from this object for cycle breaking. @@ -2085,6 +2261,20 @@ impl Py { pub fn payload(&self) -> &T { &self.0.payload } + + /// Recover the object pointer from a pointer to its `payload` field. + /// + /// # Safety + /// `payload` must point to the `payload` of a live `Py` (e.g. a `&T` + /// obtained by dereferencing a `Py`), and the object must outlive the + /// returned pointer's use. + #[inline] + #[cfg_attr(not(feature = "threading"), allow(dead_code))] + pub(crate) unsafe fn from_payload_ptr(payload: *const T) -> *const Self { + let offset = core::mem::offset_of!(PyInner, payload); + // `Py` is a newtype over `PyInner`, so their addresses coincide. + unsafe { (payload as *const u8).sub(offset) as *const Self } + } } impl ToOwned for Py { @@ -2275,13 +2465,16 @@ impl PyRef { // - HAS_TRAVERSE is true (Rust payload implements Traverse), OR // - has instance dict (user-defined class instances), OR // - heap type (all heap type instances are GC-tracked, like Py_TPFLAGS_HAVE_GC) - if ::HAS_TRAVERSE || has_dict || is_heaptype { - let gc = crate::gc_state::gc_state(); + // unless the payload opts out via NEW_REF_UNTRACKED (e.g. call frames, + // which are tracked lazily only on escape). + if (::HAS_TRAVERSE || has_dict || is_heaptype) + && !T::NEW_REF_UNTRACKED + { + // Tracks under the interpreter running now and collects if this + // allocation pushed gen0 past its threshold. unsafe { - gc.track_object(ptr.cast()); + crate::gc_state::track_new_object(ptr.cast()); } - // Check if automatic GC should run - gc.maybe_collect(); } Self { ptr } @@ -2471,7 +2664,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { static_assertions::assert_eq_align!(MaybeUninit>, PyInner); let type_payload = PyType { - base: None, + base: None.into(), bases: PyRwLock::default(), mro: PyRwLock::default(), subclasses: PyRwLock::default(), @@ -2479,9 +2672,10 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { slots: PyType::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), + abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let object_payload = PyType { - base: None, + base: None.into(), bases: PyRwLock::default(), mro: PyRwLock::default(), subclasses: PyRwLock::default(), @@ -2489,6 +2683,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { slots: object::PyBaseObject::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), + abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; // Both type_type and object_type are instances of `type`, which has // HAS_DICT and HAS_WEAKREF, so they need both ObjExt and WeakRefList prefixes. @@ -2527,6 +2722,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { vtable: PyObjVTable::of::(), gc_bits: Radium::new(0), gc_generation: Radium::new(GC_UNTRACKED), + gc_owner: Radium::new(GC_NO_OWNER), gc_pointers: Pointers::new(), payload: type_payload, }, @@ -2542,6 +2738,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { vtable: PyObjVTable::of::(), gc_bits: Radium::new(0), gc_generation: Radium::new(GC_UNTRACKED), + gc_owner: Radium::new(GC_NO_OWNER), gc_pointers: Pointers::new(), payload: object_payload, }, @@ -2565,7 +2762,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { (*object_type_ptr).payload.mro = PyRwLock::new(vec![object_type.clone()]); (*type_type_ptr).payload.bases = PyRwLock::new(vec![object_type.clone()]); - (*type_type_ptr).payload.base = Some(object_type.clone()); + (*type_type_ptr).payload.base = Some(object_type.clone()).into(); let type_type = PyTypeRef::from_raw(type_type_ptr.cast()); // type's mro is [type, object] @@ -2577,7 +2774,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { }; let weakref_type = PyType { - base: Some(object_type.clone()), + base: Some(object_type.clone()).into(), bases: PyRwLock::new(vec![object_type.clone()]), mro: PyRwLock::new(vec![object_type.clone()]), subclasses: PyRwLock::default(), @@ -2585,6 +2782,7 @@ pub(crate) fn init_type_hierarchy() -> (PyTypeRef, PyTypeRef, PyTypeRef) { slots: PyWeak::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), + abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let weakref_type = PyRef::new_ref(weakref_type, type_type.clone(), None); // Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE) diff --git a/crates/vm/src/object/ext.rs b/crates/vm/src/object/ext.rs index d400de29c38..186fa8e8a84 100644 --- a/crates/vm/src/object/ext.rs +++ b/crates/vm/src/object/ext.rs @@ -269,13 +269,16 @@ cfg_select! { _ => {} } -impl fmt::Debug for PyAtomicRef { +impl fmt::Debug for PyAtomicRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "PyAtomicRef(")?; + // The stored pointer is a `Py` — the full object, header included — + // as `Deref`, `load_raw` and `swap` all read it. Formatting it as a + // bare payload would skip the header and print misaligned bytes. unsafe { self.inner .load(Ordering::Relaxed) - .cast::() + .cast::() .as_ref() .fmt(f) }?; @@ -333,7 +336,7 @@ impl PyAtomicRef { pub fn swap_to_temporary_refs(&self, pyref: PyRef, vm: &VirtualMachine) { let old = unsafe { self.swap(pyref) }; if let Some(frame) = vm.current_frame() { - frame.temporary_refs.lock().push(old.into()); + frame.iframe().cold().temporary_refs.lock().push(old.into()); } } } @@ -365,6 +368,35 @@ impl PyAtomicRef> { self.deref_ordering(ordering).map(|x| x.to_owned()) } + /// Try-incref read of the current value. + /// + /// Unlike [`Self::to_owned`], this never increfs a destructed object: + /// it uses a conditional incref and revalidates that the slot still + /// holds the same pointer. Returns `None` when the slot is empty. + /// + /// Soundness relies on published-object memory being reclaimed only + /// after a QSBR grace period (see `object::qsbr`), so the refcount + /// word of a concurrently swapped-out value stays readable. + pub fn try_to_owned(&self, ordering: Ordering) -> Option> { + loop { + let ptr = self.inner.load(ordering); + if ptr.is_null() { + return None; + } + if let Some(obj) = unsafe { PyObject::try_to_owned_from_ptr(ptr.cast::()) } { + if core::ptr::eq(self.inner.load(Ordering::Acquire), ptr) { + // SAFETY: the slot only ever stores `PyRef` values. + return Some(unsafe { obj.downcast_unchecked::() }); + } + drop(obj); + } + // Slot changed or the value was torn down mid-read; a failed + // incref with an unchanged slot is impossible (the slot's own + // strong ref keeps the value alive), so this loop progresses. + core::hint::spin_loop(); + } + } + /// # Safety /// The caller is responsible to keep the returned PyRef alive /// until no more reference can be used via PyAtomicRef::deref() @@ -380,7 +412,7 @@ impl PyAtomicRef> { return; }; if let Some(frame) = vm.current_frame() { - frame.temporary_refs.lock().push(old.into()); + frame.iframe().cold().temporary_refs.lock().push(old.into()); } } } @@ -423,7 +455,7 @@ impl PyAtomicRef { pub fn swap_to_temporary_refs(&self, obj: PyObjectRef, vm: &VirtualMachine) { let old = unsafe { self.swap(obj) }; if let Some(frame) = vm.current_frame() { - frame.temporary_refs.lock().push(old); + frame.iframe().cold().temporary_refs.lock().push(old); } } } @@ -470,7 +502,7 @@ impl PyAtomicRef> { return; }; if let Some(frame) = vm.current_frame() { - frame.temporary_refs.lock().push(old); + frame.iframe().cold().temporary_refs.lock().push(old); } } } diff --git a/crates/vm/src/object/mod.rs b/crates/vm/src/object/mod.rs index 56db97aef1d..becfcabb1d4 100644 --- a/crates/vm/src/object/mod.rs +++ b/crates/vm/src/object/mod.rs @@ -1,6 +1,7 @@ mod core; mod ext; mod payload; +pub(crate) mod qsbr; mod traverse; mod traverse_object; @@ -8,5 +9,5 @@ pub use self::core::*; pub use self::ext::*; pub use self::payload::*; pub(crate) use core::SIZEOF_PYOBJECT_HEAD; -pub(crate) use core::{GC_PERMANENT, GC_UNTRACKED, GcLink}; +pub(crate) use core::{GC_NO_OWNER, GC_PERMANENT, GC_UNTRACKED, GcLink, GcOwner}; pub use traverse::{MaybeTraverse, Traverse, TraverseFn}; diff --git a/crates/vm/src/object/payload.rs b/crates/vm/src/object/payload.rs index 349b239f79f..261b2782108 100644 --- a/crates/vm/src/object/payload.rs +++ b/crates/vm/src/object/payload.rs @@ -48,6 +48,13 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static { fn class(ctx: &Context) -> &'static Py; + /// Whether `PyRef::new_ref` skips auto-tracking this type in the GC even + /// when it would otherwise qualify (has traverse, dict, or heap type). + /// Such objects are created untracked and must be tracked explicitly if + /// and when they can become part of a reference cycle. Used by `FrameObject`, + /// which is created untracked and tracked lazily only on escape. + const NEW_REF_UNTRACKED: bool = false; + /// Whether this type has a freelist. Types with freelists require /// immediate (non-deferred) GC untracking during dealloc to prevent /// race conditions when the object is reused. @@ -58,11 +65,13 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static { /// Try to push a dead object onto this type's freelist for reuse. /// Returns true if the object was stored (caller must NOT free the memory). - /// Called before tp_clear, so the payload is still intact. + /// Called after tp_clear, so the payload is a cleared husk; implementations + /// must not rely on its pre-clear contents. /// /// # Safety - /// `obj` must be a valid pointer to a `PyInner` with refcount 0. - /// The payload is still initialized and can be read for bucket selection. + /// `obj` must be a valid pointer to a `PyInner` with refcount 0 + /// whose tp_clear has already run, with no outstanding borrows into the + /// payload (`PyRef::new_ref` may pop and reuse the husk immediately). #[inline] unsafe fn freelist_push(_obj: *mut PyObject) -> bool { false @@ -124,6 +133,34 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static { #[inline] fn into_ref_with_type(self, vm: &VirtualMachine, cls: PyTypeRef) -> PyResult> + where + Self: core::fmt::Debug, + { + self.into_ref_with_type_and_dict(vm, cls, true) + } + + /// Like `into_ref_with_type`, but leaves the instance `__dict__` unallocated + /// until the first attribute write or `__dict__` access. Only valid for types + /// whose attribute protocol materializes the dict lazily via `get_or_insert`. + #[inline] + fn into_ref_with_type_lazy_dict( + self, + vm: &VirtualMachine, + cls: PyTypeRef, + ) -> PyResult> + where + Self: core::fmt::Debug, + { + self.into_ref_with_type_and_dict(vm, cls, false) + } + + #[inline] + fn into_ref_with_type_and_dict( + self, + vm: &VirtualMachine, + cls: PyTypeRef, + eager_dict: bool, + ) -> PyResult> where Self: core::fmt::Debug, { @@ -145,7 +182,12 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static { } return Err(_into_ref_size_error(vm, &cls, exact_class)); } - Ok(self._into_ref(cls, &vm.ctx)) + let dict = if eager_dict && cls.slots.flags.has_feature(PyTypeFlags::HAS_DICT) { + Some(vm.ctx.new_dict()) + } else { + None + }; + Ok(PyRef::new_ref(self, cls, dict)) } else { #[cold] #[inline(never)] @@ -156,7 +198,7 @@ pub trait PyPayload: MaybeTraverse + PyThreadingConstraint + Sized + 'static { ) -> PyBaseExceptionRef { vm.new_type_error(format!( "'{}' is not a subtype of '{}'", - &cls.name(), + cls.name(), exact_class.name() )) } diff --git a/crates/vm/src/object/qsbr.rs b/crates/vm/src/object/qsbr.rs new file mode 100644 index 00000000000..576cadaeaed --- /dev/null +++ b/crates/vm/src/object/qsbr.rs @@ -0,0 +1,333 @@ +//! Quiescent-state-based reclamation (QSBR) for lock-free caches. +//! +//! Objects published to lock-free caches (type method cache, type +//! specialization caches) are read via borrowed pointers plus try-incref. +//! Their memory must stay mapped until every thread that could hold such a +//! borrowed pointer has passed a quiescent state. Destructors run at the +//! normal drop point; only the final deallocation is deferred. +//! +//! Mirrors _Py_qsbr (Python/qsbr.c): a global write sequence advances on +//! each retirement; each thread records the last sequence it observed at a +//! quiescent point (eval-breaker checkpoint, attach/detach). A retired +//! allocation is freed once every online thread's sequence passes its goal. + +use core::alloc::Layout; + +/// Sequence value of an offline (detached) thread. +#[cfg(feature = "threading")] +const QSBR_OFFLINE: u64 = 0; +/// Initial write sequence value. +#[cfg(feature = "threading")] +const QSBR_INITIAL: u64 = 1; +/// Write sequence increment. +#[cfg(feature = "threading")] +const QSBR_INCR: u64 = 2; + +#[cfg(feature = "threading")] +pub(crate) use threading::*; + +#[cfg(feature = "threading")] +mod threading { + use super::*; + use alloc::sync::{Arc, Weak}; + use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + use std::sync::Mutex; + + /// Per-thread QSBR state, owned by the thread's `ThreadSlot`. + pub(crate) struct QsbrSlot { + /// Last write sequence observed at a quiescent point; + /// `QSBR_OFFLINE` while the thread is detached. + seq: AtomicU64, + /// Set when this thread should pass a checkpoint (eval-breaker bit). + pub(crate) requested: AtomicBool, + } + + struct Retired { + ptr: *mut u8, + layout: Layout, + goal: u64, + } + // SAFETY: `ptr` is an exclusively owned dead allocation; only the + // processing thread touches it. + unsafe impl Send for Retired {} + + pub(crate) struct Qsbr { + /// Global write sequence (_Py_qsbr wr_seq). + wr_seq: AtomicU64, + /// Cached minimum observed read sequence (rd_seq). + rd_seq: AtomicU64, + threads: Mutex>>, + queue: Mutex>, + /// Set while the retire queue is non-empty; gates the per-instruction + /// eval-breaker check so the hot path pays only one relaxed static + /// load when nothing is pending. + pending: AtomicBool, + } + + pub(crate) static QSBR: Qsbr = Qsbr::new(); + + impl Qsbr { + const fn new() -> Self { + Self { + wr_seq: AtomicU64::new(QSBR_INITIAL), + rd_seq: AtomicU64::new(QSBR_INITIAL), + threads: Mutex::new(Vec::new()), + queue: Mutex::new(Vec::new()), + pending: AtomicBool::new(false), + } + } + + /// Whether retired allocations are pending. The hot path now reads + /// the mirrored bit in the eval-breaker word instead; this stays + /// only for unit tests that exercise local, non-global instances. + #[inline] + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn break_pending(&self) -> bool { + self.pending.load(Ordering::Relaxed) + } + + /// Mirror `pending` into the global eval-breaker word — only for + /// the global QSBR instance, so unit-test instances never touch + /// process-global state. + fn update_breaker_bit(&self, on: bool) { + if core::ptr::eq(self, &QSBR) { + if on { + crate::signal::set_qsbr_bit(); + } else { + crate::signal::clear_qsbr_bit(); + } + } + } + + /// Register the calling thread. The returned slot is stored in the + /// thread's `ThreadSlot`; dropping it unregisters the thread. + pub(crate) fn register(&self) -> Arc { + let slot = Arc::new(QsbrSlot { + seq: AtomicU64::new(self.wr_seq.load(Ordering::Acquire)), + requested: AtomicBool::new(false), + }); + self.threads.lock().unwrap().push(Arc::downgrade(&slot)); + slot + } + + /// Advance the write sequence; returns the goal a retirement must + /// wait for (_Py_qsbr_advance). + fn advance(&self) -> u64 { + self.wr_seq.fetch_add(QSBR_INCR, Ordering::AcqRel) + QSBR_INCR + } + + /// Record that the calling thread is at a quiescent point: it holds + /// no borrowed cache pointers (_Py_qsbr_quiescent_state). + pub(crate) fn quiescent_state(&self, slot: &QsbrSlot) { + slot.seq + .store(self.wr_seq.load(Ordering::Acquire), Ordering::Release); + } + + /// Mark a thread offline (detached); it no longer delays grace + /// periods (_Py_qsbr_detach). The thread must not perform lock-free + /// cache reads while offline. + pub(crate) fn offline(&self, slot: &QsbrSlot) { + slot.seq.store(QSBR_OFFLINE, Ordering::Release); + } + + /// Mark a thread online again (_Py_qsbr_attach). + pub(crate) fn online(&self, slot: &QsbrSlot) { + self.quiescent_state(slot); + } + + /// Whether every online thread has passed `goal` (_Py_qsbr_poll). + fn poll(&self, goal: u64) -> bool { + if self.rd_seq.load(Ordering::Acquire) >= goal { + return true; + } + self.poll_scan() >= goal + } + + /// Recompute the minimum sequence over all live online threads, + /// pruning dead ones. + fn poll_scan(&self) -> u64 { + let mut min_seq = self.wr_seq.load(Ordering::Acquire); + let mut threads = self.threads.lock().unwrap(); + threads.retain(|weak| match weak.upgrade() { + Some(slot) => { + let seq = slot.seq.load(Ordering::Acquire); + if seq != QSBR_OFFLINE { + min_seq = min_seq.min(seq); + } + true + } + None => false, + }); + drop(threads); + self.rd_seq.fetch_max(min_seq, Ordering::AcqRel); + min_seq + } + + /// Defer deallocation of a dead object's memory until a grace + /// period passes (_PyMem_FreeDelayed). + /// + /// # Safety + /// `ptr`/`layout` must describe an allocation whose contents have + /// been dropped and which nothing accesses afterwards except the + /// racing try-incref reads this mechanism protects against. + pub(crate) unsafe fn free_delayed(&self, ptr: *mut u8, layout: Layout) { + let goal = self.advance(); + { + let mut queue = self.queue.lock().unwrap(); + queue.push(Retired { ptr, layout, goal }); + // Set while still holding the queue lock, so this pairs with + // `process` clearing the flag under the same lock and no + // push can be left behind with the flag cleared. + self.pending.store(true, Ordering::Release); + self.update_breaker_bit(true); + } + // Ask every registered thread to pass a checkpoint. + for weak in self.threads.lock().unwrap().iter() { + if let Some(slot) = weak.upgrade() { + slot.requested.store(true, Ordering::Release); + } + } + } + + /// Free retired allocations whose grace period has passed + /// (_PyMem_ProcessDelayed). + pub(crate) fn process(&self) { + let Ok(mut queue) = self.queue.try_lock() else { + // Another thread is already processing. + return; + }; + // Goals are usually increasing in push order, but concurrent + // `free_delayed` calls can interleave their `advance()` and + // queue push, so a smaller goal can occasionally land behind a + // larger one. Free the longest prefix whose grace period has + // passed; each drained item individually passed `poll`, so this + // is sound regardless of ordering. A goal stuck behind an + // out-of-order neighbor just waits for the next checkpoint or + // GC pass, not a correctness issue. + let safe_prefix = queue + .iter() + .position(|item| !self.poll(item.goal)) + .unwrap_or(queue.len()); + for item in queue.drain(..safe_prefix) { + // SAFETY: grace period passed; no reader can hold `ptr`. + unsafe { alloc::alloc::dealloc(item.ptr, item.layout) }; + } + if queue.is_empty() { + self.pending.store(false, Ordering::Release); + self.update_breaker_bit(false); + } + } + + /// Free all retired allocations immediately. + /// + /// # Safety + /// Only sound when no other thread can be mid-read: the post-fork + /// child, or teardown after all threads exited. + #[cfg(unix)] + pub(crate) unsafe fn drain_all(&self) { + let mut queue = self.queue.lock().unwrap(); + for item in queue.drain(..) { + // SAFETY: guaranteed single-threaded by the caller. + unsafe { alloc::alloc::dealloc(item.ptr, item.layout) }; + } + self.pending.store(false, Ordering::Release); + self.update_breaker_bit(false); + } + + /// Reset after fork: drop all registered thread entries (dead + /// parent threads' slots would otherwise stay online forever and + /// stall every future grace period) and free all retired + /// allocations. + /// + /// # Safety + /// Only sound in the single-threaded post-fork child, before the + /// surviving thread re-registers. + #[cfg(unix)] + pub(crate) unsafe fn reset_after_fork(&self) { + self.threads.lock().unwrap().clear(); + // SAFETY: single-threaded child, no concurrent reader exists. + unsafe { self.drain_all() }; + } + + #[cfg(test)] + fn pending(&self) -> usize { + self.queue.lock().unwrap().len() + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn poll_requires_all_online_threads() { + let q = Qsbr::new(); + let a = q.register(); + let b = q.register(); + let goal = q.advance(); + assert!(!q.poll(goal)); + q.quiescent_state(&a); + assert!(!q.poll(goal)); + q.quiescent_state(&b); + assert!(q.poll(goal)); + } + + #[test] + fn offline_thread_does_not_delay_grace() { + let q = Qsbr::new(); + let a = q.register(); + let b = q.register(); + let goal = q.advance(); + q.quiescent_state(&a); + q.offline(&b); + assert!(q.poll(goal)); + } + + #[test] + fn dead_thread_is_pruned() { + let q = Qsbr::new(); + let a = q.register(); + let b = q.register(); + drop(b); + let goal = q.advance(); + q.quiescent_state(&a); + assert!(q.poll(goal)); + } + + #[test] + fn process_frees_only_after_grace() { + let q = Qsbr::new(); + let a = q.register(); + let layout = Layout::new::(); + let ptr = unsafe { alloc::alloc::alloc(layout) }; + unsafe { q.free_delayed(ptr, layout) }; + assert!(a.requested.load(Ordering::Acquire)); + assert!(q.break_pending()); + q.process(); + assert_eq!(q.pending(), 1); // grace period not passed yet + assert!(q.break_pending()); + q.quiescent_state(&a); + q.process(); + assert_eq!(q.pending(), 0); + assert!(!q.break_pending()); + } + } +} + +/// Defer (threading) or immediately perform (non-threading) deallocation +/// of a dead published object's memory. +/// +/// # Safety +/// Same contract as [`Qsbr::free_delayed`]. +#[inline] +pub(crate) unsafe fn free_delayed(ptr: *mut u8, layout: Layout) { + #[cfg(feature = "threading")] + unsafe { + QSBR.free_delayed(ptr, layout) + }; + #[cfg(not(feature = "threading"))] + // No concurrent readers can exist without threads. + unsafe { + alloc::alloc::dealloc(ptr, layout) + }; +} diff --git a/crates/vm/src/object/traverse.rs b/crates/vm/src/object/traverse.rs index 9a5ae324baf..d0a20d2afa7 100644 --- a/crates/vm/src/object/traverse.rs +++ b/crates/vm/src/object/traverse.rs @@ -111,19 +111,25 @@ where unsafe impl Traverse for PyRwLock { #[inline] fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { - // if can't get a lock, this means something else is holding the lock, - // but since gc stopped the world, during gc the lock is always held - // so it is safe to ignore those in gc + // A failed try_read means a writer holds the lock. Traversal runs with + // the world stopped, but a thread force-parked while DETACHED (CAS'd + // straight to SUSPENDED from native code) may still hold the write lock + // it was in the middle of taking. Skipping such an object is safe: the + // collector then does not see its outgoing edges, which only + // under-traverses and thus over-approximates liveness (a conservative + // keep-alive), never freeing a reachable object. In single-threaded + // builds a failure only reflects the current thread's own re-entrant + // read, likewise safely skipped. if let Some(inner) = self.try_read_recursive() { inner.traverse(traverse_fn) } } } -/// Safety: We can't hold lock during traverse it's child because it may cause deadlock. -/// TODO(discord9): check if this is thread-safe to do -/// (Outside of gc phase, only incref/decref will call trace, -/// and refcnt is atomic, so it should be fine?) +/// Safety: the lock is not held across visiting children to avoid a re-entrant +/// deadlock. In threading builds traversal runs under stop-the-world so no +/// other thread mutates the guarded value while we read it; in single-threaded +/// builds there is no other writer. unsafe impl Traverse for PyMutex { #[inline] fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { @@ -135,7 +141,9 @@ unsafe impl Traverse for PyMutex { } chs.iter() .map(|ch| { - // Safety: during gc, this should be fine, because nothing should write during gc's tracing? + // Safety: the world is stopped (threading builds) or the + // interpreter is single-threaded, so `ch` is not concurrently + // freed while we hand it to the tracer. let ch = unsafe { ch.as_ref() }; traverse_fn(ch); }) diff --git a/crates/vm/src/ospath.rs b/crates/vm/src/ospath.rs index f2368a28826..05f7b061159 100644 --- a/crates/vm/src/ospath.rs +++ b/crates/vm/src/ospath.rs @@ -7,6 +7,7 @@ use crate::{ convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject}, function::FsPath, }; +use core::hint::cold_path; use std::path::{Path, PathBuf}; /// path_converter @@ -149,6 +150,7 @@ impl PathConverter { if self.non_strict || memchr::memchr(b'\0', b).is_none() { Ok(()) } else { + cold_path(); Err(vm.new_value_error(format!( "{}embedded null character in {}", self.error_prefix(), diff --git a/crates/vm/src/protocol/buffer.rs b/crates/vm/src/protocol/buffer.rs index d79c5e9933d..050c568b7ac 100644 --- a/crates/vm/src/protocol/buffer.rs +++ b/crates/vm/src/protocol/buffer.rs @@ -6,14 +6,101 @@ use crate::{ common::{ borrow::{BorrowedValue, BorrowedValueMut}, lock::{MapImmutable, PyMutex, PyMutexGuard}, + rc::PyRc, }, object::PyObjectPayload, sliceable::SequenceIndexOp, }; use alloc::borrow::Cow; +use bitflags::bitflags; use core::{fmt::Debug, ops::Range}; +use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; +bitflags! { + /// Capabilities a consumer asks a buffer exporter for, the `flags` argument of + /// `bf_getbuffer` and of `__buffer__` (`PyBUF_*`). + /// + /// The composite requests are supersets of the simpler ones, so + /// [`contains`](Self::contains) answers the `REQ_*` questions an exporter asks: + /// `flags.contains(BufferFlags::C_CONTIGUOUS)` is `REQ_C_CONTIGUOUS(flags)`. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct BufferFlags: u32 { + const WRITABLE = 0x0001; + const FORMAT = 0x0004; + const ND = 0x0008; + const STRIDES = 0x0010 | Self::ND.bits(); + const C_CONTIGUOUS = 0x0020 | Self::STRIDES.bits(); + const F_CONTIGUOUS = 0x0040 | Self::STRIDES.bits(); + const ANY_CONTIGUOUS = 0x0080 | Self::STRIDES.bits(); + const INDIRECT = 0x0100 | Self::STRIDES.bits(); + } +} + +impl BufferFlags { + /// `PyBUF_SIMPLE`: a plain read-only block of bytes. + pub const SIMPLE: Self = Self::empty(); + /// `PyBUF_CONTIG` + pub const CONTIG: Self = Self::ND.union(Self::WRITABLE); + /// `PyBUF_CONTIG_RO` + pub const CONTIG_RO: Self = Self::ND; + /// `PyBUF_STRIDED` + pub const STRIDED: Self = Self::STRIDES.union(Self::WRITABLE); + /// `PyBUF_STRIDED_RO` + pub const STRIDED_RO: Self = Self::STRIDES; + /// `PyBUF_RECORDS` + pub const RECORDS: Self = Self::STRIDED.union(Self::FORMAT); + /// `PyBUF_RECORDS_RO` + pub const RECORDS_RO: Self = Self::STRIDED_RO.union(Self::FORMAT); + /// `PyBUF_FULL`: everything an exporter can describe, writable. + pub const FULL: Self = Self::INDIRECT.union(Self::WRITABLE).union(Self::FORMAT); + /// `PyBUF_FULL_RO`: everything an exporter can describe, read-only. + pub const FULL_RO: Self = Self::INDIRECT.union(Self::FORMAT); + + /// `PyBUF_READ`. Belongs to `PyMemoryView_FromMemory`, not to `bf_getbuffer`. + const MEMORY_READ: Self = Self::from_bits_retain(0x100); + /// `PyBUF_WRITE`. Belongs to `PyMemoryView_FromMemory`, not to `bf_getbuffer`. + const MEMORY_WRITE: Self = Self::from_bits_retain(0x200); + + /// Whether this request is really a `PyMemoryView_FromMemory` access mode, + /// which no exporter can serve. + #[must_use] + pub const fn is_memory_access_mode(self) -> bool { + self.bits() == Self::MEMORY_READ.bits() || self.bits() == Self::MEMORY_WRITE.bits() + } + + /// Whether the consumer demands a writable buffer. + #[must_use] + pub const fn is_writable(self) -> bool { + self.intersects(Self::WRITABLE) + } + + /// The argument checks `PyBuffer_FillInfo` performs, for exporters that hand + /// out a flat block of bytes. + pub fn fill_info_check(self, readonly: bool, vm: &VirtualMachine) -> PyResult<()> { + if self == Self::SIMPLE { + return Ok(()); + } + if self.is_memory_access_mode() { + return Err(vm.new_system_error("bad argument to internal function")); + } + self.check_writable(readonly, "Object is not writable.", vm) + } + + /// Reject a writable request against a read-only export. + pub fn check_writable( + self, + readonly: bool, + message: &str, + vm: &VirtualMachine, + ) -> PyResult<()> { + if self.is_writable() && readonly { + return Err(vm.new_buffer_error(message.to_owned())); + } + Ok(()) + } +} + pub struct BufferMethods { pub obj_bytes: fn(&PyBuffer) -> BorrowedValue<'_, [u8]>, pub obj_bytes_mut: fn(&PyBuffer) -> BorrowedValueMut<'_, [u8]>, @@ -32,13 +119,46 @@ impl Debug for BufferMethods { } } -#[derive(Debug, Clone, Traverse)] +/// One acquisition from an exporter: the state a single `bf_getbuffer` set up, +/// shared by every handle taken from it. _PyManagedBufferObject +#[derive(Debug)] +struct BufferExport { + /// Handles and raw shares that have not been given up yet. mbuf->exports + shares: AtomicCell, + /// Whether the exporter's release has already run. + /// _Py_MANAGED_BUFFER_RELEASED + released: AtomicCell, +} + +#[derive(Debug, Traverse)] pub struct PyBuffer { pub obj: PyObjectRef, #[pytraverse(skip)] pub desc: BufferDescriptor, #[pytraverse(skip)] methods: &'static BufferMethods, + #[pytraverse(skip)] + export: PyRc, + /// Whether this handle still holds its share of `export`. + #[pytraverse(skip)] + owns_share: AtomicCell, +} + +/// Cloning takes another share of the same acquisition rather than asking the +/// exporter for a new one, and the exporter's release waits for the last share. +/// mbuf_add_view +impl Clone for PyBuffer { + fn clone(&self) -> Self { + debug_assert!(!self.export.released.load()); + self.export.shares.fetch_add(1); + Self { + obj: self.obj.clone(), + desc: self.desc.clone(), + methods: self.methods, + export: self.export.clone(), + owns_share: AtomicCell::new(true), + } + } } impl PyBuffer { @@ -47,8 +167,17 @@ impl PyBuffer { #[cfg(debug_assertions)] let desc = desc.validate(); - let zelf = Self { obj, desc, methods }; - zelf.retain(); + let zelf = Self { + obj, + desc, + methods, + export: PyRc::new(BufferExport { + shares: AtomicCell::new(1), + released: AtomicCell::new(false), + }), + owns_share: AtomicCell::new(true), + }; + (zelf.methods.retain)(&zelf); zelf } @@ -78,14 +207,16 @@ impl PyBuffer { /// assume the buffer is contiguous #[must_use] pub unsafe fn contiguous_unchecked(&self) -> BorrowedValue<'_, [u8]> { - self.obj_bytes() + let range = self.desc.contiguous_range(); + BorrowedValue::map(self.obj_bytes(), |x| &x[range]) } /// # Safety /// assume the buffer is contiguous and writable #[must_use] pub unsafe fn contiguous_mut_unchecked(&self) -> BorrowedValueMut<'_, [u8]> { - self.obj_bytes_mut() + let range = self.desc.contiguous_range(); + BorrowedValueMut::map(self.obj_bytes_mut(), |x| &mut x[range]) } pub fn append_to(&self, buf: &mut Vec) { @@ -113,6 +244,18 @@ impl PyBuffer { f(v) } + /// A copy of these bytes in C order, keeping shape and format. The copy + /// borrows nothing from the exporter, so it can be read while the exporter is + /// borrowed for writing. + #[must_use] + pub fn to_contiguous(&self, vm: &VirtualMachine) -> Self { + let mut data = vec![]; + self.append_to(&mut data); + VecBuffer::from(data) + .into_ref(&vm.ctx) + .into_pybuffer_with_descriptor(self.desc.contiguous()) + } + #[must_use] pub fn obj_as(&self) -> &Py { unsafe { self.obj.downcast_unchecked_ref() } @@ -128,31 +271,87 @@ impl PyBuffer { (self.methods.obj_bytes_mut)(self) } + /// Give up this handle's share of the acquisition. PyBuffer_Release + /// + /// Idempotent: a handle that has already been released owns nothing, so + /// dropping it afterwards does nothing, like a `Py_buffer` whose `obj` was + /// cleared. + /// + /// This can run arbitrary Python through `__release_buffer__`, so no borrow + /// of the exporter may be held while a buffer is released or dropped. pub fn release(&self) { + if self.owns_share.swap(false) { + self.drop_share(); + } + } + + /// Take a share of this acquisition that no handle owns. An exporter that + /// forwards a consumer's export onto a buffer it holds itself keeps the + /// acquisition alive this way. memory_getbuf + pub(crate) fn retain_share(&self) { + self.export.shares.fetch_add(1); + } + + /// Give back a share taken by [`Self::retain_share`]. memory_releasebuf + pub(crate) fn release_share(&self) { + self.drop_share(); + } + + fn drop_share(&self) { + if self.export.shares.fetch_sub(1) == 1 { + self.finalize(); + } + } + + /// The exporter learns its export is gone, once per acquisition. mbuf_release + fn finalize(&self) { + // Latched before the hook runs, so a release re-entered from Python is + // inert. + if self.export.released.swap(true) { + return; + } + // slot_bf_releasebuffer: a Python-level `__release_buffer__` runs first, + // then the exporter's own release so export counts stay balanced. + if self.obj.class().slots.python_release_buffer.load() { + crate::builtins::memory::release_buffer_call_python(self); + } (self.methods.release)(self) } - pub fn retain(&self) { - (self.methods.retain)(self) + /// Undo an acquisition the exporter had already handed out but that could not + /// be served, without telling Python: `bf_releasebuffer` does not run when + /// `bf_getbuffer` fails. + pub(crate) fn abort_acquisition(self) { + debug_assert_eq!(self.export.shares.load(), 1); + self.owns_share.store(false); + self.export.released.store(true); + (self.methods.release)(&self); } - // drop PyBuffer without calling release - // after this function, the owner should use forget() - // or wrap PyBuffer in the ManuallyDrop to prevent drop() - pub(crate) unsafe fn drop_without_release(&mut self) { - // SAFETY: requirements forwarded from caller - unsafe { - core::ptr::drop_in_place(&mut self.obj); - core::ptr::drop_in_place(&mut self.desc); + /// A copy that owns no share: it reads the same memory, but releasing it is + /// inert and it never finalizes the acquisition. A `Py_buffer` whose `obj` is + /// NULL. + #[must_use] + pub fn detached(&self) -> Self { + Self { + obj: self.obj.clone(), + desc: self.desc.clone(), + methods: self.methods, + export: self.export.clone(), + owns_share: AtomicCell::new(false), } } } -impl<'a> TryFromBorrowedObject<'a> for PyBuffer { - fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { +impl PyBuffer { + /// Acquire a buffer from `obj`. PyObject_GetBuffer + pub fn from_object(vm: &VirtualMachine, obj: &PyObject, flags: BufferFlags) -> PyResult { + if flags.is_memory_access_mode() { + return Err(vm.new_system_error("bad argument to internal function")); + } let cls = obj.class(); - if let Some(f) = cls.slots.as_buffer { - return f(obj, vm); + if let Some(f) = cls.slots.as_buffer.load() { + return f(obj, flags, vm); } Err(vm.new_type_error(format!( "a bytes-like object is required, not '{}'", @@ -161,6 +360,26 @@ impl<'a> TryFromBorrowedObject<'a> for PyBuffer { } } +impl PyObject { + /// Whether this object's type exports the buffer protocol. PyObject_CheckBuffer + /// + /// A consumer that falls back to something else for non-buffer objects asks + /// this instead of attempting an acquisition, so that an error raised by + /// `__buffer__` is not mistaken for "not a buffer". + #[must_use] + pub fn check_buffer(&self) -> bool { + self.class().slots.as_buffer.load().is_some() + } +} + +/// The request a conversion makes when the consumer has no say in it: describe +/// the export as fully as possible, read-only. +impl<'a> TryFromBorrowedObject<'a> for PyBuffer { + fn try_from_borrowed_object(vm: &VirtualMachine, obj: &'a PyObject) -> PyResult { + Self::from_object(vm, obj, BufferFlags::FULL_RO) + } +} + impl Drop for PyBuffer { fn drop(&mut self) { self.release(); @@ -172,10 +391,20 @@ pub struct BufferDescriptor { /// product(shape) * itemsize /// bytes length, but not the length for obj_bytes() even is contiguous pub len: usize, + /// Byte position of the element at index `[0, .., 0]` within + /// [`PyBuffer::obj_bytes`], which always yields the exporter's whole memory. + /// `Py_buffer.buf` + /// + /// A view that walks backwards begins at the far end of its data, so this is + /// where addressing starts rather than a lower bound. A view with no elements + /// addresses nothing and may sit outside the exporter altogether, which is why + /// this is signed. + pub offset: isize, pub readonly: bool, pub itemsize: usize, pub format: Cow<'static, str>, - /// (shape, stride, suboffset) for each dimension + /// (shape, stride, suboffset) for each dimension. A non-zero suboffset means + /// the dimension is reached through a pointer; slicing never introduces one. pub dim_desc: Vec<(usize, isize, isize)>, // TODO: flags } @@ -185,6 +414,7 @@ impl BufferDescriptor { pub fn simple(bytes_len: usize, readonly: bool) -> Self { Self { len: bytes_len, + offset: 0, readonly, itemsize: 1, format: Cow::Borrowed("B"), @@ -201,6 +431,7 @@ impl BufferDescriptor { ) -> Self { Self { len: bytes_len, + offset: 0, readonly, itemsize, format, @@ -208,9 +439,48 @@ impl BufferDescriptor { } } + /// The descriptor an exporter hands to a consumer that asked for `flags`: + /// everything the request did not ask for is dropped. + /// + /// A `Py_buffer` drops a field by setting it to NULL and leaves the consumer to + /// reconstruct it. A descriptor has no NULL, so a dropped field is filled in + /// here with what that reconstruction would produce: `"B"` for a format, C-order + /// strides for strides, and a single dimension of `len / itemsize` items for a + /// shape. `itemsize` is never touched, so `calcsize(format)` and `itemsize` can + /// disagree on a projected descriptor — the format governs an element's width, + /// the item size governs the step — and `product(shape) * itemsize == len` + /// continues to hold. + #[must_use] + pub fn projected(&self, flags: BufferFlags) -> Self { + let mut desc = self.clone(); + if !flags.contains(BufferFlags::FORMAT) { + desc.format = Cow::Borrowed("B"); + } + if !flags.contains(BufferFlags::ND) { + // A request this flat is refused unless the layout is C-contiguous, so + // one dimension addresses the same bytes. + let shape = desc.len.checked_div(desc.itemsize).unwrap_or(0); + desc.dim_desc = vec![(shape, desc.itemsize as isize, 0)]; + } else if !flags.contains(BufferFlags::STRIDES) { + // Shape survives but strides do not, which means C order. + let mut stride = desc.itemsize as isize; + for (shape, dim_stride, suboffset) in desc.dim_desc.iter_mut().rev() { + *dim_stride = stride; + *suboffset = 0; + stride *= *shape as isize; + } + } + desc + } + #[cfg(debug_assertions)] #[must_use] pub fn validate(self) -> Self { + // Only a view with nothing to address is allowed to start outside the + // exporter. + if self.len != 0 { + debug_assert!(self.offset >= 0); + } // ndim=0 is valid for scalar types (e.g., ctypes Structure) if self.ndim() == 0 { // Empty structures (len=0) can have itemsize=0 @@ -239,6 +509,7 @@ impl BufferDescriptor { self.dim_desc.len() } + /// Whether the elements are laid out in row-major order. _IsCContiguous #[must_use] pub fn is_contiguous(&self) -> bool { if self.len == 0 { @@ -254,11 +525,76 @@ impl BufferDescriptor { true } + /// Whether the elements are laid out in column-major order. A view whose + /// dimensions are all but one of length 1 is laid out both ways at once. + /// _IsFortranContiguous + #[must_use] + pub fn is_fortran_contiguous(&self) -> bool { + if self.len == 0 { + return true; + } + let mut sd = self.itemsize; + for (shape, stride, _) in self.dim_desc.iter().copied() { + if shape > 1 && stride != sd as isize { + return false; + } + sd *= shape; + } + true + } + + /// The byte range this view occupies in [`PyBuffer::obj_bytes`], for a + /// contiguous view. + /// + /// A view with no bytes maps to the empty range at zero: its offset is + /// wherever slicing left it and need not be a position that exists. + #[must_use] + pub fn contiguous_range(&self) -> Range { + if self.len == 0 { + return 0..0; + } + debug_assert!(self.offset >= 0); + let start = self.offset as usize; + start..start + self.len + } + + /// The same shape, format and item size, laid out in C order from byte zero. + #[must_use] + pub fn contiguous(&self) -> Self { + let itemsize = self.itemsize; + let mut dim_desc = self.dim_desc.clone(); + if let Some((_, stride, suboffset)) = dim_desc.last_mut() { + *stride = itemsize as isize; + *suboffset = 0; + } + for i in (1..dim_desc.len()).rev() { + dim_desc[i - 1].1 = dim_desc[i].1 * dim_desc[i].0 as isize; + dim_desc[i - 1].2 = 0; + } + Self { + len: self.len, + offset: 0, + readonly: self.readonly, + itemsize: self.itemsize, + format: self.format.clone(), + dim_desc, + } + } + + /// Whether any dimension is reached through a pointer rather than by + /// stepping, the layout `PyBUF_INDIRECT` describes. + #[must_use] + pub fn has_suboffsets(&self) -> bool { + self.dim_desc + .iter() + .any(|(_, _, suboffset)| *suboffset != 0) + } + /// this function do not check the bound /// panic if indices.len() != ndim #[must_use] pub fn fast_position(&self, indices: &[usize]) -> isize { - let mut pos = 0; + let mut pos = self.offset; for (i, (_, stride, suboffset)) in indices .iter() .copied() @@ -271,7 +607,7 @@ impl BufferDescriptor { /// panic if indices.len() != ndim pub fn position(&self, indices: &[isize], vm: &VirtualMachine) -> PyResult { - let mut pos = 0; + let mut pos = self.offset; for (i, (shape, stride, suboffset)) in indices .iter() .copied() @@ -289,14 +625,60 @@ impl BufferDescriptor { where F: FnMut(Range), { + // A view with no bytes reaches nothing, and its offset need not be a + // position that exists, so it yields no segment at all. + if self.len == 0 { + return; + } if self.ndim() == 0 { - f(0..self.itemsize as isize); + f(self.offset..self.offset + self.itemsize as isize); return; } if try_contiguous && self.is_last_dim_contiguous() { - self._for_each_segment::<_, true>(0, 0, &mut f); + self._for_each_segment::<_, true>(self.offset, 0, &mut f); } else { - self._for_each_segment::<_, false>(0, 0, &mut f); + self._for_each_segment::<_, false>(self.offset, 0, &mut f); + } + } + + /// Visit each item's byte range with the *first* dimension varying + /// fastest, which is the order a Fortran-ordered copy is written in. + /// `for_each_segment` visits in the opposite order and can hand over whole + /// rows at once; here every item is its own range, since consecutive items + /// in this order are a row apart. + pub fn for_each_segment_fortran(&self, mut f: F) + where + F: FnMut(Range), + { + if self.len == 0 { + return; + } + if self.ndim() == 0 { + f(self.offset..self.offset + self.itemsize as isize); + return; + } + let mut indices = vec![0usize; self.ndim()]; + loop { + let pos = self.offset + + indices + .iter() + .zip_eq(self.dim_desc.iter()) + .map(|(&i, &(_, stride, suboffset))| i as isize * stride + suboffset) + .sum::(); + f(pos..pos + self.itemsize as isize); + + let mut dim = 0; + loop { + indices[dim] += 1; + if indices[dim] < self.dim_desc[dim].0 { + break; + } + indices[dim] = 0; + dim += 1; + if dim == self.ndim() { + return; + } + } } } @@ -328,14 +710,24 @@ impl BufferDescriptor { where F: FnMut(Range, Range) -> bool, { + if self.len == 0 { + return; + } if self.ndim() == 0 { - f(0..self.itemsize as isize, 0..other.itemsize as isize); + f( + self.offset..self.offset + self.itemsize as isize, + other.offset..other.offset + other.itemsize as isize, + ); return; } - if try_contiguous && self.is_last_dim_contiguous() { - self._zip_eq::<_, true>(other, 0, 0, 0, &mut f); + // last_dim_is_contiguous: the whole-run path walks both sides at once, so + // both have to be laid out that way. + let run_at_once = + try_contiguous && self.is_last_dim_contiguous() && other.is_last_dim_contiguous(); + if run_at_once { + self._zip_eq::<_, true>(other, self.offset, other.offset, 0, &mut f); } else { - self._zip_eq::<_, false>(other, 0, 0, 0, &mut f); + self._zip_eq::<_, false>(other, self.offset, other.offset, 0, &mut f); } } diff --git a/crates/vm/src/protocol/callable.rs b/crates/vm/src/protocol/callable.rs index 42d0ac194ae..5a1605d4b5e 100644 --- a/crates/vm/src/protocol/callable.rs +++ b/crates/vm/src/protocol/callable.rs @@ -151,7 +151,7 @@ pub(crate) enum TraceEvent { impl TraceEvent { /// Whether sys.settrace receives this event. #[must_use] - const fn is_trace_event(&self) -> bool { + const fn is_trace_event(self) -> bool { matches!( self, Self::Call | Self::Return | Self::Exception | Self::Line | Self::Opcode @@ -162,7 +162,7 @@ impl TraceEvent { /// In legacy_tracing.c, profile callbacks are only registered for /// PY_RETURN, PY_UNWIND, C_CALL, C_RETURN, C_RAISE. #[must_use] - const fn is_profile_event(&self) -> bool { + const fn is_profile_event(self) -> bool { matches!( self, Self::Call | Self::Return | Self::CCall | Self::CReturn | Self::CException @@ -171,7 +171,7 @@ impl TraceEvent { /// Whether this event is dispatched only when f_trace_opcodes is set. #[must_use] - pub(crate) const fn is_opcode_event(&self) -> bool { + pub(crate) const fn is_opcode_event(self) -> bool { matches!(self, Self::Opcode) } } @@ -207,7 +207,7 @@ impl VirtualMachine { event: TraceEvent, arg: Option, ) -> PyResult> { - if self.use_tracing.get() { + if self.use_tracing.get() && !self.tracing_is_suppressed() { self._trace_event_inner(event, arg) } else { Ok(None) @@ -228,12 +228,17 @@ impl VirtualMachine { let is_profile_event = event.is_profile_event(); let is_opcode_event = event.is_opcode_event(); - let Some(frame_ref) = self.current_frame() else { + let Some(frame_ref) = crate::frame::current_thread_frame_materialize(self) else { return Ok(None); }; // Opcode events are only dispatched when f_trace_opcodes is set. - if is_opcode_event && !*frame_ref.trace_opcodes.lock() { + if is_opcode_event + && !frame_ref + .iframe() + .cold_opt() + .is_some_and(|c| *c.trace_opcodes.lock()) + { return Ok(None); } @@ -247,7 +252,9 @@ impl VirtualMachine { // tracing function itself. if is_trace_event && !self.is_none(&trace_func) { self.use_tracing.set(false); + self.enter_tracing(); let res = trace_func.call(args.clone(), self); + self.leave_tracing(); self.use_tracing.set(true); match res { Ok(result) => { @@ -259,7 +266,7 @@ impl VirtualMachine { // trace_trampoline behavior: clear per-frame f_trace // and propagate the error. if let Some(frame_ref) = self.current_frame() { - *frame_ref.trace.lock() = self.ctx.none(); + *frame_ref.iframe().cold().trace.lock() = None; } return Err(e); } @@ -268,7 +275,9 @@ impl VirtualMachine { if is_profile_event && !self.is_none(&profile_func) { self.use_tracing.set(false); + self.enter_tracing(); let res = profile_func.call(args, self); + self.leave_tracing(); self.use_tracing.set(true); if res.is_err() { *self.profile_func.borrow_mut() = self.ctx.none(); diff --git a/crates/vm/src/protocol/iter.rs b/crates/vm/src/protocol/iter.rs index 2f51287b181..1aa0bcd5b13 100644 --- a/crates/vm/src/protocol/iter.rs +++ b/crates/vm/src/protocol/iter.rs @@ -16,7 +16,11 @@ where unsafe impl> Traverse for PyIter { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - self.0.borrow().traverse(tracer_fn); + // Report the iterator itself, not its referents: an owner holding a + // `PyIter` owns the iterator object, and reporting what the iterator + // points at instead leaves the iterator's own reference unaccounted + // for, so a cycle running through it is never collected. + tracer_fn(self.0.borrow()); } } diff --git a/crates/vm/src/protocol/mod.rs b/crates/vm/src/protocol/mod.rs index 411aa4dfad3..4061e06458a 100644 --- a/crates/vm/src/protocol/mod.rs +++ b/crates/vm/src/protocol/mod.rs @@ -6,7 +6,9 @@ mod number; mod object; mod sequence; -pub use buffer::{BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, VecBuffer}; +pub use buffer::{ + BufferDescriptor, BufferFlags, BufferMethods, BufferResizeGuard, PyBuffer, VecBuffer, +}; pub use callable::PyCallable; pub(crate) use callable::TraceEvent; pub use iter::{PyIter, PyIterIter, PyIterReturn}; diff --git a/crates/vm/src/protocol/number.rs b/crates/vm/src/protocol/number.rs index 86b126538ca..301499aa115 100644 --- a/crates/vm/src/protocol/number.rs +++ b/crates/vm/src/protocol/number.rs @@ -631,18 +631,6 @@ impl Deref for PyNumber<'_> { } } -impl<'a> PyNumber<'a> { - // PyNumber_Check - slots are now inherited - #[must_use] - pub fn check(obj: &PyObject) -> bool { - let methods = &obj.class().slots.as_number; - let has_number = methods.int.load().is_some() - || methods.index.load().is_some() - || methods.float.load().is_some(); - has_number || obj.downcastable::() - } -} - impl PyNumber<'_> { // PyIndex_Check #[must_use] @@ -736,6 +724,16 @@ and may be removed in a future version of Python." } }) } + + // PyNumber_Check - slots are now inherited + #[must_use] + pub fn check(obj: &PyObject) -> bool { + let methods = &obj.class().slots.as_number; + let has_number = methods.int.load().is_some() + || methods.index.load().is_some() + || methods.float.load().is_some(); + has_number || obj.downcastable::() + } } pub fn handle_bytes_to_int_err( diff --git a/crates/vm/src/protocol/object.rs b/crates/vm/src/protocol/object.rs index 37007422404..993f3442aa3 100644 --- a/crates/vm/src/protocol/object.rs +++ b/crates/vm/src/protocol/object.rs @@ -7,7 +7,7 @@ use crate::{ PyType, PyTypeRef, PyUtf8Str, int::check_int_to_str_digits, pystr::AsPyStr, }, common::{hash::PyHash, str::to_ascii}, - convert::{ToPyObject, ToPyResult}, + convert::ToPyObject, dict_inner::DictKey, function::{Either, FuncArgs, PyArithmeticValue, PySetterValue}, object::PyPayload, @@ -230,7 +230,6 @@ impl PyObject { dict: Option, vm: &VirtualMachine, ) -> PyResult> { - let name = name_str.as_wtf8(); let obj_cls = self.class(); let cls_attr_name = vm.ctx.interned_str(name_str); let cls_attr = match cls_attr_name.and_then(|name| obj_cls.get_attr(name)) { @@ -251,7 +250,9 @@ impl PyObject { let dict = dict.or_else(|| self.dict()); let attr = if let Some(dict) = dict { - dict.get_item_opt(name, vm)? + // `Py` rather than its `&Wtf8`: the key type carries the + // cached hash and compares interned keys by pointer. + dict.get_item_opt(name_str, vm)? } else { None }; @@ -694,7 +695,7 @@ impl PyObject { pub fn hash(&self, vm: &VirtualMachine) -> PyResult { if let Some(hash) = self.class().slots.hash.load() { - return hash(self, vm); + return vm.with_recursion("while hashing", || hash(self, vm)); } Err(vm.new_type_error(format!("unhashable type: '{}'", self.class().name()))) @@ -741,8 +742,8 @@ impl PyObject { } else { if self.class().fast_issubclass(vm.ctx.types.type_type) { if self.is(vm.ctx.types.type_type) { - return PyGenericAlias::from_args(self.class().to_owned(), needle, vm) - .to_pyresult(vm); + let alias = PyGenericAlias::from_args(self.class().to_owned(), needle, vm)?; + return Ok(alias.to_pyobject(vm)); } if let Some(class_getitem) = diff --git a/crates/vm/src/protocol/sequence.rs b/crates/vm/src/protocol/sequence.rs index dbef92c66a9..774e579ee62 100644 --- a/crates/vm/src/protocol/sequence.rs +++ b/crates/vm/src/protocol/sequence.rs @@ -286,7 +286,7 @@ impl PySequence<'_> { } fn _ass_slice( - &self, + self, start: isize, stop: isize, value: Option, diff --git a/crates/vm/src/py_io.rs b/crates/vm/src/py_io.rs index 5649463b30e..aa3fea8e545 100644 --- a/crates/vm/src/py_io.rs +++ b/crates/vm/src/py_io.rs @@ -1,14 +1,15 @@ +use core::{fmt, ops}; +use std::io; + use crate::{ PyObject, PyObjectRef, PyResult, VirtualMachine, builtins::{PyBaseExceptionRef, PyBytes, PyStr}, common::ascii, }; -use alloc::fmt; -use core::ops; -use std::io; pub trait Write { type Error; + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> Result<(), Self::Error>; } @@ -24,10 +25,12 @@ impl IoWriter { impl ops::Deref for IoWriter { type Target = T; + fn deref(&self) -> &T { &self.0 } } + impl ops::DerefMut for IoWriter { fn deref_mut(&mut self) -> &mut T { &mut self.0 @@ -39,6 +42,7 @@ where W: io::Write, { type Error = io::Error; + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> { ::write_fmt(&mut self.0, args) } @@ -46,6 +50,7 @@ where impl Write for String { type Error = fmt::Error; + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { ::write_fmt(self, args) } @@ -55,8 +60,10 @@ pub struct PyWriter<'vm>(pub PyObjectRef, pub &'vm VirtualMachine); impl Write for PyWriter<'_> { type Error = PyBaseExceptionRef; + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> Result<(), Self::Error> { - let PyWriter(obj, vm) = self; + let Self(obj, vm) = self; + vm.call_method(obj, "write", (args.to_string(),)).map(drop) } } @@ -70,6 +77,7 @@ pub fn file_readline(obj: &PyObject, size: Option, vm: &VirtualMachine) - vec![vm.ctx.new_str(ascii!("EOF when reading a line")).into()], ) }; + let ret = match_class!(match ret { s @ PyStr => { // Use as_wtf8() to handle strings with surrogates (e.g., surrogateescape) @@ -77,6 +85,7 @@ pub fn file_readline(obj: &PyObject, size: Option, vm: &VirtualMachine) - if s_wtf8.is_empty() { return Err(eof_err()); } + // '\n' is ASCII, so we can check bytes directly if s_wtf8.as_bytes().last() == Some(&b'\n') { let no_nl = &s_wtf8[..s_wtf8.len() - 1]; @@ -90,13 +99,14 @@ pub fn file_readline(obj: &PyObject, size: Option, vm: &VirtualMachine) - if buf.is_empty() { return Err(eof_err()); } + if buf.last() == Some(&b'\n') { vm.ctx.new_bytes(buf[..buf.len() - 1].to_owned()).into() } else { b.into() } } - _ => return Err(vm.new_type_error("object.readline() returned non-string".to_owned())), + _ => return Err(vm.new_type_error("object.readline() returned non-string")), }); Ok(ret) } diff --git a/crates/vm/src/py_serde.rs b/crates/vm/src/py_serde.rs index 945068113f1..0e8b70781cd 100644 --- a/crates/vm/src/py_serde.rs +++ b/crates/vm/src/py_serde.rs @@ -63,7 +63,10 @@ impl serde::Serialize for PyObjectSerializer<'_> { seq.end() }; if let Some(s) = self.pyobject.downcast_ref::() { - serializer.serialize_str(s.as_ref()) + serializer.serialize_str( + s.to_str() + .ok_or_else(|| serde::ser::Error::custom("str contains surrogates"))?, + ) } else if self.pyobject.fast_isinstance(self.vm.ctx.types.float_type) { serializer.serialize_f64(float::get_value(self.pyobject)) } else if self.pyobject.fast_isinstance(self.vm.ctx.types.bool_type) { @@ -111,8 +114,9 @@ pub struct PyObjectDeserializer<'c> { } impl<'c> PyObjectDeserializer<'c> { - pub fn new(vm: &'c VirtualMachine) -> Self { - PyObjectDeserializer { vm } + #[must_use] + pub const fn new(vm: &'c VirtualMachine) -> Self { + Self { vm } } } diff --git a/crates/vm/src/recursion.rs b/crates/vm/src/recursion.rs index 7392cca4ded..dea7898c8a7 100644 --- a/crates/vm/src/recursion.rs +++ b/crates/vm/src/recursion.rs @@ -1,14 +1,15 @@ use crate::{AsObject, PyObject, VirtualMachine}; +/// A guard to protect repr methods from recursion into itself. pub struct ReprGuard<'vm> { vm: &'vm VirtualMachine, id: usize, } -/// A guard to protect repr methods from recursion into itself, impl<'vm> ReprGuard<'vm> { - /// Returns None if the guard against 'obj' is still held otherwise returns the guard. The guard - /// which is released if dropped. + /// Returns None if the guard against 'obj' is still held otherwise returns the guard. + /// + /// The guard which is released if dropped. pub fn enter(vm: &'vm VirtualMachine, obj: &PyObject) -> Option { let mut guards = vm.repr_guards.borrow_mut(); @@ -18,8 +19,9 @@ impl<'vm> ReprGuard<'vm> { if guards.contains(&id) { return None; } + guards.insert(id); - Some(ReprGuard { vm, id }) + Some(Self { vm, id }) } } diff --git a/crates/vm/src/sequence.rs b/crates/vm/src/sequence.rs index 0bc12fd2631..1e126d087ea 100644 --- a/crates/vm/src/sequence.rs +++ b/crates/vm/src/sequence.rs @@ -104,7 +104,12 @@ where return Err(vm.new_memory_error("")); } - let mut v = Vec::with_capacity(n * self.as_ref().len()); + let total = n + .checked_mul(self.as_ref().len()) + .ok_or_else(|| vm.new_memory_error(""))?; + let mut v = Vec::new(); + v.try_reserve_exact(total) + .map_err(|_| vm.new_memory_error(""))?; for _ in 0..n { v.extend_from_slice(self.as_ref()); } @@ -122,6 +127,12 @@ where fn imul(&mut self, vm: &VirtualMachine, n: isize) -> PyResult<()> { let n = vm.check_repeat_or_overflow_error(self.as_ref().len(), n)?; + + if n > 1 && core::mem::size_of_val(self.as_ref()) >= MAX_MEMORY_SIZE / n { + // TODO: make a global static NoMemory shared exc object and return its reference. + return Err(vm.new_memory_error("")); + } + if n == 0 { self.as_vec_mut().clear(); } else if n != 1 { diff --git a/crates/vm/src/signal.rs b/crates/vm/src/signal.rs index eea42f4a87e..16a097ea62b 100644 --- a/crates/vm/src/signal.rs +++ b/crates/vm/src/signal.rs @@ -2,7 +2,7 @@ use core::{ cell::{Cell, RefCell}, fmt, ops::{Deref, DerefMut, Index, IndexMut, Range}, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicU8, Ordering}, }; use std::sync::mpsc; @@ -13,7 +13,21 @@ use crate::{PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, Virtual pub(crate) const NSIG: usize = 64; -static ANY_TRIGGERED: AtomicBool = AtomicBool::new(false); +/// Eval-breaker word: bit flags checked once per bytecode instruction. +/// Signal handlers and QSBR set bits with fetch_or (async-signal-safe, +/// lock-free); consumers clear only their own bit with fetch_and. +static EVAL_BREAKER: AtomicU8 = AtomicU8::new(0); + +/// A signal handler recorded a pending signal. +const SIGNAL_BIT: u8 = 1 << 0; +/// QSBR has retired allocations pending reclamation. +#[cfg(feature = "threading")] +const QSBR_BIT: u8 = 1 << 1; +/// An automatic collection was scheduled by `maybe_collect` and must run at +/// the next bytecode safepoint rather than synchronously inside the +/// allocation that tripped the threshold. +#[cfg(feature = "threading")] +const GC_BIT: u8 = 1 << 2; #[expect( clippy::declare_interior_mutable_const, @@ -49,12 +63,12 @@ pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> { // Read-only check first: avoids cache-line invalidation on every // instruction when no signal is pending (the common case). - if !ANY_TRIGGERED.load(Ordering::Relaxed) { + if EVAL_BREAKER.load(Ordering::Relaxed) & SIGNAL_BIT == 0 { return Ok(()); } // Atomic RMW only when a signal is actually pending. - if !ANY_TRIGGERED.swap(false, Ordering::Acquire) { + if EVAL_BREAKER.fetch_and(!SIGNAL_BIT, Ordering::Acquire) & SIGNAL_BIT == 0 { return Ok(()); } @@ -101,20 +115,49 @@ fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> { } pub(crate) fn set_triggered() { - ANY_TRIGGERED.store(true, Ordering::Release); + // fetch_or (not store) so a signal handler never clobbers the QSBR bit; + // this compiles to a lock-free RMW, safe to call from a signal handler. + EVAL_BREAKER.fetch_or(SIGNAL_BIT, Ordering::Release); } +/// Any eval-breaker bit pending? One relaxed load; checked per instruction. #[inline(always)] -#[cfg(not(target_arch = "wasm32"))] -pub(crate) fn is_triggered() -> bool { - ANY_TRIGGERED.load(Ordering::Relaxed) +pub(crate) fn eval_breaker_pending() -> bool { + EVAL_BREAKER.load(Ordering::Relaxed) != 0 +} + +#[cfg(feature = "threading")] +pub(crate) fn set_qsbr_bit() { + EVAL_BREAKER.fetch_or(QSBR_BIT, Ordering::Release); +} + +#[cfg(feature = "threading")] +pub(crate) fn clear_qsbr_bit() { + EVAL_BREAKER.fetch_and(!QSBR_BIT, Ordering::Release); +} + +#[cfg(feature = "threading")] +pub(crate) fn qsbr_bit_set() -> bool { + EVAL_BREAKER.load(Ordering::Relaxed) & QSBR_BIT != 0 +} + +/// Schedule an automatic collection to run at the next bytecode safepoint. +#[cfg(feature = "threading")] +pub(crate) fn schedule_gc() { + EVAL_BREAKER.fetch_or(GC_BIT, Ordering::Release); +} + +/// Clear the scheduled-GC bit, returning whether it had been set. +#[cfg(feature = "threading")] +pub(crate) fn take_gc_scheduled() -> bool { + EVAL_BREAKER.fetch_and(!GC_BIT, Ordering::Acquire) & GC_BIT != 0 } /// Reset all signal trigger state after fork in child process. /// Stale triggers from the parent must not fire in the child. #[cfg(all(unix, feature = "host_env"))] pub(crate) fn clear_after_fork() { - ANY_TRIGGERED.store(false, Ordering::Release); + EVAL_BREAKER.fetch_and(!SIGNAL_BIT, Ordering::Release); for trigger in &TRIGGERS { trigger.store(false, Ordering::Relaxed); } diff --git a/crates/vm/src/sliceable.rs b/crates/vm/src/sliceable.rs index b0f4c7808ff..ef78614efd5 100644 --- a/crates/vm/src/sliceable.rs +++ b/crates/vm/src/sliceable.rs @@ -419,6 +419,50 @@ impl SaturatedSlice { (range, self.step, slice_len) } + // PySlice_AdjustIndices, keeping the adjusted start rather than a range. + /// The index the slice begins at, clamped into `0..=len` for a positive step + /// and into `-1..=len-1` for a negative one, together with its length. + /// + /// Unlike [`Self::adjust_indices`] this stays meaningful for an empty slice, + /// where it is still the position a strided view moves to. + #[must_use] + pub fn adjust_indices_start(&self, len: usize) -> (isize, usize) { + let len = len as isize; + let clamp = |i: isize| { + if i < 0 { + let i = i.saturating_add(len); + if i < 0 { + if self.step.is_negative() { -1 } else { 0 } + } else { + i + } + } else if i >= len { + if self.step.is_negative() { + len - 1 + } else { + len + } + } else { + i + } + }; + let start = clamp(self.start); + let stop = clamp(self.stop); + let step = self.step.unsigned_abs(); + let slice_len = if self.step.is_negative() { + if stop < start { + (start - stop - 1) as usize / step + 1 + } else { + 0 + } + } else if start < stop { + (stop - start - 1) as usize / step + 1 + } else { + 0 + }; + (start, slice_len) + } + #[must_use] pub fn iter(&self, len: usize) -> SaturatedSliceIter { SaturatedSliceIter::new(self, len) diff --git a/crates/vm/src/sorting.rs b/crates/vm/src/sorting.rs new file mode 100644 index 00000000000..5f61d6624e1 --- /dev/null +++ b/crates/vm/src/sorting.rs @@ -0,0 +1,738 @@ +// TODO: MERGESTATE_TEMP_SIZE unused — buf is a dynamic Vec, not a fixed stack array. +const MIN_GALLOP: usize = 7; +const MAX_MINRUN: usize = 64; + +enum LoBreakout { + Succeed, + CopyB, +} + +enum HiBreakout { + Succeed, + CopyA, +} + +#[derive(Clone, Copy)] +struct Run { + base: usize, + len: usize, + power: u32, +} + +struct MergeState { + buf: Vec, + min_gallop: usize, + pending: Vec, +} + +impl MergeState { + fn merge_lo( + &mut self, + values: &mut [T], + is_lt: &mut F, + start_a: usize, + mut len_a: usize, + start_b: usize, + mut len_b: usize, + ) -> Result<(), E> + where + F: FnMut(&T, &T) -> Result, + { + debug_assert!(len_a > 0); + debug_assert!(len_b > 0); + debug_assert!(start_a + len_a == start_b); + + self.buf.clear(); + self.buf + .extend_from_slice(&values[start_a..start_a + len_a]); + + let mut cursor_a = 0; + let mut cursor_b = start_b; + let mut dest = start_a; + + values[dest] = values[cursor_b].clone(); + dest += 1; + cursor_b += 1; + len_b -= 1; + + if len_b == 0 { + values[dest..dest + len_a].clone_from_slice(&self.buf[cursor_a..cursor_a + len_a]); + return Ok(()); + } + if len_a == 1 { + copy_within_clone(values, cursor_b, dest, len_b); + values[dest + len_b] = self.buf[cursor_a].clone(); + return Ok(()); + } + + let mut min_gallop = self.min_gallop; + + let breakout: Result = 'merging: loop { + let mut a_count = 0; + let mut b_count = 0; + + loop { + let b_wins = match is_lt(&values[cursor_b], &self.buf[cursor_a]) { + Ok(v) => v, + Err(e) => break 'merging Err(e), + }; + if b_wins { + values[dest] = values[cursor_b].clone(); + dest += 1; + cursor_b += 1; + len_b -= 1; + b_count += 1; + a_count = 0; + if len_b == 0 { + break 'merging Ok(LoBreakout::Succeed); + } + if b_count >= min_gallop { + break; + } + } else { + values[dest] = self.buf[cursor_a].clone(); + dest += 1; + cursor_a += 1; + len_a -= 1; + a_count += 1; + b_count = 0; + if len_a == 1 { + break 'merging Ok(LoBreakout::CopyB); + } + if a_count >= min_gallop { + break; + } + } + } + + min_gallop += 1; + loop { + if min_gallop > 1 { + min_gallop -= 1; + } + self.min_gallop = min_gallop; + let mut k = + match gallop_right(&self.buf, is_lt, &values[cursor_b], cursor_a, len_a, 0) { + Ok(k) => k, + Err(e) => break 'merging Err(e), + }; + a_count = k; + if k > 0 { + values[dest..dest + k].clone_from_slice(&self.buf[cursor_a..cursor_a + k]); + dest += k; + cursor_a += k; + len_a -= k; + if len_a == 1 { + break 'merging Ok(LoBreakout::CopyB); + } + if len_a == 0 { + break 'merging Ok(LoBreakout::Succeed); + } + } + values[dest] = values[cursor_b].clone(); + dest += 1; + cursor_b += 1; + len_b -= 1; + if len_b == 0 { + break 'merging Ok(LoBreakout::Succeed); + } + k = match gallop_left(values, is_lt, &self.buf[cursor_a], cursor_b, len_b, 0) { + Ok(k) => k, + Err(e) => break 'merging Err(e), + }; + b_count = k; + if k > 0 { + copy_within_clone(values, cursor_b, dest, k); + dest += k; + cursor_b += k; + len_b -= k; + if len_b == 0 { + break 'merging Ok(LoBreakout::Succeed); + } + } + values[dest] = self.buf[cursor_a].clone(); + dest += 1; + cursor_a += 1; + len_a -= 1; + if len_a == 1 { + break 'merging Ok(LoBreakout::CopyB); + } + if a_count < MIN_GALLOP && b_count < MIN_GALLOP { + break; + } + } + + min_gallop += 1; + self.min_gallop = min_gallop; + }; + + match breakout { + Ok(LoBreakout::CopyB) => { + copy_within_clone(values, cursor_b, dest, len_b); + values[dest + len_b] = self.buf[cursor_a].clone(); + Ok(()) + } + other => { + if len_a > 0 { + values[dest..dest + len_a] + .clone_from_slice(&self.buf[cursor_a..cursor_a + len_a]); + } + other.map(|_| ()) + } + } + } + + fn merge_hi( + &mut self, + values: &mut [T], + is_lt: &mut F, + start_a: usize, + mut len_a: usize, + start_b: usize, + mut len_b: usize, + ) -> Result<(), E> + where + F: FnMut(&T, &T) -> Result, + { + debug_assert!(len_a > 0); + debug_assert!(len_b > 0); + debug_assert!(start_a + len_a == start_b); + + self.buf.clear(); + self.buf + .extend_from_slice(&values[start_b..start_b + len_b]); + + let mut dest = start_b + len_b - 1; + let mut cursor_a = start_a + len_a - 1; + let mut cursor_b = len_b - 1; + + values[dest] = values[cursor_a].clone(); + dest -= 1; + cursor_a -= 1; + len_a -= 1; + + if len_a == 0 { + values[(dest - len_b + 1)..=dest].clone_from_slice(&self.buf[0..len_b]); + return Ok(()); + } + if len_b == 1 { + let src = cursor_a + 1 - len_a; + let dst = dest + 1 - len_a; + copy_within_clone(values, src, dst, len_a); + values[dst - 1] = self.buf[cursor_b].clone(); + return Ok(()); + } + + let mut min_gallop = self.min_gallop; + let breakout: Result = 'merging: loop { + let mut a_count = 0; + let mut b_count = 0; + + loop { + let b_wins = match is_lt(&self.buf[cursor_b], &values[cursor_a]) { + Ok(v) => v, + Err(e) => break 'merging Err(e), + }; + if b_wins { + values[dest] = values[cursor_a].clone(); + dest -= 1; + len_a -= 1; + + if len_a == 0 { + break 'merging Ok(HiBreakout::Succeed); + } + + cursor_a -= 1; + a_count += 1; + b_count = 0; + + if a_count >= min_gallop { + break; + } + } else { + values[dest] = self.buf[cursor_b].clone(); + dest -= 1; + cursor_b -= 1; + len_b -= 1; + b_count += 1; + a_count = 0; + if len_b == 1 { + break 'merging Ok(HiBreakout::CopyA); + } + if b_count >= min_gallop { + break; + } + } + } + + min_gallop += 1; + loop { + if min_gallop > 1 { + min_gallop -= 1; + } + self.min_gallop = min_gallop; + let mut k = match gallop_right( + values, + is_lt, + &self.buf[cursor_b], + start_a, + len_a, + len_a - 1, + ) { + Ok(k) => k, + Err(e) => break 'merging Err(e), + }; + k = len_a - k; + a_count = k; + if k > 0 { + copy_within_clone(values, cursor_a + 1 - k, dest + 1 - k, k); + dest -= k; + len_a -= k; + if len_a == 0 { + break 'merging Ok(HiBreakout::Succeed); + } + cursor_a -= k; + } + values[dest] = self.buf[cursor_b].clone(); + dest -= 1; + cursor_b -= 1; + len_b -= 1; + if len_b == 1 { + break 'merging Ok(HiBreakout::CopyA); + } + k = match gallop_left(&self.buf, is_lt, &values[cursor_a], 0, len_b, len_b - 1) { + Ok(k) => k, + Err(e) => break 'merging Err(e), + }; + k = len_b - k; + b_count = k; + if k > 0 { + values[dest + 1 - k..=dest] + .clone_from_slice(&self.buf[cursor_b + 1 - k..=cursor_b]); + dest -= k; + len_b -= k; + + if len_b == 0 { + break 'merging Ok(HiBreakout::Succeed); + } + cursor_b -= k; + + if len_b == 1 { + break 'merging Ok(HiBreakout::CopyA); + } + } + values[dest] = values[cursor_a].clone(); + dest -= 1; + len_a -= 1; + + if len_a == 0 { + break 'merging Ok(HiBreakout::Succeed); + } + + cursor_a -= 1; + + if a_count < MIN_GALLOP && b_count < MIN_GALLOP { + break; + } + } + min_gallop += 1; + self.min_gallop = min_gallop; + }; + + match breakout { + Ok(HiBreakout::CopyA) => { + let src = cursor_a + 1 - len_a; + let dst = dest + 1 - len_a; + copy_within_clone(values, src, dst, len_a); + values[dst - 1] = self.buf[cursor_b].clone(); + Ok(()) + } + other => { + if len_b > 0 { + values[(dest + 1) - len_b..=dest].clone_from_slice(&self.buf[0..len_b]); + } + other.map(|_| ()) + } + } + } + + fn merge_at(&mut self, values: &mut [T], is_lt: &mut F, i: usize) -> Result<(), E> + where + F: FnMut(&T, &T) -> Result, + { + debug_assert!(self.pending.len() >= 2); + debug_assert!(i == self.pending.len() - 2 || i == self.pending.len() - 3); + + let mut start_a = self.pending[i].base; + let mut len_a = self.pending[i].len; + let start_b = self.pending[i + 1].base; + let mut len_b = self.pending[i + 1].len; + + debug_assert!(len_a > 0); + debug_assert!(len_b > 0); + debug_assert!(start_a + len_a == start_b); + + self.pending[i].len = len_a + len_b; + self.pending.remove(i + 1); + + let k = gallop_right(values, is_lt, &values[start_b], start_a, len_a, 0)?; + start_a += k; + len_a -= k; + + if len_a == 0 { + return Ok(()); + } + + len_b = gallop_left( + values, + is_lt, + &values[start_a + len_a - 1], + start_b, + len_b, + len_b - 1, + )?; + + if len_b == 0 { + return Ok(()); + } + + if len_a <= len_b { + self.merge_lo(values, is_lt, start_a, len_a, start_b, len_b)?; + } else { + self.merge_hi(values, is_lt, start_a, len_a, start_b, len_b)?; + } + Ok(()) + } + + fn found_new_run( + &mut self, + new_run_len: usize, + values: &mut [T], + is_lt: &mut F, + ) -> Result<(), E> + where + F: FnMut(&T, &T) -> Result, + { + if !self.pending.is_empty() { + let last = self.pending.len() - 1; + let s1 = self.pending[last].base; + let n1 = self.pending[last].len; + let power = powerloop(s1, n1, new_run_len, values.len()); + + while self.pending.len() > 1 && self.pending[self.pending.len() - 2].power > power { + self.merge_at(values, is_lt, self.pending.len() - 2)?; + } + + debug_assert!( + self.pending.len() < 2 || self.pending[self.pending.len() - 2].power < power + ); + let last = self.pending.len() - 1; + self.pending[last].power = power; + } + Ok(()) + } + + fn push_run(&mut self, base: usize, len: usize) { + self.pending.push(Run { + base, + len, + power: 0, + }) + } + + fn merge_force_collapse(&mut self, values: &mut [T], is_lt: &mut F) -> Result<(), E> + where + F: FnMut(&T, &T) -> Result, + { + while self.pending.len() > 1 { + let mut n = self.pending.len() - 2; + if n > 0 && self.pending[n - 1].len < self.pending[n + 1].len { + n -= 1; + } + self.merge_at(values, is_lt, n)?; + } + Ok(()) + } +} + +fn binary_insertion_sort(values: &mut [T], is_lt: &mut F, start: usize) -> Result<(), E> +where + F: FnMut(&T, &T) -> Result, +{ + for i in start..values.len() { + let mut l = 0; + let mut r = i; + + while l < r { + let m = (l + r) / 2; + if is_lt(&values[i], &values[m])? { + r = m; + } else { + l = m + 1; + } + } + values[l..=i].rotate_right(1); + } + Ok(()) +} + +fn copy_within_clone(values: &mut [T], src: usize, dest: usize, n: usize) { + if dest <= src { + for k in 0..n { + values[dest + k] = values[src + k].clone(); + } + } else { + for k in (0..n).rev() { + values[dest + k] = values[src + k].clone(); + } + } +} + +fn count_run(values: &[T], is_lt: &mut F) -> Result<(usize, bool), E> +where + F: FnMut(&T, &T) -> Result, +{ + let n = values.len(); + if n == 1 { + return Ok((1, false)); + } + let mut i = 2; + let descending = is_lt(&values[1], &values[0])?; + if descending { + while i < n && is_lt(&values[i], &values[i - 1])? { + i += 1; + } + } else { + while i < n && !is_lt(&values[i], &values[i - 1])? { + i += 1; + } + } + Ok((i, descending)) +} + +fn gallop_left( + values: &[T], + is_lt: &mut F, + key: &T, + base: usize, + len: usize, + hint: usize, +) -> Result +where + F: FnMut(&T, &T) -> Result, +{ + debug_assert!(hint < len); + let mut lastofs: isize = 0; + let mut ofs: isize = 1; + let hint_i = hint as isize; + let len_i = len as isize; + + if is_lt(&values[base + hint], key)? { + let maxofs = len_i - hint_i; + while ofs < maxofs && is_lt(&values[base + hint + ofs as usize], key)? { + lastofs = ofs; + ofs = (ofs * 2) + 1; + } + if ofs > maxofs { + ofs = maxofs; + } + lastofs += hint_i; + ofs += hint_i; + } else { + let maxofs = hint_i + 1; + while ofs < maxofs && !is_lt(&values[base + (hint_i - ofs) as usize], key)? { + lastofs = ofs; + ofs = (ofs * 2) + 1; + } + if ofs > maxofs { + ofs = maxofs; + } + (lastofs, ofs) = (hint_i - ofs, hint_i - lastofs); + } + lastofs += 1; + while lastofs < ofs { + let m = lastofs + ((ofs - lastofs) / 2); + if is_lt(&values[base + m as usize], key)? { + lastofs = m + 1; + } else { + ofs = m; + } + } + Ok(ofs as usize) +} + +fn gallop_right( + values: &[T], + is_lt: &mut F, + key: &T, + base: usize, + len: usize, + hint: usize, +) -> Result +where + F: FnMut(&T, &T) -> Result, +{ + debug_assert!(hint < len); + let mut lastofs: isize = 0; + let mut ofs: isize = 1; + let hint_i = hint as isize; + let len_i = len as isize; + + if is_lt(key, &values[base + hint])? { + let maxofs = hint_i + 1; + while ofs < maxofs && is_lt(key, &values[base + (hint_i - ofs) as usize])? { + lastofs = ofs; + ofs = (ofs * 2) + 1; + } + if ofs > maxofs { + ofs = maxofs; + } + (lastofs, ofs) = (hint_i - ofs, hint_i - lastofs); + } else { + let maxofs = len_i - hint_i; + while ofs < maxofs && !is_lt(key, &values[base + hint + ofs as usize])? { + lastofs = ofs; + ofs = (ofs * 2) + 1; + } + if ofs > maxofs { + ofs = maxofs; + } + lastofs += hint_i; + ofs += hint_i; + } + lastofs += 1; + while lastofs < ofs { + let m = lastofs + ((ofs - lastofs) / 2); + if is_lt(key, &values[base + m as usize])? { + ofs = m; + } else { + lastofs = m + 1; + } + } + Ok(ofs as usize) +} + +// TODO: consider CPython 3.12+'s incremental minrun (mr_current/mr_e/mr_mask) +// for a more precise minrun; current bit-shift version is the classic one. +fn merge_compute_minrun(mut n: usize) -> usize { + let mut r = 0; + while n >= MAX_MINRUN { + r |= n & 1; + n >>= 1; + } + n + r +} + +fn powerloop(s1: usize, n1: usize, n2: usize, n: usize) -> u32 { + let mut result: u32 = 0; + let mut a = 2 * s1 + n1; + let mut b = a + n1 + n2; + + loop { + result += 1; + if a >= n { + debug_assert!(b >= a); + a -= n; + b -= n; + } else if b >= n { + break; + } + debug_assert!(a < b && b < n); + a <<= 1; + b <<= 1; + } + result +} + +/// Stable adaptive mergesort (Tim Peters' timsort with powersort's +/// merge-ordering policy, matching CPython 3.11+). `is_lt` provides comparison. +pub(crate) fn timsort(values: &mut [T], is_lt: &mut F) -> Result<(), E> +where + T: Clone, + F: FnMut(&T, &T) -> Result, +{ + let n = values.len(); + let mut ms = MergeState { + buf: Vec::new(), + min_gallop: MIN_GALLOP, + pending: Vec::new(), + }; + + if n < 2 { + return Ok(()); + } + + if n < MAX_MINRUN { + let (l, desc) = count_run(values, is_lt)?; + if desc { + values[0..l].reverse(); + } + binary_insertion_sort(values, is_lt, l)?; + return Ok(()); + } + + let minrun = merge_compute_minrun(n); + let mut lo = 0; + + while lo < n { + let (mut l, desc) = count_run(&values[lo..n], is_lt)?; + if desc { + values[lo..lo + l].reverse(); + } + if l < minrun { + let force = minrun.min(n - lo); + binary_insertion_sort(&mut values[lo..lo + force], is_lt, l)?; + l = force; + } + ms.found_new_run(l, values, is_lt)?; + ms.push_run(lo, l); + lo += l; + } + ms.merge_force_collapse(values, is_lt)?; + debug_assert!(ms.pending.len() == 1 && ms.pending[0].len == n); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sort(mut v: Vec) -> Vec { + timsort(&mut v, &mut |a: &i32, b: &i32| Ok::(a < b)).unwrap(); + v + } + + #[test] + fn basic_examples() { + assert_eq!(sort(vec![3, 1, 2]), vec![1, 2, 3]); + assert_eq!(sort(Vec::::new()), Vec::::new()); + assert_eq!(sort(vec![1]), vec![1]); + assert_eq!(sort(vec![2, 1]), vec![1, 2]); + } + + #[test] + fn five_elements_forwards_and_backwards() { + assert_eq!(sort(vec![1, 2, 3, 4, 5]), vec![1, 2, 3, 4, 5]); + assert_eq!(sort(vec![5, 4, 3, 2, 1]), vec![1, 2, 3, 4, 5]); + } + + #[test] + fn six_elements_with_duplicates() { + assert_eq!(sort(vec![3, 1, 3, 1, 2, 2]), vec![1, 1, 2, 2, 3, 3]); + } + + #[test] + fn one_thousand_elements() { + let v: Vec = (0..1000).rev().collect(); // 999..0 + let sorted: Vec = (0..1000).collect(); + assert_eq!(sort(v), sorted); + } + + #[test] + fn pseudorandom_collection() { + let v: Vec = (0..500).map(|i| (i * 7919) % 500).collect(); + let mut expected = v.clone(); + expected.sort(); + assert_eq!(sort(v), expected); + } +} diff --git a/crates/vm/src/stdlib/_abc.rs b/crates/vm/src/stdlib/_abc.rs index 6cdef861253..3b09fefaad3 100644 --- a/crates/vm/src/stdlib/_abc.rs +++ b/crates/vm/src/stdlib/_abc.rs @@ -9,11 +9,11 @@ pub(crate) use _abc::module_def; mod _abc { use crate::{ AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyFrozenSet, PyList, PySet, PyStr, PyTupleRef, PyTypeRef, PyWeak}, + builtins::{PyFrozenSet, PyList, PySet, PyStr, PyTupleRef, PyType, PyTypeRef, PyWeak}, common::lock::PyRwLock, convert::ToPyObject, protocol::PyIterReturn, - types::Constructor, + types::{Constructor, PyTypeFlags}, }; use core::sync::atomic::{AtomicU64, Ordering}; @@ -238,6 +238,23 @@ mod _abc { // Invalidate negative cache increment_invalidation_counter(); + if let Some(cls_type) = cls.downcast_ref::() + && let Some(subclass_type) = subclass.downcast_ref::() + { + // _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) } diff --git a/crates/vm/src/stdlib/_ast.rs b/crates/vm/src/stdlib/_ast.rs index 38e0d546f44..b36277f5456 100644 --- a/crates/vm/src/stdlib/_ast.rs +++ b/crates/vm/src/stdlib/_ast.rs @@ -9,13 +9,12 @@ pub(crate) use python::_ast::module_def; mod pyast; use crate::builtins::{PyInt, PyStr}; -use crate::stdlib::_ast::module::{Mod, ModFunctionType, ModInteractive}; +use crate::stdlib::_ast::module::{Mod, ModFunctionType, ModInteractive, ModModule}; use crate::stdlib::_ast::node::BoxedSlice; use crate::{ - AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, - TryFromObject, VirtualMachine, - builtins::PyIntRef, - builtins::{PyDict, PyModule, PyType, PyUtf8StrRef}, + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, + VirtualMachine, + builtins::{PyDict, PyList, PyModule, PyTuple, PyType, PyUtf8StrRef}, class::{PyClassImpl, StaticType}, compiler::{CompileError, ParseError}, convert::ToPyObject, @@ -68,36 +67,65 @@ fn singleton_node_to_object(vm: &VirtualMachine, node_type: &'static Py) .into() } +fn is_node_instance( + vm: &VirtualMachine, + object: &PyObjectRef, + node_type: &'static Py, +) -> PyResult { + object.is_instance(node_type.as_object(), vm) +} + +fn is_ast_instance(vm: &VirtualMachine, object: &PyObjectRef) -> PyResult { + let ast_type = NodeAst::make_static_type(); + object.is_instance(ast_type.as_object(), vm) +} + fn get_node_field(vm: &VirtualMachine, obj: &PyObject, field: &'static str, typ: &str) -> PyResult { vm.get_attribute_opt(obj.to_owned(), field)? .ok_or_else(|| vm.new_type_error(format!(r#"required field "{field}" missing from {typ}"#))) } -/// Read a required scalar field, rejecting both attribute absence and `None` value -/// with CPython-compatible error messages. Pairs with `get_node_field_opt` (which -/// returns `Option::None` for the same conditions): both filter `None`, but diverge -/// on whether to raise or return `None`. -/// -/// Errors: -/// - Attribute absent: `TypeError("required field \"X\" missing from Y")` (via `get_node_field`). -/// - Attribute present but `None`: `ValueError("field 'X' is required for Y")`, -/// matching CPython's `Python/ast.c` validator output. -/// -/// Use for required scalar fields where `None` is invalid (e.g. `comprehension.target`, -/// `keyword.value`, `match_case.pattern`). Do NOT use for fields where `None` is -/// legitimate (e.g. `Constant.value` representing the `None` literal — use plain -/// `get_node_field`); or for optional fields (use `get_node_field_opt`). +/// Read a required scalar field. The generated `obj2ast_*` converters only +/// reject a missing required attribute here; if the field exists but is `None`, +/// the nested converter handles it. fn get_node_field_required( vm: &VirtualMachine, obj: &PyObject, field: &'static str, typ: &str, ) -> PyResult { - let value = get_node_field(vm, obj, field, typ)?; + get_node_field(vm, obj, field, typ) +} + +fn get_required_identifier_field( + vm: &VirtualMachine, + source_file: &SourceFile, + obj: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult { + let value = get_node_field_required(vm, obj, field, typ)?; if vm.is_none(&value) { return Err(vm.new_value_error(format!("field '{field}' is required for {typ}"))); } - Ok(value) + Node::ast_from_object(vm, source_file, value) +} + +fn get_required_node_field( + vm: &VirtualMachine, + source_file: &SourceFile, + obj: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult { + let value = get_node_field_required(vm, obj, field, typ)?; + if vm.is_none(&value) { + return Err(vm.new_value_error(format!("field '{field}' is required for {typ}"))); + } + let recursion_context = format!(" while traversing '{typ}' node"); + vm.with_recursion(&recursion_context, || { + Node::ast_from_object(vm, source_file, value) + }) } fn get_node_field_opt( @@ -110,15 +138,187 @@ fn get_node_field_opt( .filter(|obj| !vm.is_none(obj))) } +fn get_node_list_field( + vm: &VirtualMachine, + source_file: &SourceFile, + obj: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult> { + let value = get_node_list_field_object(vm, obj, field, typ)?; + let list = value.downcast_ref::().unwrap(); + convert_node_list_field(vm, source_file, list, field, typ) +} + +fn get_node_list_field_object( + vm: &VirtualMachine, + obj: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult { + let Some(value) = vm.get_attribute_opt(obj.to_owned(), field)? else { + return Ok(vm.ctx.new_list(Vec::new()).into()); + }; + value.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + r#"{typ} field "{field}" must be a list, not a {}"#, + value.class().name() + )) + })?; + Ok(value) +} + +fn convert_node_list_field( + vm: &VirtualMachine, + source_file: &SourceFile, + list: &PyList, + field: &'static str, + typ: &str, +) -> PyResult> { + let len = list.borrow_vec().len(); + let mut result = Vec::with_capacity(len); + let recursion_context = format!(" while traversing '{typ}' node"); + for i in 0..len { + let item = { + let items = list.borrow_vec(); + if items.len() != len { + return Err(vm.new_runtime_error(format!( + r#"{typ} field "{field}" changed size during iteration"# + ))); + } + items[i].clone() + }; + result.push(vm.with_recursion(&recursion_context, || { + Node::ast_from_object(vm, source_file, item) + })?); + if list.borrow_vec().len() != len { + return Err(vm.new_runtime_error(format!( + r#"{typ} field "{field}" changed size during iteration"# + ))); + } + } + Ok(result) +} + +fn get_node_boxed_slice_field( + vm: &VirtualMachine, + source_file: &SourceFile, + obj: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult> { + Ok(get_node_list_field(vm, source_file, obj, field, typ)?.into_boxed_slice()) +} + +fn runtime_expr_list_from_values( + values: Vec>, +) -> (Option>>, Vec) { + let metadata = runtime_expr_list_metadata(&values); + (metadata, lower_runtime_expr_list(values)) +} + +fn runtime_expr_boxed_slice_from_values( + values: Vec>, +) -> (Option>>, Box<[ast::Expr]>) { + let (metadata, values) = runtime_expr_list_from_values(values); + (metadata, values.into_boxed_slice()) +} + +fn runtime_expr_list_metadata(values: &[Option]) -> Option>> { + values.iter().any(Option::is_none).then(|| values.to_vec()) +} + +fn runtime_stmt_list_from_values( + values: Vec>, +) -> (Option>>, ast::Suite) { + let metadata = runtime_stmt_list_metadata(&values); + (metadata, lower_runtime_stmt_list(values)) +} + +fn runtime_stmt_list_metadata(values: &[Option]) -> Option>> { + values.iter().any(Option::is_none).then(|| values.to_vec()) +} + +fn runtime_except_handler_list_metadata( + values: &[Option], +) -> Option>> { + values.iter().any(Option::is_none).then(|| values.to_vec()) +} + +fn lower_runtime_stmt_list(values: Vec>) -> ast::Suite { + values + .into_iter() + .map(|value| value.unwrap_or_else(runtime_null_stmt_placeholder)) + .collect() +} + +fn lower_runtime_expr_list(values: Vec>) -> Vec { + values + .into_iter() + .map(|value| value.unwrap_or_else(runtime_null_expr_placeholder)) + .collect() +} + +fn runtime_null_stmt_placeholder() -> ast::Stmt { + ast::Stmt::Pass(ast::StmtPass { + range: Default::default(), + node_index: Default::default(), + }) +} + +fn runtime_null_expr_placeholder() -> ast::Expr { + ast::Expr::NoneLiteral(ast::ExprNoneLiteral { + range: Default::default(), + node_index: Default::default(), + }) +} + fn get_int_field( vm: &VirtualMachine, obj: &PyObject, field: &'static str, typ: &str, -) -> PyResult> { - get_node_field(vm, obj, field, typ)? - .downcast_exact(vm) - .map_err(|_| vm.new_type_error(format!(r#"field "{field}" must have integer type"#))) +) -> PyResult { + node_object_to_i32(vm, get_node_field(vm, obj, field, typ)?) +} + +pub(super) fn node_object_to_i32(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + if obj.is(&vm.ctx.true_value) { + return Ok(1); + } + if obj.is(&vm.ctx.false_value) { + return Ok(0); + } + let int: PyRef = match obj.clone().try_into_value(vm) { + Ok(int) => int, + Err(_) => { + return Err(vm.new_value_error(format!("invalid integer value: {}", obj.repr(vm)?))); + } + }; + i32::try_from(int.as_bigint()) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C int")) +} + +pub(super) fn node_object_to_ast_string( + vm: &VirtualMachine, + obj: PyObjectRef, +) -> PyResult { + let cls = obj.class(); + if cls.is(vm.ctx.types.str_type) || cls.is(vm.ctx.types.bytes_type) { + Ok(obj) + } else { + Err(vm.new_type_error("AST string must be of type str or bytes")) + } +} + +fn get_ast_string_field_opt( + vm: &VirtualMachine, + obj: &PyObject, + field: &'static str, +) -> PyResult> { + get_node_field_opt(vm, obj, field)? + .map(|obj| node_object_to_ast_string(vm, obj)) + .transpose() } struct PySourceRange { @@ -188,7 +388,17 @@ fn text_range_to_source_range(source_file: &SourceFile, text_range: TextRange) - let start_row = index.line_index(text_range.start()); let end_row = index.line_index(text_range.end()); let start_col = text_range.start() - index.line_start(start_row, source); - let end_col = text_range.end() - index.line_start(end_row, source); + let (end_row, end_col) = { + let end_col = text_range.end() - index.line_start(end_row, source); + if end_col == TextSize::new(0) && end_row > start_row { + let prev_line_end = text_range.end() - TextSize::new(1); + let row = index.line_index(prev_line_end); + let col = prev_line_end - index.line_start(row, source) + TextSize::new(1); + (row, col) + } else { + (end_row, end_col) + } + }; PySourceRange { start: PySourceLocation { @@ -202,140 +412,1362 @@ fn text_range_to_source_range(source_file: &SourceFile, text_range: TextRange) - } } -fn get_opt_int_field( +fn get_opt_int_field( + vm: &VirtualMachine, + obj: &PyObject, + field: &'static str, +) -> PyResult> { + match get_node_field_opt(vm, obj, field)? { + Some(val) => node_object_to_i32(vm, val).map(Some), + None => Ok(None), + } +} + +fn get_attribute_from_field( + vm: &VirtualMachine, + obj: &PyObjectRef, + field: PyObjectRef, +) -> PyResult> { + let field = field + .downcast::() + .map_err(|_| vm.new_type_error("attribute name must be string"))?; + vm.get_attribute_opt(obj.clone(), &field) +} + +#[derive(Default)] +struct AstSourceExtent { + max_line: usize, + max_col: usize, +} + +impl AstSourceExtent { + fn update_location(&mut self, vm: &VirtualMachine, obj: &PyObject) -> PyResult<()> { + if let Some(lineno) = get_opt_int_field(vm, obj, "lineno")? + && lineno > 0 + { + self.max_line = self.max_line.max(lineno as usize); + } + if let Some(end_lineno) = get_opt_int_field(vm, obj, "end_lineno")? + && end_lineno > 0 + { + self.max_line = self.max_line.max(end_lineno as usize); + } + if let Some(col_offset) = get_opt_int_field(vm, obj, "col_offset")? + && col_offset > 0 + { + self.max_col = self.max_col.max(col_offset as usize); + } + if let Some(end_col_offset) = get_opt_int_field(vm, obj, "end_col_offset")? + && end_col_offset > 0 + { + self.max_col = self.max_col.max(end_col_offset as usize); + } + Ok(()) + } +} + +fn scan_ast_source_extent( + vm: &VirtualMachine, + object: &PyObjectRef, + extent: &mut AstSourceExtent, +) -> PyResult<()> { + if is_ast_instance(vm, object)? { + extent.update_location(vm, object)?; + if let Some(fields) = object.class().get_attr(vm.ctx.intern_str("_fields")) { + let fields = fields.sequence_unchecked(); + let len = fields.length(vm)?; + for i in 0..len { + let field = fields.get_item(i as isize, vm)?; + if let Some(value) = get_attribute_from_field(vm, object, field)? { + vm.with_recursion(" while scanning AST node", || { + scan_ast_source_extent(vm, &value, extent) + })?; + } + } + } + } else if let Some(list) = object.downcast_ref::() { + let items = list.borrow_vec().to_vec(); + for item in items { + vm.with_recursion(" while scanning AST node", || { + scan_ast_source_extent(vm, &item, extent) + })?; + } + } else if let Some(tuple) = object.downcast_ref::() { + for item in tuple.as_slice() { + vm.with_recursion(" while scanning AST node", || { + scan_ast_source_extent(vm, item, extent) + })?; + } + } + Ok(()) +} + +fn copy_ast_passthrough_fields( + vm: &VirtualMachine, + source: &PyObjectRef, + target: &PyObjectRef, +) -> PyResult<()> { + if !is_ast_instance(vm, source)? + || !is_ast_instance(vm, target)? + || !source.is_instance(target.class().as_object(), vm)? + { + return Ok(()); + } + + let fields: &[&str] = + if is_node_instance(vm, target, pyast::NodeStmtFunctionDef::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtAsyncFunctionDef::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtAssign::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtFor::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtAsyncFor::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtWith::static_type())? + || is_node_instance(vm, target, pyast::NodeStmtAsyncWith::static_type())? + || is_node_instance(vm, target, pyast::NodeArg::static_type())? + { + &["type_comment"] + } else if is_node_instance(vm, target, pyast::NodeComprehension::static_type())? { + &["is_async"] + } else if is_node_instance(vm, target, pyast::NodeExprConstant::static_type())? { + &["kind"] + } else if is_node_instance(vm, target, pyast::NodeExprInterpolation::static_type())? { + &["str"] + } else { + &[] + }; + + for field in fields { + if let Some(value) = vm.get_attribute_opt(source.clone(), *field)? { + target.set_attr(*field, value, vm)?; + } + } + + let Some(source_fields) = source.class().get_attr(vm.ctx.intern_str("_fields")) else { + return Ok(()); + }; + let Some(target_fields) = target.class().get_attr(vm.ctx.intern_str("_fields")) else { + return Ok(()); + }; + let source_fields = source_fields.sequence_unchecked(); + let target_fields = target_fields.sequence_unchecked(); + let len = source_fields.length(vm)?; + if len != target_fields.length(vm)? { + return Ok(()); + } + + for i in 0..len { + let source_field = source_fields.get_item(i as isize, vm)?; + let target_field = target_fields.get_item(i as isize, vm)?; + if !vm.bool_eq(&source_field, &target_field)? { + return Ok(()); + } + let Some(source_value) = get_attribute_from_field(vm, source, source_field)? else { + continue; + }; + let Some(target_value) = get_attribute_from_field(vm, target, target_field)? else { + continue; + }; + copy_ast_passthrough_children(vm, &source_value, &target_value)?; + } + + Ok(()) +} + +fn get_ast_location_field( + vm: &VirtualMachine, + object: &PyObjectRef, + field: &'static str, +) -> PyResult> { + Ok(vm + .get_attribute_opt(object.clone(), field)? + .filter(|value| !vm.is_none(value))) +} + +fn ast_start_location_matches( + vm: &VirtualMachine, + source: &PyObjectRef, + target: &PyObjectRef, +) -> PyResult { + for field in ["lineno", "col_offset"] { + let Some(source_value) = get_ast_location_field(vm, source, field)? else { + return Ok(false); + }; + let Some(target_value) = get_ast_location_field(vm, target, field)? else { + return Ok(false); + }; + if !vm.bool_eq(&source_value, &target_value)? { + return Ok(false); + } + } + + for field in ["end_lineno", "end_col_offset"] { + let Some(source_value) = get_ast_location_field(vm, source, field)? else { + continue; + }; + let Some(target_value) = get_ast_location_field(vm, target, field)? else { + continue; + }; + if !vm.bool_eq(&source_value, &target_value)? { + return Ok(false); + } + } + + Ok(true) +} + +fn ast_passthrough_location_candidate_matches( + vm: &VirtualMachine, + source: &PyObjectRef, + target: &PyObjectRef, +) -> PyResult { + Ok(is_ast_instance(vm, source)? + && is_ast_instance(vm, target)? + && source.is_instance(target.class().as_object(), vm)? + && ast_start_location_matches(vm, source, target)?) +} + +fn copy_ast_passthrough_list_items_by_location( + vm: &VirtualMachine, + source_items: &[PyObjectRef], + target_items: &[PyObjectRef], +) -> PyResult<()> { + let mut used_source_items = vec![false; source_items.len()]; + for target_item in target_items { + for (index, source_item) in source_items.iter().enumerate() { + if used_source_items[index] { + continue; + } + if ast_passthrough_location_candidate_matches(vm, source_item, target_item)? { + used_source_items[index] = true; + copy_ast_passthrough_fields(vm, source_item, target_item)?; + break; + } + } + } + Ok(()) +} + +fn copy_ast_passthrough_children( + vm: &VirtualMachine, + source: &PyObjectRef, + target: &PyObjectRef, +) -> PyResult<()> { + if is_ast_instance(vm, source)? && is_ast_instance(vm, target)? { + return copy_ast_passthrough_fields(vm, source, target); + } + + if let (Some(source_list), Some(target_list)) = ( + source.downcast_ref::(), + target.downcast_ref::(), + ) { + let source_items = source_list.borrow_vec().to_vec(); + let target_items = target_list.borrow_vec().to_vec(); + if source_items.len() == target_items.len() { + for (source_item, target_item) in source_items.iter().zip(target_items.iter()) { + copy_ast_passthrough_children(vm, source_item, target_item)?; + } + } else { + copy_ast_passthrough_list_items_by_location(vm, &source_items, &target_items)?; + } + } else if let (Some(source_tuple), Some(target_tuple)) = ( + source.downcast_ref::(), + target.downcast_ref::(), + ) && source_tuple.as_slice().len() == target_tuple.as_slice().len() + { + for (source_item, target_item) in source_tuple + .as_slice() + .iter() + .zip(target_tuple.as_slice().iter()) + { + copy_ast_passthrough_children(vm, source_item, target_item)?; + } + } + + Ok(()) +} + +fn synthetic_source_from_ast_object(vm: &VirtualMachine, object: &PyObjectRef) -> PyResult { + let mut extent = AstSourceExtent::default(); + scan_ast_source_extent(vm, object, &mut extent)?; + if extent.max_line == 0 { + return Ok(String::new()); + } + + let line_len = extent.max_col.saturating_add(1); + let line_width = line_len + .checked_add(1) + .ok_or_else(|| vm.new_memory_error("source location is too large"))?; + let capacity = line_width + .checked_mul(extent.max_line) + .ok_or_else(|| vm.new_memory_error("source location is too large"))?; + let mut source = String::new(); + source + .try_reserve(capacity) + .map_err(|_| vm.new_memory_error("source location is too large"))?; + + for _ in 0..extent.max_line { + source.extend(core::iter::repeat_n(' ', line_len)); + source.push('\n'); + } + Ok(source) +} + +fn range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + name: &str, +) -> PyResult { + range_from_object_impl(vm, source_file, object, name, false) +} + +fn type_param_range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + range_from_object_impl(vm, source_file, object, "type_param", true) +} + +fn expr_range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + range_from_object_impl(vm, source_file, object, "expr", false) +} + +fn stmt_range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + range_from_object_impl(vm, source_file, object, "stmt", false) +} + +fn pattern_range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + range_from_object_impl(vm, source_file, object, "pattern", true) +} + +fn excepthandler_range_from_object( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + range_from_object_impl(vm, source_file, object, "excepthandler", false) +} + +fn excepthandler_range_from_object_unvalidated( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, +) -> PyResult { + let start_row = get_int_field(vm, &object, "lineno", "excepthandler")?; + let start_column = get_int_field(vm, &object, "col_offset", "excepthandler")?; + let end_row = get_opt_int_field(vm, &object, "end_lineno")?.unwrap_or(start_row); + let end_column = get_opt_int_field(vm, &object, "end_col_offset")?.unwrap_or(start_column); + + let location = PySourceRange { + start: PySourceLocation { + row: Row(if start_row > 0 { + OneIndexed::new(start_row as usize).unwrap_or(OneIndexed::MIN) + } else { + OneIndexed::MIN + }), + column: Column(TextSize::new(start_column.max(0) as u32)), + }, + end: PySourceLocation { + row: Row(if end_row > 0 { + OneIndexed::new(end_row as usize).unwrap_or(OneIndexed::MIN) + } else { + OneIndexed::MIN + }), + column: Column(TextSize::new(end_column.max(0) as u32)), + }, + }; + + Ok(source_range_to_text_range_unvalidated( + source_file, + location, + )) +} + +fn range_from_object_impl( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + name: &str, + end_required: bool, +) -> PyResult { + let start_row = get_int_field(vm, &object, "lineno", name)?; + let start_column = get_int_field(vm, &object, "col_offset", name)?; + let end_row = if end_required { + get_int_field(vm, &object, "end_lineno", name)? + } else { + get_opt_int_field(vm, &object, "end_lineno")?.unwrap_or(start_row) + }; + let end_column = if end_required { + get_int_field(vm, &object, "end_col_offset", name)? + } else { + get_opt_int_field(vm, &object, "end_col_offset")?.unwrap_or(start_column) + }; + + // lineno=0 or negative values as a special case (no location info). + // Use default values (line 1, col 0) when lineno <= 0. + let start_row_val = start_row; + let end_row_val = end_row; + let start_col_val = start_column; + let end_col_val = end_column; + + if start_row_val > end_row_val { + return Err(vm.new_value_error(format!( + "AST node line range ({start_row_val}, {end_row_val}) is not valid" + ))); + } + if (start_row_val < 0 && end_row_val != start_row_val) + || (start_col_val < 0 && end_col_val != start_col_val) + { + return Err(vm.new_value_error(format!( + "AST node column range ({start_col_val}, {end_col_val}) for line range ({start_row_val}, {end_row_val}) is not valid" + ))); + } + if start_row_val == end_row_val && start_col_val > end_col_val { + return Err(vm.new_value_error(format!( + "line {start_row_val}, column {start_col_val}-{end_col_val} is not a valid range" + ))); + } + + let location = PySourceRange { + start: PySourceLocation { + row: Row(if start_row_val > 0 { + OneIndexed::new(start_row_val as usize).unwrap_or(OneIndexed::MIN) + } else { + OneIndexed::MIN + }), + column: Column(TextSize::new(start_col_val.max(0) as u32)), + }, + end: PySourceLocation { + row: Row(if end_row_val > 0 { + OneIndexed::new(end_row_val as usize).unwrap_or(OneIndexed::MIN) + } else { + OneIndexed::MIN + }), + column: Column(TextSize::new(end_col_val.max(0) as u32)), + }, + }; + + Ok(source_range_to_text_range(source_file, location)) +} + +fn source_range_to_text_range(source_file: &SourceFile, location: PySourceRange) -> TextRange { + let index = LineIndex::from_source_text(source_file.clone().source_text()); + let source = &source_file.source_text(); + + if source.is_empty() { + return TextRange::new(TextSize::new(0), TextSize::new(0)); + } + + let start = index.offset( + location.start.to_source_location(), + source, + PositionEncoding::Utf8, + ); + let end = index.offset( + location.end.to_source_location(), + source, + PositionEncoding::Utf8, + ); + + TextRange::new(start, end) +} + +fn source_range_to_text_range_unvalidated( + source_file: &SourceFile, + location: PySourceRange, +) -> TextRange { + let index = LineIndex::from_source_text(source_file.clone().source_text()); + let source = &source_file.source_text(); + + if source.is_empty() { + return TextRange::new(TextSize::new(0), TextSize::new(0)); + } + + let start = index.offset( + location.start.to_source_location(), + source, + PositionEncoding::Utf8, + ); + let end = index.offset( + location.end.to_source_location(), + source, + PositionEncoding::Utf8, + ); + + if start <= end { + TextRange::new(start, end) + } else { + TextRange::empty(start) + } +} + +fn node_add_location( + dict: &Py, + range: TextRange, + vm: &VirtualMachine, + source_file: &SourceFile, +) { + let range = text_range_to_source_range(source_file, range); + dict.set_item("lineno", vm.ctx.new_int(range.start.row.get()).into(), vm) + .unwrap(); + dict.set_item( + "col_offset", + vm.ctx.new_int(range.start.column.get()).into(), + vm, + ) + .unwrap(); + dict.set_item("end_lineno", vm.ctx.new_int(range.end.row.get()).into(), vm) + .unwrap(); + dict.set_item( + "end_col_offset", + vm.ctx.new_int(range.end.column.get()).into(), + vm, + ) + .unwrap(); +} + +/// Return the expected Python AST root type class for a compile() mode string. +/// +/// builtin compile() accepts func_type only with PyCF_ONLY_AST. +/// Source-string func_type parsing is handled separately, but Python AST +/// FunctionType still uses the mode check before obj-to-AST conversion. +pub(crate) fn mode_type_and_name(mode: &str) -> Option<(PyRef, &'static str)> { + match mode { + "exec" => Some((pyast::NodeModModule::make_static_type(), "Module")), + "eval" => Some((pyast::NodeModExpression::make_static_type(), "Expression")), + "single" => Some((pyast::NodeModInteractive::make_static_type(), "Interactive")), + "func_type" => Some(( + pyast::NodeModFunctionType::make_static_type(), + "FunctionType", + )), + _ => None, + } +} + +struct TypeCommentLine<'a> { + text: &'a str, + comment_start: Option, +} + +struct TypeCommentSource<'a> { + lines: Vec>, +} + +impl<'a> TypeCommentSource<'a> { + fn new(source: &'a str, tokens: &ast::token::Tokens) -> Self { + let mut comment_offsets = Vec::new(); + for token in tokens { + if matches!(token.kind(), ast::token::TokenKind::Comment) { + comment_offsets.push(token.start().to_usize()); + } + } + + let mut comment_offsets = comment_offsets.into_iter().peekable(); + let mut line_start = 0usize; + let mut lines = Vec::new(); + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + let comment_start = comment_offsets.next_if(|offset| *offset < line_end); + lines.push(TypeCommentLine { + text: line, + comment_start: comment_start.map(|offset| offset - line_start), + }); + line_start = line_end; + } + + Self { lines } + } +} + +fn type_comment_position(line: &TypeCommentLine<'_>) -> Option { + let comment = line.comment_start?; + line.text[comment + 1..] + .trim_start() + .starts_with("type:") + .then_some(comment) +} + +fn type_comment_text<'a>(line: &'a TypeCommentLine<'a>) -> Option<&'a str> { + let comment = line.comment_start?; + let text = line.text.trim_end_matches(['\n', '\r']); + let mut rest = text[comment + 1..].trim_start_matches([' ', '\t']); + rest = rest.strip_prefix("type:")?; + Some(rest.trim_start_matches([' ', '\t'])) +} + +fn type_ignore_tag(comment: &str) -> Option<&str> { + let rest = comment.strip_prefix("ignore")?; + if let Some(next) = rest.as_bytes().first() + && (next.is_ascii_alphanumeric() || !next.is_ascii()) + { + return None; + } + Some(rest) +} + +fn regular_type_comment_text<'a>(line: &'a TypeCommentLine<'a>) -> Option<&'a str> { + let comment = type_comment_text(line)?; + type_ignore_tag(comment).is_none().then_some(comment) +} + +fn type_comment_parse_error( + source_file: &SourceFile, + message: &str, + start: usize, + end: usize, +) -> CompileError { + let range = TextRange::new(TextSize::new(start as u32), TextSize::new(end as u32)); + let source_range = text_range_to_source_range(source_file, range); + ParseError { + error: parser::ParseErrorType::OtherError(message.to_owned()), + raw_location: range, + location: source_range.start.to_source_location(), + end_location: source_range.end.to_source_location(), + source_path: "".to_string(), + is_unclosed_bracket: false, + } + .into() +} + +#[cfg(feature = "codegen")] +fn future_feature_compile_error( + source_file: &SourceFile, + error: codegen::preprocess::FutureFeatureError, +) -> CompileError { + let location = source_file + .to_source_code() + .source_location(error.range.start(), PositionEncoding::Utf8); + let error = match error.kind { + codegen::preprocess::FutureFeatureErrorKind::InvalidFeature(feature) => { + codegen::error::CodegenErrorType::InvalidFutureFeature(feature) + } + codegen::preprocess::FutureFeatureErrorKind::InvalidBraces => { + codegen::error::CodegenErrorType::InvalidFutureBraces + } + }; + codegen::error::CodegenError { + location: Some(location), + error, + source_path: source_file.name().to_owned(), + } + .into() +} + +fn trimmed_line_end(line: &str) -> usize { + line.trim_end_matches(['\n', '\r']).len() +} + +fn line_end_error( + source_file: &SourceFile, + message: &str, + line_start: usize, + line: &str, +) -> CompileError { + let start = line_start + trimmed_line_end(line); + type_comment_parse_error(source_file, message, start, start + 1) +} + +fn point_error_end(source: &str, start: usize) -> usize { + match source.as_bytes().get(start) { + None => start, + Some(_) => start + 1, + } +} + +fn line_after_colon_error( + source_file: &SourceFile, + message: &str, + line_start: usize, + line: &str, +) -> Option { + let code = &line[..trimmed_line_end(line)]; + let colon = code.rfind(':')?; + (!code[colon + 1..].trim().is_empty()) + .then(|| line_end_error(source_file, message, line_start, line)) +} + +fn find_line_containing_offset(source: &str, offset: usize) -> Option<(usize, &str)> { + let mut line_start = 0usize; + for line in source.split_inclusive('\n') { + let line_end = line_start + line.len(); + if offset < line_end { + return Some((line_start, line)); + } + line_start = line_end; + } + (offset == source.len()).then_some((line_start, "")) +} + +fn find_next_nonempty_line_end(source: &str, offset: usize) -> Option { + let (mut line_start, line) = find_line_containing_offset(source, offset)?; + line_start += line.len(); + for line in source[line_start..].split_inclusive('\n') { + if !line.trim().is_empty() { + return Some(line_start + trimmed_line_end(line)); + } + line_start += line.len(); + } + None +} + +fn find_numeric_literal_containing_underscore(code: &str) -> Option<(usize, usize)> { + let bytes = code.as_bytes(); + for idx in 1..bytes.len().saturating_sub(1) { + if bytes[idx] == b'_' && bytes[idx - 1].is_ascii_digit() && bytes[idx + 1].is_ascii_digit() + { + let mut start = idx - 1; + while start > 0 + && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') + { + start -= 1; + } + let mut end = idx + 2; + while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') { + end += 1; + } + return Some((start, end)); + } + } + None +} + +fn bracket_delta(code: &str) -> i32 { + code.chars().fold(0, |depth, ch| match ch { + '(' | '[' | '{' => depth + 1, + ')' | ']' | '}' => depth - 1, + _ => depth, + }) +} + +fn def_header_complete(code: &str, depth: i32) -> bool { + depth <= 0 && code.trim_end().ends_with(':') +} + +fn is_assignment_stmt_line(code: &str) -> bool { + let bytes = code.as_bytes(); + for (idx, byte) in bytes.iter().enumerate() { + if *byte != b'=' { + continue; + } + let prev = idx.checked_sub(1).and_then(|idx| bytes.get(idx)).copied(); + let next = bytes.get(idx + 1).copied(); + if matches!( + prev, + Some( + b'=' | b'!' + | b'<' + | b'>' + | b':' + | b'+' + | b'-' + | b'*' + | b'/' + | b'%' + | b'&' + | b'|' + | b'^' + ) + ) || matches!(next, Some(b'=')) + { + continue; + } + return true; + } + false +} + +fn line_allows_stmt_type_comment(code: &str) -> bool { + let stripped = code.trim_start(); + (stripped.starts_with("for ") || stripped.starts_with("async for ")) && stripped.ends_with(':') + || (stripped.starts_with("with ") || stripped.starts_with("async with ")) + && stripped.ends_with(':') + || is_assignment_stmt_line(code) +} + +fn invalid_type_comment_syntax_error( + source_file: &SourceFile, + type_comment_source: &TypeCommentSource<'_>, +) -> Option { + let mut line_start = 0usize; + let mut in_def_header = false; + let mut def_depth = 0i32; + let mut pending_func_type_comment = false; + let mut previous_def_had_type_comment = false; + for line in &type_comment_source.lines { + let line_end = line_start + line.text.len(); + let stripped = line.text.trim_start(); + let code_end = type_comment_position(line).unwrap_or(line.text.len()); + let code = line.text[..code_end].trim(); + let has_regular_type_comment = regular_type_comment_text(line).is_some(); + + if let Some(comment) = type_comment_position(line) { + if code == "*" || code == "*," || code.ends_with("*,") { + return Some(type_comment_parse_error( + source_file, + "bare * has associated type comment", + line_start + comment, + line_start + line.text.len(), + )); + } + if previous_def_had_type_comment && code.is_empty() { + return Some(type_comment_parse_error( + source_file, + "Cannot have two type comments on def", + line_start + comment, + line_start + line.text.len(), + )); + } + let allowed = !has_regular_type_comment + || in_def_header + || line_allows_stmt_type_comment(code) + || stripped.starts_with("def ") + || stripped.starts_with("async def ") + || (pending_func_type_comment && code.is_empty()); + if !allowed { + return Some(type_comment_parse_error( + source_file, + "invalid syntax", + line_start + comment, + line_start + line.text.len(), + )); + } + } + + let starts_def = stripped.starts_with("def ") || stripped.starts_with("async def "); + if starts_def && !in_def_header { + def_depth = bracket_delta(code); + let complete = def_header_complete(code, def_depth); + in_def_header = !complete; + previous_def_had_type_comment = complete && has_regular_type_comment; + pending_func_type_comment = complete && !has_regular_type_comment; + } else if in_def_header { + def_depth += bracket_delta(code); + let complete = def_header_complete(code, def_depth); + if complete { + in_def_header = false; + previous_def_had_type_comment = has_regular_type_comment; + pending_func_type_comment = !has_regular_type_comment; + } + } else if (pending_func_type_comment && code.is_empty() && has_regular_type_comment) + || (!stripped.trim().is_empty() && !starts_def && !code.is_empty()) + { + pending_func_type_comment = false; + previous_def_had_type_comment = false; + } + + line_start = line_end; + } + None +} + +fn feature_version_syntax_error( + source: &str, + source_file: &SourceFile, + target_version: ast::PythonVersion, +) -> Option { + let mut line_start = 0usize; + let mut async_def_error = None; + let mut pending_async_def = false; + let mut pending_block_error = None; + for line in source.split_inclusive('\n') { + let code_end = line.find('#').unwrap_or(line.len()); + let code = &line[..code_end]; + let stripped = code.trim_start(); + if pending_async_def && !stripped.trim().is_empty() { + if async_def_error.is_none() { + async_def_error = Some(line_end_error( + source_file, + "Async functions are only supported in Python 3.5 and greater", + line_start, + line, + )); + } + pending_async_def = false; + } + if let Some(message) = pending_block_error.take() { + if !stripped.trim().is_empty() { + return Some(line_end_error(source_file, message, line_start, line)); + } + pending_block_error = Some(message); + } + + if target_version.minor < 5 { + if stripped.starts_with("async def ") && async_def_error.is_none() { + let message = "Async functions are only supported in Python 3.5 and greater"; + if let Some(error) = line_after_colon_error(source_file, message, line_start, line) + { + async_def_error = Some(error); + } else { + pending_async_def = true; + } + } + if stripped.starts_with("async for ") { + let message = "Async for loops are only supported in Python 3.5 and greater"; + if let Some(error) = line_after_colon_error(source_file, message, line_start, line) + { + return Some(error); + } + pending_block_error = Some(message); + } + if stripped.starts_with("async with ") { + let message = "Async with statements are only supported in Python 3.5 and greater"; + if let Some(error) = line_after_colon_error(source_file, message, line_start, line) + { + return Some(error); + } + pending_block_error = Some(message); + } + if stripped.starts_with("await ") { + return Some(line_end_error( + source_file, + "Await expressions are only supported in Python 3.5 and greater", + line_start, + line, + )); + } + if let Some(pos) = code.find('@') + && !stripped.starts_with('@') + { + let is_augassign = code.as_bytes().get(pos + 1) == Some(&b'='); + let (start, end) = if is_augassign { + (line_start + pos, line_start + pos + 2) + } else { + let start = line_start + trimmed_line_end(line); + (start, start + 1) + }; + return Some(type_comment_parse_error( + source_file, + "The '@' operator is only supported in Python 3.5 and greater", + start, + end, + )); + } + } + + if target_version.minor < 6 { + if !stripped.starts_with("async for ") && code.contains(" async for ") { + let start = line_start + trimmed_line_end(line).saturating_sub(1); + return Some(type_comment_parse_error( + source_file, + "Async comprehensions are only supported in Python 3.6 and greater", + start, + point_error_end(source_file.source_text(), start), + )); + } + if let Some((start, end)) = find_numeric_literal_containing_underscore(code) { + return Some(type_comment_parse_error( + source_file, + "Underscores in numeric literals are only supported in Python 3.6 and greater", + line_start + start, + line_start + end, + )); + } + } + + line_start += line.len(); + } + async_def_error +} + +fn ann_assign_feature_error(stmts: &[ast::Stmt], source_file: &SourceFile) -> Option { + for stmt in stmts { + match stmt { + ast::Stmt::AnnAssign(ann) => { + let start = ann.range().end().to_usize(); + return Some(type_comment_parse_error( + source_file, + "Variable annotation syntax is only supported in Python 3.6 and greater", + start, + point_error_end(source_file.source_text(), start), + )); + } + ast::Stmt::FunctionDef(def) => { + if let Some(error) = ann_assign_feature_error(&def.body, source_file) { + return Some(error); + } + } + ast::Stmt::ClassDef(class_def) => { + if let Some(error) = ann_assign_feature_error(&class_def.body, source_file) { + return Some(error); + } + } + ast::Stmt::For(for_stmt) => { + if let Some(error) = ann_assign_feature_error(&for_stmt.body, source_file) + .or_else(|| ann_assign_feature_error(&for_stmt.orelse, source_file)) + { + return Some(error); + } + } + ast::Stmt::While(while_stmt) => { + if let Some(error) = ann_assign_feature_error(&while_stmt.body, source_file) + .or_else(|| ann_assign_feature_error(&while_stmt.orelse, source_file)) + { + return Some(error); + } + } + ast::Stmt::If(if_stmt) => { + if let Some(error) = ann_assign_feature_error(&if_stmt.body, source_file) { + return Some(error); + } + for clause in &if_stmt.elif_else_clauses { + if let Some(error) = ann_assign_feature_error(&clause.body, source_file) { + return Some(error); + } + } + } + ast::Stmt::With(with_stmt) => { + if let Some(error) = ann_assign_feature_error(&with_stmt.body, source_file) { + return Some(error); + } + } + ast::Stmt::Match(match_stmt) => { + for case in &match_stmt.cases { + if let Some(error) = ann_assign_feature_error(&case.body, source_file) { + return Some(error); + } + } + } + ast::Stmt::Try(try_stmt) => { + if let Some(error) = ann_assign_feature_error(&try_stmt.body, source_file) + .or_else(|| ann_assign_feature_error(&try_stmt.orelse, source_file)) + .or_else(|| ann_assign_feature_error(&try_stmt.finalbody, source_file)) + { + return Some(error); + } + for handler in &try_stmt.handlers { + let ast::ExceptHandler::ExceptHandler(handler) = handler; + if let Some(error) = ann_assign_feature_error(&handler.body, source_file) { + return Some(error); + } + } + } + _ => {} + } + } + None +} + +fn feature_version_ast_syntax_error( + top: &ast::Mod, + source_file: &SourceFile, + target_version: ast::PythonVersion, +) -> Option { + if target_version.minor >= 6 { + return None; + } + match top { + ast::Mod::Module(module) => ann_assign_feature_error(&module.body, source_file), + ast::Mod::Expression(_) => None, + } +} + +fn cpython_unsupported_syntax_message( + error: &parser::UnsupportedSyntaxError, +) -> Option<&'static str> { + match error.kind { + parser::UnsupportedSyntaxErrorKind::Match => { + Some("Pattern matching is only supported in Python 3.10 and greater") + } + parser::UnsupportedSyntaxErrorKind::Walrus => { + Some("Assignment expressions are only supported in Python 3.8 and greater") + } + parser::UnsupportedSyntaxErrorKind::ExceptStar => { + Some("Exception groups are only supported in Python 3.11 and greater") + } + parser::UnsupportedSyntaxErrorKind::PositionalOnlyParameter => { + Some("Positional-only parameters are only supported in Python 3.8 and greater") + } + parser::UnsupportedSyntaxErrorKind::TypeParameterList => { + Some("Type parameter lists are only supported in Python 3.12 and greater") + } + parser::UnsupportedSyntaxErrorKind::TypeAliasStatement => { + Some("Type statement is only supported in Python 3.12 and greater") + } + parser::UnsupportedSyntaxErrorKind::TypeParamDefault => { + Some("Type parameter defaults are only supported in Python 3.13 and greater") + } + parser::UnsupportedSyntaxErrorKind::TemplateStrings => { + Some("t-strings are only supported in Python 3.14 and greater") + } + parser::UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => Some( + "except expressions without parentheses are only supported in Python 3.14 and greater", + ), + _ => None, + } +} + +fn cpython_unsupported_syntax_error( + error: &parser::UnsupportedSyntaxError, + source: &str, + source_file: &SourceFile, +) -> Option { + let message = cpython_unsupported_syntax_message(error)?; + let start = match error.kind { + parser::UnsupportedSyntaxErrorKind::Match + | parser::UnsupportedSyntaxErrorKind::ExceptStar + | parser::UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { + find_next_nonempty_line_end(source, error.range.start().to_usize()) + .unwrap_or_else(|| error.range.end().to_usize()) + } + parser::UnsupportedSyntaxErrorKind::Walrus + | parser::UnsupportedSyntaxErrorKind::PositionalOnlyParameter + | parser::UnsupportedSyntaxErrorKind::TypeParamDefault => error.range.end().to_usize(), + parser::UnsupportedSyntaxErrorKind::TypeAliasStatement => { + let (line_start, line) = + find_line_containing_offset(source, error.range.start().to_usize())?; + line_start + trimmed_line_end(line) + } + parser::UnsupportedSyntaxErrorKind::TypeParameterList => { + let (line_start, line) = + find_line_containing_offset(source, error.range.start().to_usize())?; + let code = &line[..trimmed_line_end(line)]; + line_start + + code + .as_bytes() + .iter() + .rposition(|byte| *byte == b']') + .unwrap_or_else(|| error.range.end().to_usize() - line_start) + } + parser::UnsupportedSyntaxErrorKind::TemplateStrings => { + let (line_start, line) = + find_line_containing_offset(source, error.range.start().to_usize())?; + line_start + trimmed_line_end(line).saturating_sub(1) + } + _ => error.range.start().to_usize(), + }; + Some(type_comment_parse_error( + source_file, + message, + start, + point_error_end(source, start), + )) +} + +fn should_report_unsupported_syntax_error(error: &parser::UnsupportedSyntaxError) -> bool { + cpython_unsupported_syntax_message(error).is_some() + || matches!( + error.kind, + parser::UnsupportedSyntaxErrorKind::LazyImportStatement + | parser::UnsupportedSyntaxErrorKind::ParenthesizedKeywordArgumentName + ) +} + +fn node_list_field( vm: &VirtualMachine, - obj: &PyObject, + object: &PyObjectRef, field: &'static str, -) -> PyResult>> { - match get_node_field_opt(vm, obj, field)? { - Some(val) => val - .downcast_exact(vm) - .map(Some) - .map_err(|_| vm.new_type_error(format!(r#"field "{field}" must have integer type"#))), - None => Ok(None), - } +) -> Vec { + vm.get_attribute_opt(object.clone(), field) + .ok() + .flatten() + .and_then(|value| { + value + .downcast_ref::() + .map(|list| list.borrow_vec().to_vec()) + }) + .unwrap_or_default() } -fn range_from_object( +fn node_optional_field( vm: &VirtualMachine, - source_file: &SourceFile, - object: PyObjectRef, - name: &str, -) -> PyResult { - let start_row = get_int_field(vm, &object, "lineno", name)?; - let start_column = get_int_field(vm, &object, "col_offset", name)?; - // end_lineno and end_col_offset are optional, default to start values - let end_row = - get_opt_int_field(vm, &object, "end_lineno")?.unwrap_or_else(|| start_row.clone()); - let end_column = - get_opt_int_field(vm, &object, "end_col_offset")?.unwrap_or_else(|| start_column.clone()); + object: &PyObjectRef, + field: &'static str, +) -> Option { + vm.get_attribute_opt(object.clone(), field) + .ok() + .flatten() + .filter(|value| !vm.is_none(value)) +} - // lineno=0 or negative values as a special case (no location info). - // Use default values (line 1, col 0) when lineno <= 0. - let start_row_val: i32 = start_row.try_to_primitive(vm)?; - let end_row_val: i32 = end_row.try_to_primitive(vm)?; - let start_col_val: i32 = start_column.try_to_primitive(vm)?; - let end_col_val: i32 = end_column.try_to_primitive(vm)?; +fn node_lineno(vm: &VirtualMachine, object: &PyObjectRef) -> Option { + node_optional_field(vm, object, "lineno")? + .try_into_value(vm) + .ok() +} - if start_row_val > end_row_val { - return Err(vm.new_value_error(format!( - "AST node line range ({start_row_val}, {end_row_val}) is not valid" - ))); - } - if (start_row_val < 0 && end_row_val != start_row_val) - || (start_col_val < 0 && end_col_val != start_col_val) - { - return Err(vm.new_value_error(format!( - "AST node column range ({start_col_val}, {end_col_val}) for line range ({start_row_val}, {end_row_val}) is not valid" - ))); - } - if start_row_val == end_row_val && start_col_val > end_col_val { - return Err(vm.new_value_error(format!( - "line {start_row_val}, column {start_col_val}-{end_col_val} is not a valid range" - ))); - } +fn source_line<'a>( + lines: &'a TypeCommentSource<'a>, + lineno: usize, +) -> Option<&'a TypeCommentLine<'a>> { + lineno.checked_sub(1).and_then(|idx| lines.lines.get(idx)) +} - let location = PySourceRange { - start: PySourceLocation { - row: Row(if start_row_val > 0 { - OneIndexed::new(start_row_val as usize).unwrap_or(OneIndexed::MIN) - } else { - OneIndexed::MIN - }), - column: Column(TextSize::new(start_col_val.max(0) as u32)), - }, - end: PySourceLocation { - row: Row(if end_row_val > 0 { - OneIndexed::new(end_row_val as usize).unwrap_or(OneIndexed::MIN) - } else { - OneIndexed::MIN - }), - column: Column(TextSize::new(end_col_val.max(0) as u32)), - }, - }; +fn set_type_comment(vm: &VirtualMachine, object: &PyObjectRef, comment: Option<&str>) { + let value = comment.map_or_else(|| vm.ctx.none(), |comment| vm.ctx.new_str(comment).into()); + object + .as_object() + .dict() + .unwrap() + .set_item("type_comment", value, vm) + .unwrap(); +} - Ok(source_range_to_text_range(source_file, location)) +fn same_line_type_comment<'a>( + vm: &VirtualMachine, + lines: &'a TypeCommentSource<'a>, + object: &PyObjectRef, +) -> Option<&'a str> { + let lineno = node_lineno(vm, object)?; + regular_type_comment_text(source_line(lines, lineno)?) } -fn source_range_to_text_range(source_file: &SourceFile, location: PySourceRange) -> TextRange { - let index = LineIndex::from_source_text(source_file.clone().source_text()); - let source = &source_file.source_text(); +fn function_type_comment<'a>( + vm: &VirtualMachine, + lines: &'a TypeCommentSource<'a>, + object: &PyObjectRef, +) -> Option<&'a str> { + let lineno = node_lineno(vm, object)?; + if let Some(comment) = regular_type_comment_text(source_line(lines, lineno)?) { + return Some(comment); + } - if source.is_empty() { - return TextRange::new(TextSize::new(0), TextSize::new(0)); + let next_line = source_line(lines, lineno + 1)?; + let comment_pos = type_comment_position(next_line)?; + next_line.text[..comment_pos] + .trim() + .is_empty() + .then(|| regular_type_comment_text(next_line)) + .flatten() +} + +fn apply_type_comments_to_arguments( + vm: &VirtualMachine, + lines: &TypeCommentSource<'_>, + arguments: &PyObjectRef, +) { + for field in ["posonlyargs", "args", "kwonlyargs"] { + for arg in node_list_field(vm, arguments, field) { + set_type_comment(vm, &arg, same_line_type_comment(vm, lines, &arg)); + } + } + for field in ["vararg", "kwarg"] { + if let Some(arg) = node_optional_field(vm, arguments, field) { + set_type_comment(vm, &arg, same_line_type_comment(vm, lines, &arg)); + } } +} - let start = index.offset( - location.start.to_source_location(), - source, - PositionEncoding::Utf8, - ); - let end = index.offset( - location.end.to_source_location(), - source, - PositionEncoding::Utf8, - ); +fn apply_type_comments_to_node( + vm: &VirtualMachine, + lines: &TypeCommentSource<'_>, + object: &PyObjectRef, +) { + let cls = object.class(); + if cls.is(pyast::NodeStmtFunctionDef::static_type()) + || cls.is(pyast::NodeStmtAsyncFunctionDef::static_type()) + { + set_type_comment(vm, object, function_type_comment(vm, lines, object)); + if let Some(arguments) = node_optional_field(vm, object, "args") { + apply_type_comments_to_arguments(vm, lines, &arguments); + } + } else if cls.is(pyast::NodeStmtAssign::static_type()) + || cls.is(pyast::NodeStmtFor::static_type()) + || cls.is(pyast::NodeStmtAsyncFor::static_type()) + || cls.is(pyast::NodeStmtWith::static_type()) + || cls.is(pyast::NodeStmtAsyncWith::static_type()) + { + set_type_comment(vm, object, same_line_type_comment(vm, lines, object)); + } - TextRange::new(start, end) + for field in ["body", "orelse", "finalbody"] { + for child in node_list_field(vm, object, field) { + apply_type_comments_to_node(vm, lines, &child); + } + } + for field in ["handlers", "cases"] { + for child in node_list_field(vm, object, field) { + apply_type_comments_to_node(vm, lines, &child); + } + } } -fn node_add_location( - dict: &Py, - range: TextRange, +fn apply_type_comments_to_module( vm: &VirtualMachine, - source_file: &SourceFile, + lines: &TypeCommentSource<'_>, + module: &PyObjectRef, ) { - let range = text_range_to_source_range(source_file, range); - dict.set_item("lineno", vm.ctx.new_int(range.start.row.get()).into(), vm) - .unwrap(); - dict.set_item( - "col_offset", - vm.ctx.new_int(range.start.column.get()).into(), - vm, - ) - .unwrap(); - dict.set_item("end_lineno", vm.ctx.new_int(range.end.row.get()).into(), vm) - .unwrap(); - dict.set_item( - "end_col_offset", - vm.ctx.new_int(range.end.column.get()).into(), - vm, - ) - .unwrap(); + for statement in node_list_field(vm, module, "body") { + apply_type_comments_to_node(vm, lines, &statement); + } } -/// Return the expected AST mod type class for a compile() mode string. -pub(crate) fn mode_type_and_name(mode: &str) -> Option<(PyRef, &'static str)> { - match mode { - "exec" => Some((pyast::NodeModModule::make_static_type(), "Module")), - "eval" => Some((pyast::NodeModExpression::make_static_type(), "Expression")), - "single" => Some((pyast::NodeModInteractive::make_static_type(), "Interactive")), - "func_type" => Some(( - pyast::NodeModFunctionType::make_static_type(), - "FunctionType", - )), - _ => None, +#[cfg(feature = "parser")] +fn ipython_escape_command_syntax_error( + top: &ast::Mod, + source_file: &SourceFile, +) -> Option { + use ast::visitor::{Visitor, walk_expr, walk_stmt}; + + #[derive(Default)] + struct IpyEscapeCommandVisitor { + range: Option, + } + + impl Visitor<'_> for IpyEscapeCommandVisitor { + fn visit_stmt(&mut self, stmt: &ast::Stmt) { + if self.range.is_some() { + return; + } + match stmt { + ast::Stmt::IpyEscapeCommand(stmt) => { + self.range = Some(stmt.range); + } + _ => walk_stmt(self, stmt), + } + } + + fn visit_expr(&mut self, expr: &ast::Expr) { + if self.range.is_some() { + return; + } + match expr { + ast::Expr::IpyEscapeCommand(expr) => { + self.range = Some(expr.range); + } + _ => walk_expr(self, expr), + } + } + } + + let mut visitor = IpyEscapeCommandVisitor::default(); + match top { + ast::Mod::Module(module) => { + for statement in &module.body { + visitor.visit_stmt(statement); + if visitor.range.is_some() { + break; + } + } + } + ast::Mod::Expression(expression) => { + visitor.visit_expr(&expression.body); + } } + let range = visitor.range?; + let source_range = text_range_to_source_range(source_file, range); + Some( + ParseError { + error: parser::ParseErrorType::OtherError("invalid syntax".to_owned()), + raw_location: range, + location: source_range.start.to_source_location(), + end_location: source_range.end.to_source_location(), + source_path: "".to_owned(), + is_unclosed_bracket: false, + } + .into(), + ) } /// Create an empty `arguments` AST node (no parameters). @@ -361,6 +1793,7 @@ fn empty_arguments_object(vm: &VirtualMachine) -> PyObjectRef { } #[cfg(feature = "parser")] +#[allow(clippy::too_many_arguments)] pub(crate) fn parse( vm: &VirtualMachine, source: &str, @@ -368,14 +1801,30 @@ pub(crate) fn parse( optimize: u8, target_version: Option, type_comments: bool, + optimized_ast: bool, + interactive: bool, + explicit_future_annotations: bool, + dont_imply_dedent: bool, ) -> Result { let source_file = SourceFileBuilder::new("".to_owned(), source.to_owned()).finish(); let mut options = parser::ParseOptions::from(mode); let target_version = target_version.unwrap_or(ast::PythonVersion::PY314); + if let Some(error) = feature_version_syntax_error(source, &source_file, target_version) { + return Err(error); + } options = options.with_target_version(target_version); - let parsed = parser::parse(source, options).map_err(|parse_error| { + let parsed = parser::parse_unchecked(source, options); + let type_comment_source = + type_comments.then(|| TypeCommentSource::new(source, parsed.tokens())); + if let Some(lines) = &type_comment_source + && let Some(error) = invalid_type_comment_syntax_error(&source_file, lines) + { + return Err(error); + } + if let Err(errors) = parsed.as_result() { + let parse_error = errors[0].clone(); let range = text_range_to_source_range(&source_file, parse_error.location); - ParseError { + return Err(ParseError { error: parse_error.error, raw_location: parse_error.location, location: range.start.to_source_location(), @@ -383,9 +1832,23 @@ pub(crate) fn parse( source_path: "".to_string(), is_unclosed_bracket: false, } - })?; + .into()); + } + if dont_imply_dedent + && interactive + && let Some(error) = rustpython_compiler::dont_imply_dedent_source_error(&source_file) + { + return Err(error); + } - if let Some(error) = parsed.unsupported_syntax_errors().first() { + if let Some(error) = parsed + .unsupported_syntax_errors() + .iter() + .find(|error| should_report_unsupported_syntax_error(error)) + { + if let Some(error) = cpython_unsupported_syntax_error(error, source, &source_file) { + return Err(error); + } let range = text_range_to_source_range(&source_file, error.range()); return Err(ParseError { error: parser::ParseErrorType::OtherError(error.to_string()), @@ -398,20 +1861,65 @@ pub(crate) fn parse( .into()); } + if let Some(error) = rustpython_compiler::long_decimal_integer_literal_error( + &source_file, + parsed.tokens(), + vm.state.int_max_str_digits.load(), + ) { + return Err(error); + } + let mut top = parsed.into_syntax(); - if optimize > 0 { - fold_match_value_constants(&mut top); + if let Some(error) = ipython_escape_command_syntax_error(&top, &source_file) { + return Err(error); + } + if let Some(error) = feature_version_ast_syntax_error(&top, &source_file, target_version) { + return Err(error); + } + #[cfg(feature = "codegen")] + { + let future_features = codegen::preprocess::checked_future_features(&top) + .map_err(|err| future_feature_compile_error(&source_file, err))?; + let future_annotations = explicit_future_annotations + || future_features.contains(crate::bytecode::CodeFlags::FUTURE_ANNOTATIONS); + if interactive && let ast::Mod::Module(module) = &mut top { + codegen::preprocess::preprocess_statements( + &mut module.body, + optimize, + future_annotations, + !optimized_ast, + ); + } else { + codegen::preprocess::preprocess_mod( + &mut top, + optimize, + future_annotations, + !optimized_ast, + ); + } } - if optimize >= 2 { - strip_docstrings(&mut top); + #[cfg(not(feature = "codegen"))] + { + if optimized_ast && optimize > 0 { + fold_match_value_constants(&mut top); + } + if optimize >= 2 { + strip_docstrings(&mut top); + } } let top = match top { - ast::Mod::Module(m) => Mod::Module(m), + ast::Mod::Module(m) => Mod::Module(ModModule { + module: m, + type_ignores: Vec::new(), + }), ast::Mod::Expression(e) => Mod::Expression(e), }; let obj = top.ast_to_object(vm, &source_file); - if type_comments && obj.class().is(pyast::NodeModModule::static_type()) { - let type_ignores = type_ignores_from_source(vm, source); + if let Some(lines) = &type_comment_source + && obj.class().is(pyast::NodeModModule::static_type()) + { + apply_type_comments_to_module(vm, lines, &obj); + let type_ignores = type_ignores_from_source(vm, lines); let dict = obj.as_object().dict().unwrap(); dict.set_item("type_ignores", vm.ctx.new_list(type_ignores).into(), vm) .unwrap(); @@ -441,8 +1949,18 @@ pub(crate) fn parse_func_type( target_version: Option, ) -> Result { let _ = optimize; - let _ = target_version; let source = source.trim(); + let invalid_func_type = || -> CompileError { + ParseError { + error: parser::ParseErrorType::OtherError("invalid syntax".to_owned()), + raw_location: TextRange::default(), + location: SourceLocation::default(), + end_location: SourceLocation::default(), + source_path: "".to_owned(), + is_unclosed_bracket: false, + } + .into() + }; let mut depth = 0i32; let mut split_at = None; let mut chars = source.chars().peekable(); @@ -477,7 +1995,9 @@ pub(crate) fn parse_func_type( let parse_expr = |expr_src: &str| -> Result { let source_file = SourceFileBuilder::new("".to_owned(), expr_src.to_owned()).finish(); - let parsed = parser::parse_expression(expr_src).map_err(|parse_error| { + let options = parser::ParseOptions::from(parser::Mode::Expression) + .with_target_version(target_version.unwrap_or(ast::PythonVersion::PY314)); + let parsed = parser::parse(expr_src, options).map_err(|parse_error| { let range = text_range_to_source_range(&source_file, parse_error.location); ParseError { error: parse_error.error, @@ -488,43 +2008,92 @@ pub(crate) fn parse_func_type( is_unclosed_bracket: false, } })?; - Ok(*parsed.into_syntax().body) + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + unreachable!(); + }; + Ok(*expression.body) }; - let arg_expr = parse_expr(left)?; - let returns = parse_expr(right)?; - - let argtypes: Vec = match arg_expr { - ast::Expr::Tuple(tup) => tup.elts, - ast::Expr::Name(_) | ast::Expr::Subscript(_) | ast::Expr::Attribute(_) => vec![arg_expr], - other => vec![other], + if !left.starts_with('(') || !left.ends_with(')') { + return Err(invalid_func_type()); + } + let inner = left[1..left.len() - 1].trim(); + let argtypes = if inner.is_empty() { + Vec::new() + } else { + if inner.ends_with(',') { + return Err(invalid_func_type()); + } + let call_source = format!("__rustpython_func_type__({inner})"); + let source_file = SourceFileBuilder::new("".to_owned(), call_source.clone()).finish(); + let options = parser::ParseOptions::from(parser::Mode::Expression) + .with_target_version(target_version.unwrap_or(ast::PythonVersion::PY314)); + let parsed = parser::parse(&call_source, options).map_err(|parse_error| { + let range = text_range_to_source_range(&source_file, parse_error.location); + ParseError { + error: parse_error.error, + raw_location: parse_error.location, + location: range.start.to_source_location(), + end_location: range.end.to_source_location(), + source_path: "".to_string(), + is_unclosed_bracket: false, + } + })?; + let ast::Mod::Expression(expression) = parsed.into_syntax() else { + unreachable!(); + }; + let ast::Expr::Call(call) = *expression.body else { + return Err(invalid_func_type()); + }; + let mut args = Vec::new(); + let positional_len = call.arguments.args.len(); + let mut seen_star = false; + for (index, arg) in call.arguments.args.into_iter().enumerate() { + match arg { + ast::Expr::Starred(starred) => { + if seen_star || index + 1 != positional_len { + return Err(invalid_func_type()); + } + seen_star = true; + args.push(*starred.value); + } + expr => args.push(expr), + } + } + let mut seen_kw_star = false; + for keyword in call.arguments.keywords { + if keyword.arg.is_some() || seen_kw_star { + return Err(invalid_func_type()); + } + seen_kw_star = true; + args.push(keyword.value); + } + args }; + let returns = parse_expr(right)?; + let func_type = ModFunctionType { argtypes: argtypes.into_boxed_slice(), returns, - range: TextRange::default(), + runtime_argtypes: None, }; let source_file = SourceFileBuilder::new("".to_owned(), source.to_owned()).finish(); Ok(func_type.ast_to_object(vm, &source_file)) } -fn type_ignores_from_source(vm: &VirtualMachine, source: &str) -> Vec { +fn type_ignores_from_source( + vm: &VirtualMachine, + lines: &TypeCommentSource<'_>, +) -> Vec { let mut ignores = Vec::new(); - for (idx, line) in source.lines().enumerate() { - let Some(pos) = line.find('#') else { + for (idx, line) in lines.lines.iter().enumerate() { + let Some(comment) = type_comment_text(line) else { continue; }; - - let comment = &line[pos + 1..]; - let comment = comment.trim_start(); - - let Some(rest) = comment.strip_prefix("type: ignore") else { + let Some(tag) = type_ignore_tag(comment) else { continue; }; - - let tag = rest.trim_start(); - let tag = if tag.is_empty() { "" } else { tag }; let node = NodeAst .into_ref_with_type( vm, @@ -542,7 +2111,7 @@ fn type_ignores_from_source(vm: &VirtualMachine, source: &str) -> Vec fold_stmts(&mut module.body), @@ -550,7 +2119,7 @@ fn fold_match_value_constants(top: &mut ast::Mod) { } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn strip_docstrings(top: &mut ast::Mod) { match top { ast::Mod::Module(module) => strip_docstring_in_body(&mut module.body), @@ -558,8 +2127,8 @@ fn strip_docstrings(top: &mut ast::Mod) { } } -#[cfg(feature = "parser")] -fn strip_docstring_in_body(body: &mut Vec) { +#[cfg(all(feature = "parser", not(feature = "codegen")))] +fn strip_docstring_in_body(body: &mut ast::Suite) { if let Some(range) = take_docstring(body) && body.is_empty() { @@ -580,12 +2149,19 @@ fn strip_docstring_in_body(body: &mut Vec) { } } -#[cfg(feature = "parser")] -fn take_docstring(body: &mut Vec) -> Option { +#[cfg(all(feature = "parser", not(feature = "codegen")))] +fn take_docstring(body: &mut ast::Suite) -> Option { let ast::Stmt::Expr(expr_stmt) = body.first()? else { return None; }; - if matches!(expr_stmt.value.as_ref(), ast::Expr::StringLiteral(_)) { + if matches!( + expr_stmt.value.as_ref(), + ast::Expr::StringLiteral(_) + | ast::Expr::Constant(ast::ExprConstant { + value: ast::ConstantValue::Str(_), + .. + }) + ) { let range = expr_stmt.range; body.remove(0); return Some(range); @@ -593,14 +2169,14 @@ fn take_docstring(body: &mut Vec) -> Option { None } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn fold_stmts(stmts: &mut [ast::Stmt]) { for stmt in stmts { fold_stmt(stmt); } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn fold_stmt(stmt: &mut ast::Stmt) { use ast::Stmt; match stmt { @@ -641,7 +2217,7 @@ fn fold_stmt(stmt: &mut ast::Stmt) { } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn fold_pattern(pattern: &mut ast::Pattern) { use ast::Pattern; match pattern { @@ -681,7 +2257,7 @@ fn fold_pattern(pattern: &mut ast::Pattern) { } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn fold_expr(expr: &mut ast::Expr) { use ast::Expr; if let Expr::UnaryOp(unary) = expr { @@ -720,11 +2296,12 @@ fn fold_expr(expr: &mut ast::Expr) { let Expr::NumberLiteral(left) = binop.left.as_ref() else { return; }; + let Expr::NumberLiteral(right) = binop.right.as_ref() else { return; }; - if let Some(number) = fold_number_binop(&left.value, &binop.op, &right.value) { + if let Some(number) = fold_number_binop(&left.value, binop.op, &right.value) { *expr = Expr::NumberLiteral(ast::ExprNumberLiteral { node_index: binop.node_index.clone(), range: binop.range, @@ -734,10 +2311,10 @@ fn fold_expr(expr: &mut ast::Expr) { } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn fold_number_binop( left: &ast::Number, - op: &ast::Operator, + op: ast::Operator, right: &ast::Number, ) -> Option { let (left_real, left_imag, left_is_complex) = number_to_complex(left)?; @@ -760,7 +2337,7 @@ fn fold_number_binop( } } -#[cfg(feature = "parser")] +#[cfg(all(feature = "parser", not(feature = "codegen")))] fn number_to_complex(number: &ast::Number) -> Option<(f64, f64, bool)> { match number { ast::Number::Complex { real, imag } => Some((*real, *imag, true)), @@ -769,94 +2346,175 @@ fn number_to_complex(number: &ast::Number) -> Option<(f64, f64, bool)> { } } +#[cfg(feature = "codegen")] +pub(crate) fn preprocess_ast_object( + vm: &VirtualMachine, + object: PyObjectRef, + filename: &str, + optimize: u8, + optimized_ast: bool, + explicit_future_annotations: bool, +) -> PyResult { + let original_object = object.clone(); + let text = synthetic_source_from_ast_object(vm, &object)?; + let source_file = SourceFileBuilder::new(filename.to_owned(), text).finish(); + let ast = Node::ast_from_object(vm, &source_file, object)?; + validate::validate_mod(vm, &ast)?; + let syntax_check_only = !optimized_ast; + + let ast = match ast { + Mod::Module(mut module) => { + let mut ast = ast::Mod::Module(module.module); + let future_features = + codegen::preprocess::checked_future_features(&ast).map_err(|err| { + vm.new_syntax_error(&future_feature_compile_error(&source_file, err), None) + })?; + let future_annotations = explicit_future_annotations + || future_features.contains(crate::bytecode::CodeFlags::FUTURE_ANNOTATIONS); + codegen::preprocess::preprocess_mod( + &mut ast, + optimize, + future_annotations, + syntax_check_only, + ); + let ast::Mod::Module(processed_module) = ast else { + unreachable!(); + }; + module.module = processed_module; + Mod::Module(module) + } + Mod::Interactive(mut interactive) => { + let future_features = codegen::preprocess::checked_future_features_in_body( + &interactive.body, + ) + .map_err(|err| { + vm.new_syntax_error(&future_feature_compile_error(&source_file, err), None) + })?; + let future_annotations = explicit_future_annotations + || future_features.contains(crate::bytecode::CodeFlags::FUTURE_ANNOTATIONS); + codegen::preprocess::preprocess_statements( + &mut interactive.body, + optimize, + future_annotations, + syntax_check_only, + ); + Mod::Interactive(interactive) + } + Mod::Expression(expression) => { + let mut ast = ast::Mod::Expression(expression); + codegen::preprocess::preprocess_mod( + &mut ast, + optimize, + explicit_future_annotations, + syntax_check_only, + ); + let ast::Mod::Expression(expression) = ast else { + unreachable!(); + }; + Mod::Expression(expression) + } + Mod::FunctionType(function_type) => Mod::FunctionType(function_type), + }; + let result = ast.ast_to_object(vm, &source_file); + copy_ast_passthrough_fields(vm, &original_object, &result)?; + Ok(result) +} + #[cfg(feature = "codegen")] pub(crate) fn compile( vm: &VirtualMachine, object: PyObjectRef, filename: &str, mode: crate::compiler::Mode, - optimize: Option, + mut opts: codegen::CompileOpts, ) -> PyResult { - let mut opts = vm.compile_opts(); - if let Some(optimize) = optimize { - opts.optimize = optimize; - } - - let source_file = SourceFileBuilder::new(filename.to_owned(), "".to_owned()).finish(); - let ast: Mod = Node::ast_from_object(vm, &source_file, object)?; + let text = synthetic_source_from_ast_object(vm, &object)?; + let source_file = SourceFileBuilder::new(filename.to_owned(), text.clone()).finish(); + let ast = Node::ast_from_object(vm, &source_file, object)?; validate::validate_mod(vm, &ast)?; let ast = match ast { - Mod::Module(m) => ast::Mod::Module(m), - Mod::Interactive(ModInteractive { range, body }) => ast::Mod::Module(ast::ModModule { + Mod::Module(m) => ast::Mod::Module(m.module), + Mod::Interactive(ModInteractive { range, body, .. }) => ast::Mod::Module(ast::ModModule { node_index: Default::default(), range, body, + runtime_body: None, }), Mod::Expression(e) => ast::Mod::Expression(e), - Mod::FunctionType(_) => todo!(), + Mod::FunctionType(_) => { + return Err(vm.new_runtime_error("this compiler does not handle FunctionTypes")); + } }; - // TODO: create a textual representation of the ast - let text = ""; + opts.future_features |= codegen::preprocess::future_features(&ast); + let source = text.clone(); let source_file = SourceFileBuilder::new(filename, text).finish(); - let code = codegen::compile::compile_top(ast, source_file, mode, opts) - .map_err(|err| vm.new_syntax_error(&err.into(), None))?; // FIXME source + #[cfg(feature = "parser")] + let code = { + let source_path = filename.to_owned(); + // A warning the filter escalates to an exception is stashed here so a + // non-SyntaxWarning category propagates unchanged, matching + // PyErr_ExceptionMatches(SyntaxWarning) in compiler_warn. + let escalated: core::cell::Cell> = + core::cell::Cell::new(None); + let mut syntax_warning_handler = |location: SourceLocation, message: String| { + let fname = vm.ctx.new_str(source_path.as_str()); + let message = vm.ctx.new_str(message); + crate::warn::warn_explicit( + Some(vm.ctx.exceptions.syntax_warning.to_owned()), + message.into(), + fname, + location.line.get(), + None, + vm.ctx.none(), + None, + None, + vm, + ) + .map_err(|exception| { + let message = exception.as_object().str(vm).map_or_else( + |_| "compiler warning raised as an exception".to_owned(), + |message| message.as_wtf8().to_string(), + ); + let marker = codegen::error::CodegenError { + location: Some(location), + error: codegen::error::CodegenErrorType::SyntaxError(message), + source_path: source_path.clone(), + }; + escalated.set(Some(exception)); + marker + }) + }; + let result = codegen::compile::compile_top_with_syntax_warning_handler( + ast, + source_file, + mode, + opts, + Some(&mut syntax_warning_handler), + ); + match escalated.take() { + Some(exception) if !exception.fast_isinstance(vm.ctx.exceptions.syntax_warning) => { + return Err(exception); + } + _ => result, + } + }; + #[cfg(not(feature = "parser"))] + let code = codegen::compile::compile_top(ast, source_file, mode, opts); + let code = code.map_err(|err| vm.new_syntax_error(&err.into(), Some(source.as_str())))?; Ok(crate::builtins::PyCode::new_ref_from_bytecode(vm, code).into()) } -#[cfg(feature = "codegen")] +#[cfg(not(feature = "rustpython-codegen"))] pub(crate) fn validate_ast_object(vm: &VirtualMachine, object: PyObjectRef) -> PyResult<()> { let source_file = SourceFileBuilder::new("".to_owned(), "".to_owned()).finish(); - let ast: Mod = Node::ast_from_object(vm, &source_file, object)?; + let ast = Node::ast_from_object(vm, &source_file, object)?; validate::validate_mod(vm, &ast)?; Ok(()) } -// Used by builtins::compile() -pub(crate) const PY_CF_ONLY_AST: i32 = 0x0400; - // The following flags match the values from Include/cpython/compile.h -// Caveat emptor: These flags are undocumented on purpose and depending -// on their effect outside the standard library is **unsupported**. -pub(crate) const PY_CF_SOURCE_IS_UTF8: i32 = 0x0100; -pub(crate) const PY_CF_DONT_IMPLY_DEDENT: i32 = 0x200; -pub(crate) const PY_CF_IGNORE_COOKIE: i32 = 0x0800; -pub(crate) const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = 0x4000; -pub(crate) const PY_CF_OPTIMIZED_AST: i32 = 0x8000 | PY_CF_ONLY_AST; -pub(crate) const PY_CF_TYPE_COMMENTS: i32 = 0x1000; -pub(crate) const PY_CF_ALLOW_TOP_LEVEL_AWAIT: i32 = 0x2000; - -// __future__ flags - sync with Lib/__future__.py -// TODO: These flags aren't being used in rust code -// CO_FUTURE_ANNOTATIONS does make a difference in the codegen, -// so it should be used in compile(). -// see compiler/codegen/src/compile.rs -const CO_NESTED: i32 = 0x0010; -const CO_GENERATOR_ALLOWED: i32 = 0; -const CO_FUTURE_DIVISION: i32 = 0x20000; -const CO_FUTURE_ABSOLUTE_IMPORT: i32 = 0x40000; -const CO_FUTURE_WITH_STATEMENT: i32 = 0x80000; -const CO_FUTURE_PRINT_FUNCTION: i32 = 0x100000; -const CO_FUTURE_UNICODE_LITERALS: i32 = 0x200000; -const CO_FUTURE_BARRY_AS_BDFL: i32 = 0x400000; -const CO_FUTURE_GENERATOR_STOP: i32 = 0x800000; -const CO_FUTURE_ANNOTATIONS: i32 = 0x1000000; - -// Used by builtins::compile() - the summary of all flags -pub(crate) const PY_COMPILE_FLAGS_MASK: i32 = PY_CF_ONLY_AST - | PY_CF_SOURCE_IS_UTF8 - | PY_CF_DONT_IMPLY_DEDENT - | PY_CF_IGNORE_COOKIE - | PY_CF_ALLOW_TOP_LEVEL_AWAIT - | PY_CF_ALLOW_INCOMPLETE_INPUT - | PY_CF_OPTIMIZED_AST - | PY_CF_TYPE_COMMENTS - | CO_NESTED - | CO_GENERATOR_ALLOWED - | CO_FUTURE_DIVISION - | CO_FUTURE_ABSOLUTE_IMPORT - | CO_FUTURE_WITH_STATEMENT - | CO_FUTURE_PRINT_FUNCTION - | CO_FUTURE_UNICODE_LITERALS - | CO_FUTURE_BARRY_AS_BDFL - | CO_FUTURE_GENERATOR_STOP - | CO_FUTURE_ANNOTATIONS; +pub(crate) use crate::vm::compile_mode::{ + PY_CF_ALLOW_INCOMPLETE_INPUT, PY_CF_ALLOW_TOP_LEVEL_AWAIT, PY_CF_DONT_IMPLY_DEDENT, + PY_CF_IGNORE_COOKIE, PY_CF_ONLY_AST, PY_CF_OPTIMIZED_AST, PY_CF_SOURCE_IS_UTF8, + PY_CF_TYPE_COMMENTS, +}; diff --git a/crates/vm/src/stdlib/_ast/argument.rs b/crates/vm/src/stdlib/_ast/argument.rs index 626024f5bd6..5019c436624 100644 --- a/crates/vm/src/stdlib/_ast/argument.rs +++ b/crates/vm/src/stdlib/_ast/argument.rs @@ -2,14 +2,78 @@ use super::*; use rustpython_compiler_core::SourceFile; pub(super) struct PositionalArguments { - pub range: TextRange, - pub args: Box<[ast::Expr]>, + range: TextRange, + kind: PositionalArgumentsKind, +} + +enum PositionalArgumentsKind { + Args(Box<[ast::Expr]>), + RuntimeValues(Vec>), +} + +impl PositionalArguments { + pub(super) fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + let values: Vec> = + 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>) -> Self { + Self { + range, + kind: PositionalArgumentsKind::RuntimeValues(values), + } + } + + fn from_values(range: TextRange, values: Vec>) -> Self { + if values.iter().any(Option::is_none) { + Self::from_runtime_values(range, values) + } else { + Self::from_args( + range, + values + .into_iter() + .flatten() + .collect::>() + .into_boxed_slice(), + ) + } + } + + fn range(&self) -> TextRange { + self.range + } + + fn into_args_and_runtime_values(self) -> (Box<[ast::Expr]>, Option>>) { + match self.kind { + PositionalArgumentsKind::Args(args) => (args, None), + PositionalArgumentsKind::RuntimeValues(values) => ( + lower_runtime_expr_list(values.clone()).into_boxed_slice(), + Some(values), + ), + } + } } impl Node for PositionalArguments { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { args, range: _ } = self; - BoxedSlice(args).ast_to_object(vm, source_file) + match self.kind { + PositionalArgumentsKind::Args(args) => BoxedSlice(args).ast_to_object(vm, source_file), + PositionalArgumentsKind::RuntimeValues(values) => values.ast_to_object(vm, source_file), + } } fn ast_from_object( @@ -18,10 +82,7 @@ impl Node for PositionalArguments { object: PyObjectRef, ) -> PyResult { let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; - Ok(Self { - args: args.0, - range: TextRange::default(), // TODO - }) + Ok(Self::from_args(TextRange::default(), args.0)) } } @@ -30,6 +91,21 @@ pub(super) struct KeywordArguments { pub keywords: Box<[ast::Keyword]>, } +impl KeywordArguments { + pub(super) fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + Ok(Self { + keywords: get_node_boxed_slice_field(vm, source_file, object, field, typ)?, + range: TextRange::default(), + }) + } +} + impl Node for KeywordArguments { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { keywords, range: _ } = self; @@ -45,7 +121,7 @@ impl Node for KeywordArguments { let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; Ok(Self { keywords: keywords.0, - range: TextRange::default(), // TODO + range: TextRange::default(), }) } } @@ -54,13 +130,16 @@ pub(super) fn merge_function_call_arguments( pos_args: PositionalArguments, key_args: KeywordArguments, ) -> ast::Arguments { - let range = pos_args.range.cover(key_args.range); + let range = pos_args.range().cover(key_args.range); + let (args, runtime_args) = pos_args.into_args_and_runtime_values(); ast::Arguments { node_index: Default::default(), range, - args: pos_args.args, + args, keywords: key_args.keywords, + runtime_args, + runtime_bases: None, } } @@ -68,10 +147,11 @@ pub(super) fn split_function_call_arguments( args: ast::Arguments, ) -> (PositionalArguments, KeywordArguments) { let ast::Arguments { - node_index: _, range: _, args, keywords, + runtime_args, + .. } = args; let positional_arguments_range = args @@ -80,10 +160,10 @@ pub(super) fn split_function_call_arguments( .reduce(|acc, next| acc.cover(next)) .unwrap_or_default(); // debug_assert!(range.contains_range(positional_arguments_range)); - let positional_arguments = PositionalArguments { - range: positional_arguments_range, - args, - }; + let positional_arguments = runtime_args.map_or_else( + || PositionalArguments::from_args(positional_arguments_range, args), + |values| PositionalArguments::from_runtime_values(positional_arguments_range, values), + ); let keyword_arguments_range = keywords .iter() @@ -107,10 +187,12 @@ pub(super) fn split_class_def_args( Some(args) => *args, }; let ast::Arguments { - node_index: _, range: _, args, keywords, + runtime_args: _, + runtime_bases, + .. } = args; let positional_arguments_range = args @@ -119,10 +201,10 @@ pub(super) fn split_class_def_args( .reduce(|acc, next| acc.cover(next)) .unwrap_or_default(); // debug_assert!(range.contains_range(positional_arguments_range)); - let positional_arguments = PositionalArguments { - range: positional_arguments_range, - args, - }; + let positional_arguments = runtime_bases.map_or_else( + || PositionalArguments::from_args(positional_arguments_range, args), + |values| PositionalArguments::from_runtime_values(positional_arguments_range, values), + ); let keyword_arguments_range = keywords .iter() @@ -146,10 +228,10 @@ pub(super) fn merge_class_def_args( return None; } - let args = if let Some(positional_arguments) = positional_arguments { - positional_arguments.args + let (args, runtime_bases) = if let Some(positional_arguments) = positional_arguments { + positional_arguments.into_args_and_runtime_values() } else { - vec![].into_boxed_slice() + (vec![].into_boxed_slice(), None) }; let keywords = if let Some(keyword_arguments) = keyword_arguments { keyword_arguments.keywords @@ -162,5 +244,7 @@ pub(super) fn merge_class_def_args( range: Default::default(), // TODO args, keywords, + runtime_args: None, + runtime_bases, })) } diff --git a/crates/vm/src/stdlib/_ast/basic.rs b/crates/vm/src/stdlib/_ast/basic.rs index 28e4a6803ee..c25dd3c55f6 100644 --- a/crates/vm/src/stdlib/_ast/basic.rs +++ b/crates/vm/src/stdlib/_ast/basic.rs @@ -1,4 +1,5 @@ use super::*; +use crate::builtins::PyIntRef; use rustpython_codegen::compile::ruff_int_to_bigint; use rustpython_compiler_core::SourceFile; @@ -13,7 +14,11 @@ impl Node for ast::Identifier { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let py_str = PyUtf8StrRef::try_from_object(vm, object)?; + if !object.class().is(vm.ctx.types.str_type) { + return Err(vm.new_type_error("AST identifier must be of type str")); + } + let py_str = PyUtf8StrRef::try_from_object(vm, object) + .map_err(|_| vm.new_type_error("AST identifier must be of type str"))?; Ok(Self::new(py_str.as_str(), TextRange::default())) } } @@ -28,7 +33,6 @@ impl Node for ast::Int { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - // FIXME: performance let value: PyIntRef = object.try_into_value(vm)?; let value = value.as_bigint().to_string(); Ok(value.parse().unwrap()) @@ -45,6 +49,6 @@ impl Node for bool { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - i32::try_from_object(vm, object).map(|i| i != 0) + node_object_to_i32(vm, object).map(|i| i != 0) } } diff --git a/crates/vm/src/stdlib/_ast/constant.rs b/crates/vm/src/stdlib/_ast/constant.rs index b1a8a015689..6debbf5c0d1 100644 --- a/crates/vm/src/stdlib/_ast/constant.rs +++ b/crates/vm/src/stdlib/_ast/constant.rs @@ -1,12 +1,15 @@ use super::*; use crate::builtins::{PyComplex, PyFrozenSet, PyTuple}; use ast::str_prefix::StringLiteralPrefix; -use rustpython_compiler_core::SourceFile; +use rustpython_codegen::compile::ruff_int_to_bigint; +use rustpython_compiler_core::{SourceFile, bytecode::ConstantData}; #[derive(Debug)] pub(super) struct Constant { pub(super) range: TextRange, pub(super) value: ConstantLiteral, + kind: Option>, + invalid_type: Option, } impl Constant { @@ -19,6 +22,8 @@ impl Constant { Self { range, value: ConstantLiteral::Str { value, prefix }, + kind: None, + invalid_type: None, } } @@ -26,6 +31,8 @@ impl Constant { Self { range, value: ConstantLiteral::Int(value), + kind: None, + invalid_type: None, } } @@ -33,6 +40,8 @@ impl Constant { Self { range, value: ConstantLiteral::Float(value), + kind: None, + invalid_type: None, } } @@ -40,6 +49,8 @@ impl Constant { Self { range, value: ConstantLiteral::Complex { real, imag }, + kind: None, + invalid_type: None, } } @@ -47,6 +58,8 @@ impl Constant { Self { range, value: ConstantLiteral::Bytes(value), + kind: None, + invalid_type: None, } } @@ -54,6 +67,8 @@ impl Constant { Self { range, value: ConstantLiteral::Bool(value), + kind: None, + invalid_type: None, } } @@ -61,6 +76,8 @@ impl Constant { Self { range, value: ConstantLiteral::None, + kind: None, + invalid_type: None, } } @@ -68,15 +85,29 @@ impl Constant { Self { range, value: ConstantLiteral::Ellipsis, + kind: None, + invalid_type: None, } } pub(crate) fn into_expr(self) -> ast::Expr { - constant_to_ruff_expr(self) + let Self { + range, + value, + kind, + invalid_type, + } = self; + ast::Expr::Constant(ast::ExprConstant { + node_index: Default::default(), + range, + value: constant_data_to_ast_constant_value(constant_literal_to_constant_data(&value)), + kind: kind.or_else(|| constant_literal_kind(&value)), + invalid_type: invalid_type.map(String::into_boxed_str), + }) } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) enum ConstantLiteral { None, Bool(bool), @@ -96,20 +127,361 @@ pub(crate) enum ConstantLiteral { Ellipsis, } +pub(super) fn invalid_constant_type(expr: &ast::Expr) -> Option> { + match expr { + ast::Expr::Constant(expr) => expr.invalid_type.clone(), + _ => None, + } +} + +pub(super) fn runtime_string_from_pyobject( + vm: &VirtualMachine, + object: PyObjectRef, +) -> (Option>, Option>) { + runtime_string_from_object(vm, object) +} + +pub(super) fn runtime_string_object( + vm: &VirtualMachine, + value: Option>, + bytes: Option>, +) -> Option { + 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, + format_spec: Option>, +) -> Option<(PyObjectRef, Option>)> { + 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>, + bytes: Option>, +) -> Option { + 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> { + 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 { + 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>, Option>) { + if object.class().is(vm.ctx.types.str_type) { + ( + Some( + object + .try_to_value::(vm) + .expect("AST string field was validated as str") + .into_boxed_str(), + ), + None, + ) + } else { + ( + None, + Some( + object + .try_to_value::>(vm) + .expect("AST string field was validated as bytes"), + ), + ) + } +} + +fn runtime_string_to_object( + vm: &VirtualMachine, + value: Option>, + bytes: Option>, +) -> Option { + 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 { + 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::().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::().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> { + let cls = value_object.class(); + if cls.is(vm.ctx.types.none_type) + || cls.is(vm.ctx.types.bool_type) + || cls.is(vm.ctx.types.str_type) + || cls.is(vm.ctx.types.bytes_type) + || cls.is(vm.ctx.types.int_type) + || cls.is(vm.ctx.types.float_type) + || cls.is(vm.ctx.types.complex_type) + || cls.is(vm.ctx.types.ellipsis_type) + { + return Ok(None); + } + if cls.is(vm.ctx.types.tuple_type) || cls.is(vm.ctx.types.frozenset_type) { + return first_invalid_constant_type(vm, value_object).map(Some); + } + Ok(Some(cls.name().to_owned())) +} + +fn constant_data_to_object(vm: &VirtualMachine, constant: ConstantData) -> PyObjectRef { + match constant { + ConstantData::None => vm.ctx.none(), + ConstantData::Boolean { value } => vm.ctx.new_bool(value).to_pyobject(vm), + ConstantData::Str { value } => vm.ctx.new_str(value.to_string()).to_pyobject(vm), + ConstantData::Bytes { value } => vm.ctx.new_bytes(value).to_pyobject(vm), + ConstantData::Integer { value } => vm.ctx.new_int(value).into(), + ConstantData::Tuple { elements } => { + let value = elements + .into_iter() + .map(|c| constant_data_to_object(vm, c)) + .collect(); + vm.ctx.new_tuple(value).to_pyobject(vm) + } + ConstantData::Frozenset { elements } => PyFrozenSet::from_iter( + vm, + elements.into_iter().map(|c| constant_data_to_object(vm, c)), + ) + .unwrap() + .into_pyobject(vm), + ConstantData::Float { value } => vm.ctx.new_float(value).into_pyobject(vm), + ConstantData::Complex { value } => vm.ctx.new_complex(value).into_pyobject(vm), + ConstantData::Ellipsis => vm.ctx.ellipsis.clone().into(), + ConstantData::Code { .. } | ConstantData::Slice { .. } => { + unreachable!("ast.Constant values cannot contain code objects or slices") + } + } +} + // constructor +pub(super) fn constant_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + 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::(vm)?.into_boxed_str()) + }) + .transpose()?; + + Ok(Constant { + range, + value, + kind, + invalid_type, + }) +} + impl Node for Constant { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { range, value } = self; + let Self { + range, + value, + kind, + invalid_type: _, + } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprConstant::static_type().to_owned()) .unwrap(); - let kind = match &value { - ConstantLiteral::Str { - prefix: StringLiteralPrefix::Unicode, - .. - } => vm.ctx.new_str("u").into(), - _ => vm.ctx.none(), - }; + let kind = kind + .or_else(|| constant_literal_kind(&value)) + .map_or_else(|| vm.ctx.none(), |kind| vm.ctx.new_str(kind).into()); let value = value.ast_to_object(vm, source_file); let dict = node.as_object().dict().unwrap(); dict.set_item("value", value, vm).unwrap(); @@ -123,16 +495,8 @@ impl Node for Constant { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let value_object = get_node_field(vm, &object, "value", "Constant")?; - let value = Node::ast_from_object(vm, source_file, value_object)?; - - Ok(Self { - value, - // kind: get_node_field_opt(_vm, &_object, "kind")? - // .map(|obj| Node::ast_from_object(_vm, obj)) - // .transpose()?, - range: range_from_object(vm, source_file, object, "Constant")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Constant")?; + constant_from_object_with_range(vm, source_file, object, range) } } @@ -201,8 +565,12 @@ impl Node for ConstantLiteral { })?; let tuple = tuple .into_iter() - .cloned() - .map(|object| Node::ast_from_object(vm, source_file, object)) + .map(|object| { + let object = object.clone(); + vm.with_recursion(" during compilation", || { + Node::ast_from_object(vm, source_file, object) + }) + }) .collect::>()?; Self::Tuple(tuple) } else if cls.is(vm.ctx.types.frozenset_type) { @@ -210,7 +578,11 @@ impl Node for ConstantLiteral { let elements = set .elements() .into_iter() - .map(|object| Node::ast_from_object(vm, source_file, object)) + .map(|object| { + vm.with_recursion(" during compilation", || { + Node::ast_from_object(vm, source_file, object) + }) + }) .collect::>()?; Self::FrozenSet(elements) } else if cls.is(vm.ctx.types.float_type) { @@ -244,117 +616,6 @@ impl Node for ConstantLiteral { } } -fn constant_to_ruff_expr(value: Constant) -> ast::Expr { - let Constant { value, range } = value; - match value { - ConstantLiteral::None => ast::Expr::NoneLiteral(ast::ExprNoneLiteral { - node_index: Default::default(), - range, - }), - ConstantLiteral::Bool(value) => ast::Expr::BooleanLiteral(ast::ExprBooleanLiteral { - node_index: Default::default(), - range, - value, - }), - ConstantLiteral::Str { value, prefix } => { - ast::Expr::StringLiteral(ast::ExprStringLiteral { - node_index: Default::default(), - range, - value: ast::StringLiteralValue::single(ast::StringLiteral { - node_index: Default::default(), - range, - value, - flags: ast::StringLiteralFlags::empty().with_prefix(prefix), - }), - }) - } - ConstantLiteral::Bytes(value) => { - ast::Expr::BytesLiteral(ast::ExprBytesLiteral { - node_index: Default::default(), - range, - value: ast::BytesLiteralValue::single(ast::BytesLiteral { - node_index: Default::default(), - range, - value, - flags: ast::BytesLiteralFlags::empty(), // TODO - }), - }) - } - ConstantLiteral::Int(value) => ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - node_index: Default::default(), - range, - value: ast::Number::Int(value), - }), - ConstantLiteral::Tuple(value) => ast::Expr::Tuple(ast::ExprTuple { - node_index: Default::default(), - range, - elts: value - .into_iter() - .map(|value| { - constant_to_ruff_expr(Constant { - range: TextRange::default(), - value, - }) - }) - .collect(), - ctx: ast::ExprContext::Load, - // TODO: Does this matter? - parenthesized: true, - }), - ConstantLiteral::FrozenSet(value) => { - let args = if value.is_empty() { - Vec::new() - } else { - vec![ast::Expr::Set(ast::ExprSet { - node_index: Default::default(), - range: TextRange::default(), - elts: value - .into_iter() - .map(|value| { - constant_to_ruff_expr(Constant { - range: TextRange::default(), - value, - }) - }) - .collect(), - })] - }; - ast::Expr::Call(ast::ExprCall { - node_index: Default::default(), - range, - func: Box::new(ast::Expr::Name(ast::ExprName { - node_index: Default::default(), - range: TextRange::default(), - id: ast::name::Name::new_static("frozenset"), - ctx: ast::ExprContext::Load, - })), - arguments: ast::Arguments { - node_index: Default::default(), - range, - args: args.into(), - keywords: Box::default(), - }, - }) - } - ConstantLiteral::Float(value) => ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - node_index: Default::default(), - range, - value: ast::Number::Float(value), - }), - ConstantLiteral::Complex { real, imag } => { - ast::Expr::NumberLiteral(ast::ExprNumberLiteral { - node_index: Default::default(), - range, - value: ast::Number::Complex { real, imag }, - }) - } - ConstantLiteral::Ellipsis => ast::Expr::EllipsisLiteral(ast::ExprEllipsisLiteral { - node_index: Default::default(), - range, - }), - } -} - pub(super) fn number_literal_to_object( vm: &VirtualMachine, source_file: &SourceFile, @@ -364,6 +625,7 @@ pub(super) fn number_literal_to_object( node_index: _, range, value, + .. } = constant; let c = match value { ast::Number::Int(n) => Constant::new_int(n, range), @@ -382,6 +644,7 @@ pub(super) fn string_literal_to_object( node_index: _, range, value, + .. } = constant; let prefix = value .iter() @@ -400,6 +663,7 @@ pub(super) fn bytes_literal_to_object( node_index: _, range, value, + .. } = constant; let bytes = value.as_slice().iter().flat_map(|b| b.value.iter()); let c = Constant::new_bytes(bytes.copied().collect(), range); @@ -415,6 +679,7 @@ pub(super) fn boolean_literal_to_object( node_index: _, range, value, + .. } = constant; let c = Constant::new_bool(value, range); c.ast_to_object(vm, source_file) @@ -428,6 +693,7 @@ pub(super) fn none_literal_to_object( let ast::ExprNoneLiteral { node_index: _, range, + .. } = constant; let c = Constant::new_none(range); c.ast_to_object(vm, source_file) @@ -441,6 +707,7 @@ pub(super) fn ellipsis_literal_to_object( let ast::ExprEllipsisLiteral { node_index: _, range, + .. } = constant; let c = Constant::new_ellipsis(range); c.ast_to_object(vm, source_file) diff --git a/crates/vm/src/stdlib/_ast/elif_else_clause.rs b/crates/vm/src/stdlib/_ast/elif_else_clause.rs index 0afdbc02ac1..2097c349c3f 100644 --- a/crates/vm/src/stdlib/_ast/elif_else_clause.rs +++ b/crates/vm/src/stdlib/_ast/elif_else_clause.rs @@ -12,10 +12,15 @@ pub(super) fn ast_to_object( range, test, body, + runtime_body, + runtime_orelse, } = clause; let Some(test) = test else { assert!(rest.len() == 0); - return body.ast_to_object(vm, source_file); + return runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); }; let node = NodeAst .into_ref_with_type(vm, pyast::NodeStmtIf::static_type().to_owned()) @@ -24,10 +29,15 @@ pub(super) fn ast_to_object( dict.set_item("test", test.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); - let orelse = if let Some(next) = rest.next() { + let orelse = if let Some(values) = runtime_orelse { + values.ast_to_object(vm, source_file) + } else if let Some(next) = rest.next() { if next.test.is_some() { let next = ast::ElifElseClause { range: TextRange::new(next.range.start(), range.end()), @@ -37,7 +47,10 @@ pub(super) fn ast_to_object( .new_list(vec![ast_to_object(next, rest, vm, source_file)]) .into() } else { - next.body.ast_to_object(vm, source_file) + next.runtime_body.map_or_else( + || next.body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ) } } else { vm.ctx.new_list(vec![]).into() @@ -48,40 +61,45 @@ pub(super) fn ast_to_object( node.into() } -pub(super) fn ast_from_object( +pub(super) fn ast_from_object_with_range( vm: &VirtualMachine, source_file: &SourceFile, object: PyObjectRef, + range: TextRange, ) -> PyResult { - let test = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "test", "If")?)?; - let body = Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "body", "If")?)?; - let orelse: Vec = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "orelse", "If")?, - )?; - let range = range_from_object(vm, source_file, object, "If")?; + let test = get_required_node_field(vm, source_file, &object, "test", "If")?; + let body: Vec> = get_node_list_field(vm, source_file, &object, "body", "If")?; + let orelse: Vec> = + get_node_list_field(vm, source_file, &object, "orelse", "If")?; + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_orelse = runtime_stmt_list_metadata(&orelse); + let body = lower_runtime_stmt_list(body); + let orelse = lower_runtime_stmt_list(orelse); let elif_else_clauses = if orelse.is_empty() { vec![] } else if let [ast::Stmt::If(_)] = &*orelse { let Some(ast::Stmt::If(ast::StmtIf { - node_index: _, + node_index, range, test, body, mut elif_else_clauses, + runtime_body, })) = orelse.into_iter().next() else { unreachable!() }; + debug_assert!(runtime_orelse.is_none()); elif_else_clauses.insert( 0, ast::ElifElseClause { - node_index: Default::default(), + node_index, range, test: Some(*test), body, + runtime_body, + runtime_orelse: None, }, ); elif_else_clauses @@ -91,6 +109,8 @@ pub(super) fn ast_from_object( range, test: None, body: orelse, + runtime_body: runtime_orelse, + runtime_orelse: None, }] }; @@ -100,5 +120,6 @@ pub(super) fn ast_from_object( body, elif_else_clauses, range, + runtime_body, }) } diff --git a/crates/vm/src/stdlib/_ast/exception.rs b/crates/vm/src/stdlib/_ast/exception.rs index 2daabecc84c..b79e52d05e6 100644 --- a/crates/vm/src/stdlib/_ast/exception.rs +++ b/crates/vm/src/stdlib/_ast/exception.rs @@ -1,6 +1,22 @@ use super::*; use rustpython_compiler_core::SourceFile; +fn ensure_excepthandler_node(vm: &VirtualMachine, object: &PyObjectRef) -> PyResult<()> { + if vm.is_none(object) + || !is_node_instance( + vm, + object, + pyast::NodeExceptHandlerExceptHandler::static_type(), + )? + { + return Err(vm.new_type_error(format!( + "expected some sort of excepthandler, but got {}", + object.repr(vm)? + ))); + } + Ok(()) +} + // sum impl Node for ast::ExceptHandler { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { @@ -13,25 +29,53 @@ impl Node for ast::ExceptHandler { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok( - if cls.is(pyast::NodeExceptHandlerExceptHandler::static_type()) { - Self::ExceptHandler(ast::ExceptHandlerExceptHandler::ast_from_object( - vm, - source_file, - object, - )?) - } else { - return Err(vm.new_type_error(format!( - "expected some sort of excepthandler, but got {}", - object.repr(vm)? - ))); - }, - ) + ensure_excepthandler_node(vm, &object)?; + let range = excepthandler_range_from_object(vm, source_file, object.clone())?; + Ok(Self::ExceptHandler(except_handler_from_object_with_range( + vm, + source_file, + object, + range, + )?)) } } // constructor +fn except_handler_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let body: Vec> = + 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 { + ensure_excepthandler_node(vm, &object)?; + let range = excepthandler_range_from_object_unvalidated(vm, source_file, object.clone())?; + Ok(ast::ExceptHandler::ExceptHandler( + except_handler_from_object_with_range(vm, source_file, object, range)?, + )) +} + impl Node for ast::ExceptHandlerExceptHandler { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -40,6 +84,7 @@ impl Node for ast::ExceptHandlerExceptHandler { name, body, range, + runtime_body, } = self; let node = NodeAst .into_ref_with_type( @@ -52,8 +97,11 @@ impl Node for ast::ExceptHandlerExceptHandler { .unwrap(); dict.set_item("name", name.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -63,20 +111,7 @@ impl Node for ast::ExceptHandlerExceptHandler { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - type_: get_node_field_opt(vm, &object, "type")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - name: get_node_field_opt(vm, &object, "name")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "ExceptHandler")?, - )?, - range: range_from_object(vm, source_file, object, "ExceptHandler")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "ExceptHandler")?; + except_handler_from_object_with_range(vm, source_file, object, range) } } diff --git a/crates/vm/src/stdlib/_ast/expression.rs b/crates/vm/src/stdlib/_ast/expression.rs index 2b32a33f34d..10bdf526684 100644 --- a/crates/vm/src/stdlib/_ast/expression.rs +++ b/crates/vm/src/stdlib/_ast/expression.rs @@ -1,8 +1,7 @@ use super::*; -use crate::stdlib::_ast::{ - argument::{merge_function_call_arguments, split_function_call_arguments}, - constant::Constant, - string::JoinedStr, +use crate::stdlib::_ast::argument::{ + KeywordArguments, PositionalArguments, merge_function_call_arguments, + split_function_call_arguments, }; use rustpython_compiler_core::SourceFile; @@ -27,6 +26,7 @@ impl Node for ast::Expr { Self::YieldFrom(cons) => cons.ast_to_object(vm, source_file), Self::Compare(cons) => cons.ast_to_object(vm, source_file), Self::Call(cons) => cons.ast_to_object(vm, source_file), + Self::Constant(cons) => constant::expr_constant_to_object(vm, source_file, cons), Self::Attribute(cons) => cons.ast_to_object(vm, source_file), Self::Subscript(cons) => cons.ast_to_object(vm, source_file), Self::Starred(cons) => cons.ast_to_object(vm, source_file), @@ -47,7 +47,7 @@ impl Node for ast::Expr { } Self::Named(cons) => cons.ast_to_object(vm, source_file), Self::IpyEscapeCommand(_) => { - unimplemented!("IPython escape command is not allowed in Python AST") + unreachable!("IPython escape command is not part of Python AST") } } } @@ -57,94 +57,307 @@ impl Node for ast::Expr { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeExprBoolOp::static_type()) { - Self::BoolOp(ast::ExprBoolOp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprNamedExpr::static_type()) { - Self::Named(ast::ExprNamed::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprBinOp::static_type()) { - Self::BinOp(ast::ExprBinOp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprUnaryOp::static_type()) { - Self::UnaryOp(ast::ExprUnaryOp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprLambda::static_type()) { - Self::Lambda(ast::ExprLambda::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprIfExp::static_type()) { - Self::If(ast::ExprIf::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprDict::static_type()) { - Self::Dict(ast::ExprDict::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprSet::static_type()) { - Self::Set(ast::ExprSet::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprListComp::static_type()) { - Self::ListComp(ast::ExprListComp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprSetComp::static_type()) { - Self::SetComp(ast::ExprSetComp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprDictComp::static_type()) { - Self::DictComp(ast::ExprDictComp::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprGeneratorExp::static_type()) { - Self::Generator(ast::ExprGenerator::ast_from_object( + if vm.is_none(&object) { + return Err(vm.new_type_error(format!( + "expected some sort of expr, but got {}", + object.repr(vm)? + ))); + } + enum ExprKind { + BoolOp, + Named, + BinOp, + UnaryOp, + Lambda, + If, + Dict, + Set, + ListComp, + SetComp, + DictComp, + Generator, + Await, + Yield, + YieldFrom, + Compare, + Call, + FormattedValue, + Interpolation, + JoinedStr, + TemplateStr, + Constant, + Attribute, + Subscript, + Starred, + Name, + List, + Tuple, + Slice, + } + let kind = if is_node_instance(vm, &object, pyast::NodeExprBoolOp::static_type())? { + ExprKind::BoolOp + } else if is_node_instance(vm, &object, pyast::NodeExprNamedExpr::static_type())? { + ExprKind::Named + } else if is_node_instance(vm, &object, pyast::NodeExprBinOp::static_type())? { + ExprKind::BinOp + } else if is_node_instance(vm, &object, pyast::NodeExprUnaryOp::static_type())? { + ExprKind::UnaryOp + } else if is_node_instance(vm, &object, pyast::NodeExprLambda::static_type())? { + ExprKind::Lambda + } else if is_node_instance(vm, &object, pyast::NodeExprIfExp::static_type())? { + ExprKind::If + } else if is_node_instance(vm, &object, pyast::NodeExprDict::static_type())? { + ExprKind::Dict + } else if is_node_instance(vm, &object, pyast::NodeExprSet::static_type())? { + ExprKind::Set + } else if is_node_instance(vm, &object, pyast::NodeExprListComp::static_type())? { + ExprKind::ListComp + } else if is_node_instance(vm, &object, pyast::NodeExprSetComp::static_type())? { + ExprKind::SetComp + } else if is_node_instance(vm, &object, pyast::NodeExprDictComp::static_type())? { + ExprKind::DictComp + } else if is_node_instance(vm, &object, pyast::NodeExprGeneratorExp::static_type())? { + ExprKind::Generator + } else if is_node_instance(vm, &object, pyast::NodeExprAwait::static_type())? { + ExprKind::Await + } else if is_node_instance(vm, &object, pyast::NodeExprYield::static_type())? { + ExprKind::Yield + } else if is_node_instance(vm, &object, pyast::NodeExprYieldFrom::static_type())? { + ExprKind::YieldFrom + } else if is_node_instance(vm, &object, pyast::NodeExprCompare::static_type())? { + ExprKind::Compare + } else if is_node_instance(vm, &object, pyast::NodeExprCall::static_type())? { + ExprKind::Call + } else if is_node_instance(vm, &object, pyast::NodeExprFormattedValue::static_type())? { + ExprKind::FormattedValue + } else if is_node_instance(vm, &object, pyast::NodeExprInterpolation::static_type())? { + ExprKind::Interpolation + } else if is_node_instance(vm, &object, pyast::NodeExprJoinedStr::static_type())? { + ExprKind::JoinedStr + } else if is_node_instance(vm, &object, pyast::NodeExprTemplateStr::static_type())? { + ExprKind::TemplateStr + } else if is_node_instance(vm, &object, pyast::NodeExprConstant::static_type())? { + ExprKind::Constant + } else if is_node_instance(vm, &object, pyast::NodeExprAttribute::static_type())? { + ExprKind::Attribute + } else if is_node_instance(vm, &object, pyast::NodeExprSubscript::static_type())? { + ExprKind::Subscript + } else if is_node_instance(vm, &object, pyast::NodeExprStarred::static_type())? { + ExprKind::Starred + } else if is_node_instance(vm, &object, pyast::NodeExprName::static_type())? { + ExprKind::Name + } else if is_node_instance(vm, &object, pyast::NodeExprList::static_type())? { + ExprKind::List + } else if is_node_instance(vm, &object, pyast::NodeExprTuple::static_type())? { + ExprKind::Tuple + } else if is_node_instance(vm, &object, pyast::NodeExprSlice::static_type())? { + ExprKind::Slice + } else { + return Err(vm.new_type_error(format!( + "expected some sort of expr, but got {}", + object.repr(vm)? + ))); + }; + let range = expr_range_from_object(vm, source_file, object.clone())?; + Ok(match kind { + ExprKind::BoolOp => Self::BoolOp(expr_bool_op_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeExprAwait::static_type()) { - Self::Await(ast::ExprAwait::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprYield::static_type()) { - Self::Yield(ast::ExprYield::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprYieldFrom::static_type()) { - Self::YieldFrom(ast::ExprYieldFrom::ast_from_object( + range, + )?), + ExprKind::Named => Self::Named(expr_named_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeExprCompare::static_type()) { - Self::Compare(ast::ExprCompare::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprCall::static_type()) { - Self::Call(ast::ExprCall::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprAttribute::static_type()) { - Self::Attribute(ast::ExprAttribute::ast_from_object( + range, + )?), + ExprKind::BinOp => Self::BinOp(expr_bin_op_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeExprSubscript::static_type()) { - Self::Subscript(ast::ExprSubscript::ast_from_object( + range, + )?), + ExprKind::UnaryOp => Self::UnaryOp(expr_unary_op_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeExprStarred::static_type()) { - Self::Starred(ast::ExprStarred::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprName::static_type()) { - Self::Name(ast::ExprName::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprList::static_type()) { - Self::List(ast::ExprList::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprTuple::static_type()) { - Self::Tuple(ast::ExprTuple::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprSlice::static_type()) { - Self::Slice(ast::ExprSlice::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeExprConstant::static_type()) { - Constant::ast_from_object(vm, source_file, object)?.into_expr() - } else if cls.is(pyast::NodeExprJoinedStr::static_type()) { - JoinedStr::ast_from_object(vm, source_file, object)?.into_expr() - } else if cls.is(pyast::NodeExprTemplateStr::static_type()) { - let template = string::TemplateStr::ast_from_object(vm, source_file, object)?; - return string::template_str_to_expr(vm, template); - } else if cls.is(pyast::NodeExprInterpolation::static_type()) { - let interpolation = - string::TStringInterpolation::ast_from_object(vm, source_file, object)?; - return string::interpolation_to_expr(vm, interpolation); - } else if vm.is_none(&object) { - return Err(vm.new_value_error("None disallowed in expression list")); - } else { - return Err(vm.new_type_error(format!( - "expected some sort of expr, but got {}", - object.repr(vm)? - ))); + range, + )?), + ExprKind::Lambda => Self::Lambda(expr_lambda_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::If => Self::If(expr_if_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Dict => Self::Dict(expr_dict_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Set => Self::Set(expr_set_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::ListComp => Self::ListComp(expr_list_comp_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::SetComp => Self::SetComp(expr_set_comp_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::DictComp => Self::DictComp(expr_dict_comp_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Generator => Self::Generator(expr_generator_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Await => Self::Await(expr_await_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Yield => Self::Yield(expr_yield_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::YieldFrom => Self::YieldFrom(expr_yield_from_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Compare => Self::Compare(expr_compare_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Call => Self::Call(expr_call_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::FormattedValue => { + let formatted = + string::formatted_value_from_object_with_range(vm, source_file, object, range)?; + string::formatted_value_to_expr(true, formatted) + } + ExprKind::Interpolation => { + let interpolation = string::tstring_interpolation_from_object_with_range( + vm, + source_file, + object, + range, + )?; + string::interpolation_to_expr(vm, source_file, interpolation)? + } + ExprKind::JoinedStr => { + string::joined_str_from_object_with_range(vm, source_file, object, range)? + .into_expr(true) + } + ExprKind::TemplateStr => { + let template = + string::template_str_from_object_with_range(vm, source_file, object, range)?; + string::template_str_to_expr(vm, source_file, template)? + } + ExprKind::Constant => { + constant::constant_from_object_with_range(vm, source_file, object, range)? + .into_expr() + } + ExprKind::Attribute => Self::Attribute(expr_attribute_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Subscript => Self::Subscript(expr_subscript_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Starred => Self::Starred(expr_starred_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Name => Self::Name(expr_name_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::List => Self::List(expr_list_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Tuple => Self::Tuple(expr_tuple_from_object_with_range( + vm, + source_file, + object, + range, + )?), + ExprKind::Slice => Self::Slice(expr_slice_from_object_with_range( + vm, + source_file, + object, + range, + )?), }) } } // constructor +fn expr_bool_op_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let values: Vec> = + get_node_list_field(vm, source_file, &object, "values", "BoolOp")?; + let (runtime_values, values) = runtime_expr_list_from_values(values); + Ok(ast::ExprBoolOp { + node_index: Default::default(), + op: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "op", "BoolOp")?, + )?, + values, + range, + runtime_values, + }) +} + impl Node for ast::ExprBoolOp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -152,6 +365,7 @@ impl Node for ast::ExprBoolOp { op, values, range, + runtime_values, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprBoolOp::static_type().to_owned()) @@ -159,8 +373,11 @@ impl Node for ast::ExprBoolOp { let dict = node.as_object().dict().unwrap(); dict.set_item("op", op.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("values", values.ast_to_object(vm, source_file), vm) - .unwrap(); + let values = runtime_values.map_or_else( + || values.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("values", values, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -170,24 +387,26 @@ impl Node for ast::ExprBoolOp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - op: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "op", "BoolOp")?, - )?, - values: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "values", "BoolOp")?, - )?, - range: range_from_object(vm, source_file, object, "BoolOp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "BoolOp")?; + expr_bool_op_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_named_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprNamed { + node_index: Default::default(), + target: get_required_node_field(vm, source_file, &object, "target", "NamedExpr")?, + value: get_required_node_field(vm, source_file, &object, "value", "NamedExpr")?, + range, + }) +} + impl Node for ast::ExprNamed { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -213,24 +432,31 @@ impl Node for ast::ExprNamed { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - target: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "target", "NamedExpr")?, - )?, - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "NamedExpr")?, - )?, - range: range_from_object(vm, source_file, object, "NamedExpr")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "NamedExpr")?; + expr_named_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_bin_op_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprBinOp { + node_index: Default::default(), + left: get_required_node_field(vm, source_file, &object, "left", "BinOp")?, + op: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "op", "BinOp")?, + )?, + right: get_required_node_field(vm, source_file, &object, "right", "BinOp")?, + range, + }) +} + impl Node for ast::ExprBinOp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -259,29 +485,30 @@ impl Node for ast::ExprBinOp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - left: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "left", "BinOp")?, - )?, - op: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "op", "BinOp")?, - )?, - right: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "right", "BinOp")?, - )?, - range: range_from_object(vm, source_file, object, "BinOp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "BinOp")?; + expr_bin_op_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_unary_op_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprUnaryOp { + node_index: Default::default(), + op: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "op", "UnaryOp")?, + )?, + operand: get_required_node_field(vm, source_file, &object, "operand", "UnaryOp")?, + range, + }) +} + impl Node for ast::ExprUnaryOp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -306,24 +533,30 @@ impl Node for ast::ExprUnaryOp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - op: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "op", "UnaryOp")?, - )?, - operand: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "operand", "UnaryOp")?, - )?, - range: range_from_object(vm, source_file, object, "UnaryOp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "UnaryOp")?; + expr_unary_op_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_lambda_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprLambda { + node_index: Default::default(), + parameters: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "args", "Lambda")?, + )?, + body: get_required_node_field(vm, source_file, &object, "body", "Lambda")?, + range, + }) +} + impl Node for ast::ExprLambda { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -336,7 +569,6 @@ impl Node for ast::ExprLambda { .into_ref_with_type(vm, pyast::NodeExprLambda::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - // Lambda with no parameters should have an empty arguments object, not None let args = match parameters { Some(params) => params.ast_to_object(vm, source_file), None => empty_arguments_object(vm), @@ -353,24 +585,27 @@ impl Node for ast::ExprLambda { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - parameters: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "args", "Lambda")?, - )?, - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "Lambda")?, - )?, - range: range_from_object(vm, source_file, object, "Lambda")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Lambda")?; + expr_lambda_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_if_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprIf { + node_index: Default::default(), + test: get_required_node_field(vm, source_file, &object, "test", "IfExp")?, + body: get_required_node_field(vm, source_file, &object, "body", "IfExp")?, + orelse: get_required_node_field(vm, source_file, &object, "orelse", "IfExp")?, + range, + }) +} + impl Node for ast::ExprIf { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -399,35 +634,46 @@ impl Node for ast::ExprIf { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - test: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "test", "IfExp")?, - )?, - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "IfExp")?, - )?, - orelse: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "orelse", "IfExp")?, - )?, - range: range_from_object(vm, source_file, object, "IfExp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "IfExp")?; + expr_if_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_dict_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let keys: Vec> = + get_node_list_field(vm, source_file, &object, "keys", "Dict")?; + let values: Vec> = + get_node_list_field(vm, source_file, &object, "values", "Dict")?; + if keys.len() != values.len() { + return Err(vm.new_value_error("Dict doesn't have the same number of keys as values")); + } + let runtime_values = runtime_expr_list_metadata(&values); + let items = keys + .into_iter() + .zip(lower_runtime_expr_list(values)) + .map(|(key, value)| ast::DictItem { key, value }) + .collect(); + Ok(ast::ExprDict { + node_index: Default::default(), + items, + range, + runtime_values, + }) +} + impl Node for ast::ExprDict { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, items, range, + runtime_values, } = self; let (keys, values) = items @@ -443,8 +689,11 @@ impl Node for ast::ExprDict { let dict = node.as_object().dict().unwrap(); dict.set_item("keys", keys.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("values", values.ast_to_object(vm, source_file), vm) - .unwrap(); + let values = runtime_values.map_or_else( + || values.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("values", values, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -454,46 +703,46 @@ impl Node for ast::ExprDict { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let keys: Vec> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "keys", "Dict")?, - )?; - let values: Vec<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "values", "Dict")?, - )?; - if keys.len() != values.len() { - return Err(vm.new_value_error("Dict doesn't have the same number of keys as values")); - } - let items = keys - .into_iter() - .zip(values) - .map(|(key, value)| ast::DictItem { key, value }) - .collect(); - Ok(Self { - node_index: Default::default(), - items, - range: range_from_object(vm, source_file, object, "Dict")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Dict")?; + expr_dict_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_set_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let elts: Vec> = + get_node_list_field(vm, source_file, &object, "elts", "Set")?; + let (runtime_elts, elts) = runtime_expr_list_from_values(elts); + Ok(ast::ExprSet { + node_index: Default::default(), + elts, + range, + runtime_elts, + }) +} + impl Node for ast::ExprSet { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, elts, range, + runtime_elts, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprSet::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("elts", elts.ast_to_object(vm, source_file), vm) - .unwrap(); + let elts = runtime_elts.map_or_else( + || elts.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("elts", elts, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -502,19 +751,26 @@ impl Node for ast::ExprSet { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elts: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elts", "Set")?, - )?, - range: range_from_object(vm, source_file, object, "Set")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Set")?; + expr_set_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_list_comp_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprListComp { + node_index: Default::default(), + elt: get_required_node_field(vm, source_file, &object, "elt", "ListComp")?, + generators: get_node_list_field(vm, source_file, &object, "generators", "ListComp")?, + range, + }) +} + impl Node for ast::ExprListComp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -540,24 +796,26 @@ impl Node for ast::ExprListComp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elt: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elt", "ListComp")?, - )?, - generators: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "generators", "ListComp")?, - )?, - range: range_from_object(vm, source_file, object, "ListComp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "ListComp")?; + expr_list_comp_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_set_comp_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprSetComp { + node_index: Default::default(), + elt: get_required_node_field(vm, source_file, &object, "elt", "SetComp")?, + generators: get_node_list_field(vm, source_file, &object, "generators", "SetComp")?, + range, + }) +} + impl Node for ast::ExprSetComp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -583,24 +841,27 @@ impl Node for ast::ExprSetComp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elt: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elt", "SetComp")?, - )?, - generators: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "generators", "SetComp")?, - )?, - range: range_from_object(vm, source_file, object, "SetComp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "SetComp")?; + expr_set_comp_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_dict_comp_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprDictComp { + node_index: Default::default(), + key: get_required_node_field(vm, source_file, &object, "key", "DictComp")?, + value: get_required_node_field(vm, source_file, &object, "value", "DictComp")?, + generators: get_node_list_field(vm, source_file, &object, "generators", "DictComp")?, + range, + }) +} + impl Node for ast::ExprDictComp { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -629,29 +890,27 @@ impl Node for ast::ExprDictComp { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - key: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "key", "DictComp")?, - )?, - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "DictComp")?, - )?, - generators: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "generators", "DictComp")?, - )?, - range: range_from_object(vm, source_file, object, "DictComp")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "DictComp")?; + expr_dict_comp_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_generator_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprGenerator { + node_index: Default::default(), + elt: get_required_node_field(vm, source_file, &object, "elt", "GeneratorExp")?, + generators: get_node_list_field(vm, source_file, &object, "generators", "GeneratorExp")?, + range, + parenthesized: true, + }) +} + impl Node for ast::ExprGenerator { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -688,26 +947,25 @@ impl Node for ast::ExprGenerator { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elt: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elt", "GeneratorExp")?, - )?, - generators: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "generators", "GeneratorExp")?, - )?, - range: range_from_object(vm, source_file, object, "GeneratorExp")?, - // TODO: Is this correct? - parenthesized: true, - }) + let range = range_from_object(vm, source_file, object.clone(), "GeneratorExp")?; + expr_generator_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_await_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprAwait { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "Await")?, + range, + }) +} + impl Node for ast::ExprAwait { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -729,19 +987,27 @@ impl Node for ast::ExprAwait { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Await")?, - )?, - range: range_from_object(vm, source_file, object, "Await")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Await")?; + expr_await_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_yield_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprYield { + node_index: Default::default(), + value: get_node_field_opt(vm, &object, "value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::ExprYield { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -764,17 +1030,25 @@ impl Node for ast::ExprYield { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: get_node_field_opt(vm, &object, "value")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "Yield")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Yield")?; + expr_yield_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_yield_from_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprYieldFrom { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "YieldFrom")?, + range, + }) +} + impl Node for ast::ExprYieldFrom { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -797,19 +1071,31 @@ impl Node for ast::ExprYieldFrom { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field_required(vm, &object, "value", "YieldFrom")?, - )?, - range: range_from_object(vm, source_file, object, "YieldFrom")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "YieldFrom")?; + expr_yield_from_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_compare_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let comparators: Vec> = + get_node_list_field(vm, source_file, &object, "comparators", "Compare")?; + let (runtime_comparators, comparators) = runtime_expr_boxed_slice_from_values(comparators); + Ok(ast::ExprCompare { + node_index: Default::default(), + left: get_required_node_field(vm, source_file, &object, "left", "Compare")?, + ops: get_node_boxed_slice_field(vm, source_file, &object, "ops", "Compare")?, + comparators, + range, + runtime_comparators, + }) +} + impl Node for ast::ExprCompare { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -818,6 +1104,7 @@ impl Node for ast::ExprCompare { ops, comparators, range, + runtime_comparators, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprCompare::static_type().to_owned()) @@ -827,12 +1114,11 @@ impl Node for ast::ExprCompare { .unwrap(); dict.set_item("ops", BoxedSlice(ops).ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item( - "comparators", - BoxedSlice(comparators).ast_to_object(vm, source_file), - vm, - ) - .unwrap(); + let comparators = runtime_comparators.map_or_else( + || BoxedSlice(comparators).ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("comparators", comparators, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -842,35 +1128,29 @@ impl Node for ast::ExprCompare { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - left: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "left", "Compare")?, - )?, - ops: { - let ops: BoxedSlice<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ops", "Compare")?, - )?; - ops.0 - }, - comparators: { - let comparators: BoxedSlice<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "comparators", "Compare")?, - )?; - comparators.0 - }, - range: range_from_object(vm, source_file, object, "Compare")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Compare")?; + expr_compare_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_call_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprCall { + node_index: Default::default(), + func: get_required_node_field(vm, source_file, &object, "func", "Call")?, + arguments: merge_function_call_arguments( + PositionalArguments::ast_from_field(vm, source_file, &object, "args", "Call")?, + KeywordArguments::ast_from_field(vm, source_file, &object, "keywords", "Call")?, + ), + range, + }) +} + impl Node for ast::ExprCall { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -907,31 +1187,31 @@ impl Node for ast::ExprCall { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - func: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "func", "Call")?, - )?, - arguments: merge_function_call_arguments( - Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "args", "Call")?, - )?, - Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "keywords", "Call")?, - )?, - ), - range: range_from_object(vm, source_file, object, "Call")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Call")?; + expr_call_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_attribute_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprAttribute { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "Attribute")?, + attr: get_required_identifier_field(vm, source_file, &object, "attr", "Attribute")?, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "Attribute")?, + )?, + range, + }) +} + impl Node for ast::ExprAttribute { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -960,29 +1240,31 @@ impl Node for ast::ExprAttribute { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Attribute")?, - )?, - attr: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "attr", "Attribute")?, - )?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "Attribute")?, - )?, - range: range_from_object(vm, source_file, object, "Attribute")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Attribute")?; + expr_attribute_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_subscript_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprSubscript { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "Subscript")?, + slice: get_required_node_field(vm, source_file, &object, "slice", "Subscript")?, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "Subscript")?, + )?, + range, + }) +} + impl Node for ast::ExprSubscript { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1010,29 +1292,30 @@ impl Node for ast::ExprSubscript { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Subscript")?, - )?, - slice: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "slice", "Subscript")?, - )?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "Subscript")?, - )?, - range: range_from_object(vm, source_file, object, "Subscript")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Subscript")?; + expr_subscript_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_starred_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprStarred { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "Starred")?, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "Starred")?, + )?, + range, + }) +} + impl Node for ast::ExprStarred { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1057,24 +1340,30 @@ impl Node for ast::ExprStarred { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Starred")?, - )?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "Starred")?, - )?, - range: range_from_object(vm, source_file, object, "Starred")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Starred")?; + expr_starred_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_name_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprName { + node_index: Default::default(), + id: get_required_identifier_field(vm, source_file, &object, "id", "Name")?, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "Name")?, + )?, + range, + }) +} + impl Node for ast::ExprName { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1087,7 +1376,8 @@ impl Node for ast::ExprName { .into_ref_with_type(vm, pyast::NodeExprName::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("id", id.to_pyobject(vm), vm).unwrap(); + dict.set_item("id", id.ast_to_object(vm, source_file), vm) + .unwrap(); dict.set_item("ctx", ctx.ast_to_object(vm, source_file), vm) .unwrap(); node_add_location(&dict, range, vm, source_file); @@ -1099,20 +1389,34 @@ impl Node for ast::ExprName { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - id: Node::ast_from_object(vm, source_file, get_node_field(vm, &object, "id", "Name")?)?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "Name")?, - )?, - range: range_from_object(vm, source_file, object, "Name")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Name")?; + expr_name_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_list_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let elts: Vec> = + get_node_list_field(vm, source_file, &object, "elts", "List")?; + let (runtime_elts, elts) = runtime_expr_list_from_values(elts); + Ok(ast::ExprList { + node_index: Default::default(), + elts, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "List")?, + )?, + range, + runtime_elts, + }) +} + impl Node for ast::ExprList { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1120,13 +1424,17 @@ impl Node for ast::ExprList { elts, ctx, range, + runtime_elts, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprList::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("elts", elts.ast_to_object(vm, source_file), vm) - .unwrap(); + let elts = runtime_elts.map_or_else( + || elts.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("elts", elts, vm).unwrap(); dict.set_item("ctx", ctx.ast_to_object(vm, source_file), vm) .unwrap(); node_add_location(&dict, range, vm, source_file); @@ -1138,24 +1446,35 @@ impl Node for ast::ExprList { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elts: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elts", "List")?, - )?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "List")?, - )?, - range: range_from_object(vm, source_file, object, "List")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "List")?; + expr_list_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_tuple_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let elts: Vec> = + get_node_list_field(vm, source_file, &object, "elts", "Tuple")?; + let (runtime_elts, elts) = runtime_expr_list_from_values(elts); + Ok(ast::ExprTuple { + node_index: Default::default(), + elts, + ctx: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "ctx", "Tuple")?, + )?, + range, + parenthesized: true, + runtime_elts, + }) +} + impl Node for ast::ExprTuple { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1164,13 +1483,17 @@ impl Node for ast::ExprTuple { ctx, range: _range, parenthesized: _, + runtime_elts, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprTuple::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("elts", elts.ast_to_object(vm, source_file), vm) - .unwrap(); + let elts = runtime_elts.map_or_else( + || elts.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("elts", elts, vm).unwrap(); dict.set_item("ctx", ctx.ast_to_object(vm, source_file), vm) .unwrap(); node_add_location(&dict, _range, vm, source_file); @@ -1182,25 +1505,33 @@ impl Node for ast::ExprTuple { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - elts: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "elts", "Tuple")?, - )?, - ctx: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ctx", "Tuple")?, - )?, - range: range_from_object(vm, source_file, object, "Tuple")?, - parenthesized: true, // TODO: is this correct? - }) + let range = range_from_object(vm, source_file, object.clone(), "Tuple")?; + expr_tuple_from_object_with_range(vm, source_file, object, range) } } // constructor +fn expr_slice_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::ExprSlice { + node_index: Default::default(), + lower: get_node_field_opt(vm, &object, "lower")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + upper: get_node_field_opt(vm, &object, "upper")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + step: get_node_field_opt(vm, &object, "step")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::ExprSlice { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1229,19 +1560,8 @@ impl Node for ast::ExprSlice { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - lower: get_node_field_opt(vm, &object, "lower")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - upper: get_node_field_opt(vm, &object, "upper")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - step: get_node_field_opt(vm, &object, "step")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "Slice")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Slice")?; + expr_slice_from_object_with_range(vm, source_file, object, range) } } @@ -1253,7 +1573,7 @@ impl Node for ast::ExprContext { Self::Store => pyast::NodeExprContextStore::static_type(), Self::Del => pyast::NodeExprContextDel::static_type(), Self::Invalid => { - unimplemented!("Invalid expression context is not allowed in Python AST") + unreachable!() } }; singleton_node_to_object(vm, node_type) @@ -1264,19 +1584,20 @@ impl Node for ast::ExprContext { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeExprContextLoad::static_type()) { - Self::Load - } else if cls.is(pyast::NodeExprContextStore::static_type()) { - Self::Store - } else if cls.is(pyast::NodeExprContextDel::static_type()) { - Self::Del - } else { - return Err(vm.new_type_error(format!( - "expected some sort of expr_context, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if is_node_instance(vm, &object, pyast::NodeExprContextLoad::static_type())? { + Self::Load + } else if is_node_instance(vm, &object, pyast::NodeExprContextStore::static_type())? { + Self::Store + } else if is_node_instance(vm, &object, pyast::NodeExprContextDel::static_type())? { + Self::Del + } else { + return Err(vm.new_type_error(format!( + "expected some sort of expr_context, but got {}", + object.repr(vm)? + ))); + }, + ) } } @@ -1290,6 +1611,8 @@ impl Node for ast::Comprehension { ifs, is_async, range: _range, + runtime_ifs, + runtime_is_async, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeComprehension::static_type().to_owned()) @@ -1299,10 +1622,16 @@ impl Node for ast::Comprehension { .unwrap(); dict.set_item("iter", iter.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("ifs", ifs.ast_to_object(vm, source_file), vm) - .unwrap(); - dict.set_item("is_async", is_async.ast_to_object(vm, source_file), vm) - .unwrap(); + let ifs = runtime_ifs.map_or_else( + || ifs.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("ifs", ifs, vm).unwrap(); + let is_async = runtime_is_async.map_or_else( + || is_async.ast_to_object(vm, source_file), + |value| vm.ctx.new_int(value).into(), + ); + dict.set_item("is_async", is_async, vm).unwrap(); node.into() } @@ -1311,29 +1640,23 @@ impl Node for ast::Comprehension { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { + let ifs: Vec> = + get_node_list_field(vm, source_file, &object, "ifs", "comprehension")?; + let is_async = node_object_to_i32( + vm, + get_node_field(vm, &object, "is_async", "comprehension")?, + )?; + let runtime_ifs = runtime_expr_list_metadata(&ifs); + let runtime_is_async = (is_async != 0 && is_async != 1).then_some(is_async); Ok(Self { node_index: Default::default(), - target: Node::ast_from_object( - vm, - source_file, - get_node_field_required(vm, &object, "target", "comprehension")?, - )?, - iter: Node::ast_from_object( - vm, - source_file, - get_node_field_required(vm, &object, "iter", "comprehension")?, - )?, - ifs: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "ifs", "comprehension")?, - )?, - is_async: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "is_async", "comprehension")?, - )?, + target: get_required_node_field(vm, source_file, &object, "target", "comprehension")?, + iter: get_required_node_field(vm, source_file, &object, "iter", "comprehension")?, + ifs: lower_runtime_expr_list(ifs), + is_async: is_async != 0, range: Default::default(), + runtime_ifs, + runtime_is_async, }) } } diff --git a/crates/vm/src/stdlib/_ast/module.rs b/crates/vm/src/stdlib/_ast/module.rs index b4c2468d33b..37a6bc62849 100644 --- a/crates/vm/src/stdlib/_ast/module.rs +++ b/crates/vm/src/stdlib/_ast/module.rs @@ -18,7 +18,7 @@ use rustpython_compiler_core::SourceFile; /// - `FunctionType`: A function signature with argument and return type /// annotations, representing the type hints of a function (e.g., `def add(x: int, y: int) -> int`). pub(super) enum Mod { - Module(ast::ModModule), + Module(ModModule), Interactive(ModInteractive), Expression(ast::ModExpression), FunctionType(ModFunctionType), @@ -40,53 +40,66 @@ impl Node for Mod { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeModModule::static_type()) { - Self::Module(ast::ModModule::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeModInteractive::static_type()) { - Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeModExpression::static_type()) { - Self::Expression(ast::ModExpression::ast_from_object( - vm, - source_file, - object, - )?) - } else if cls.is(pyast::NodeModFunctionType::static_type()) { - Self::FunctionType(ModFunctionType::ast_from_object(vm, source_file, object)?) - } else { - return Err(vm.new_type_error(format!( - "expected some sort of mod, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if object.is_instance(pyast::NodeModModule::static_type().as_object(), vm)? { + Self::Module(ModModule::ast_from_object(vm, source_file, object)?) + } else if object + .is_instance(pyast::NodeModInteractive::static_type().as_object(), vm)? + { + Self::Interactive(ModInteractive::ast_from_object(vm, source_file, object)?) + } else if object.is_instance(pyast::NodeModExpression::static_type().as_object(), vm)? { + Self::Expression(ast::ModExpression::ast_from_object( + vm, + source_file, + object, + )?) + } else if object + .is_instance(pyast::NodeModFunctionType::static_type().as_object(), vm)? + { + Self::FunctionType(ModFunctionType::ast_from_object(vm, source_file, object)?) + } else { + return Err(vm.new_type_error(format!( + "expected some sort of mod, but got {}", + object.repr(vm)? + ))); + }, + ) } } +pub(super) struct ModModule { + pub(crate) module: ast::ModModule, + pub(crate) type_ignores: Vec, +} + // constructor -impl Node for ast::ModModule { +impl Node for ModModule { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { + module, + type_ignores, + } = self; + let ast::ModModule { node_index: _, body, - // type_ignores, - range, - } = self; + range: _, + runtime_body, + } = module; let node = NodeAst .into_ref_with_type(vm, pyast::NodeModModule::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); - // TODO: Improve ruff API - // ruff ignores type_ignore comments currently. - let type_ignores: Vec = vec![]; + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); dict.set_item( "type_ignores", type_ignores.ast_to_object(vm, source_file), vm, ) .unwrap(); - let _ = range; node.into() } @@ -95,38 +108,45 @@ impl Node for ast::ModModule { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { + let body: Vec> = + get_node_list_field(vm, source_file, &object, "body", "Module")?; + let (runtime_body, body) = runtime_stmt_list_from_values(body); + let type_ignores = get_node_list_field(vm, source_file, &object, "type_ignores", "Module")?; Ok(Self { - node_index: Default::default(), - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "Module")?, - )?, - // type_ignores: Node::ast_from_object( - // _vm, - // get_node_field(_vm, &_object, "type_ignores", "Module")?, - // )?, - range: Default::default(), + module: ast::ModModule { + node_index: Default::default(), + body, + range: Default::default(), + runtime_body, + }, + type_ignores, }) } } pub(super) struct ModInteractive { pub(crate) range: TextRange, - pub(crate) body: Vec, + pub(crate) body: ast::Suite, + pub(crate) runtime_body: Option>>, } // constructor impl Node for ModInteractive { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { body, range } = self; + let Self { + body, + range: _, + runtime_body, + } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeModInteractive::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); - let _ = range; + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); node.into() } @@ -135,13 +155,13 @@ impl Node for ModInteractive { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { + let body: Vec> = + get_node_list_field(vm, source_file, &object, "body", "Interactive")?; + let (runtime_body, body) = runtime_stmt_list_from_values(body); Ok(Self { - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "Interactive")?, - )?, + body, range: Default::default(), + runtime_body, }) } } @@ -152,7 +172,7 @@ impl Node for ast::ModExpression { let Self { node_index: _, body, - range, + range: _, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeModExpression::static_type().to_owned()) @@ -160,7 +180,6 @@ impl Node for ast::ModExpression { let dict = node.as_object().dict().unwrap(); dict.set_item("body", body.ast_to_object(vm, source_file), vm) .unwrap(); - let _ = range; node.into() } @@ -171,11 +190,7 @@ impl Node for ast::ModExpression { ) -> PyResult { Ok(Self { node_index: Default::default(), - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "Expression")?, - )?, + body: get_required_node_field(vm, source_file, &object, "body", "Expression")?, range: Default::default(), }) } @@ -184,7 +199,7 @@ impl Node for ast::ModExpression { pub(super) struct ModFunctionType { pub(crate) argtypes: Box<[ast::Expr]>, pub(crate) returns: ast::Expr, - pub(crate) range: TextRange, + pub(crate) runtime_argtypes: Option>>, } // constructor @@ -193,21 +208,19 @@ impl Node for ModFunctionType { let Self { argtypes, returns, - range, + runtime_argtypes, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeModFunctionType::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item( - "argtypes", - BoxedSlice(argtypes).ast_to_object(vm, source_file), - vm, - ) - .unwrap(); + let argtypes = runtime_argtypes.map_or_else( + || BoxedSlice(argtypes).ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("argtypes", argtypes, vm).unwrap(); dict.set_item("returns", returns.ast_to_object(vm, source_file), vm) .unwrap(); - let _ = range; node.into() } @@ -216,21 +229,13 @@ impl Node for ModFunctionType { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { + let argtypes: Vec> = + get_node_list_field(vm, source_file, &object, "argtypes", "FunctionType")?; + let (runtime_argtypes, argtypes) = runtime_expr_list_from_values(argtypes); Ok(Self { - argtypes: { - let argtypes: BoxedSlice<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "argtypes", "FunctionType")?, - )?; - argtypes.0 - }, - returns: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "returns", "FunctionType")?, - )?, - range: Default::default(), + argtypes: argtypes.into_boxed_slice(), + returns: get_required_node_field(vm, source_file, &object, "returns", "FunctionType")?, + runtime_argtypes, }) } } diff --git a/crates/vm/src/stdlib/_ast/node.rs b/crates/vm/src/stdlib/_ast/node.rs index 4ee3893b665..7737ae076bb 100644 --- a/crates/vm/src/stdlib/_ast/node.rs +++ b/crates/vm/src/stdlib/_ast/node.rs @@ -1,5 +1,6 @@ -use crate::{PyObjectRef, PyResult, VirtualMachine}; +use crate::{PyObjectRef, PyResult, VirtualMachine, builtins::PyList}; use rustpython_compiler_core::SourceFile; +use thin_vec::ThinVec; pub(crate) trait Node: Sized { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef; @@ -31,14 +32,52 @@ impl Node for Vec { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - // Recursion guard for each element: prevents stack overflow when a - // sequence element transitively references the sequence itself - // (e.g. `l = ast.List(...); l.elts = [l]`). See issue #4862. - vm.extract_elements_with(&object, |obj| { - vm.with_recursion("while traversing AST node", || { - Node::ast_from_object(vm, source_file, obj) - }) - }) + let list = object.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + "AST list field must be a list, not a {}", + object.class().name() + )) + })?; + let len = list.borrow_vec().len(); + let mut result = Self::with_capacity(len); + for i in 0..len { + let item = { + let items = list.borrow_vec(); + if items.len() != len { + return Err( + vm.new_runtime_error("AST list field changed size during iteration") + ); + } + items[i].clone() + }; + result.push(vm.with_recursion("while traversing AST node", || { + Node::ast_from_object(vm, source_file, item) + })?); + if list.borrow_vec().len() != len { + return Err(vm.new_runtime_error("AST list field changed size during iteration")); + } + } + Ok(result) + } +} + +impl Node for ThinVec { + 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 { + Vec::::ast_from_object(vm, source_file, object).map(Into::into) } } @@ -52,10 +91,6 @@ impl Node for Box { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - // Recursion guard: every descent through a Box increments the - // VM's recursion depth so cyclic or pathologically deep ASTs raise - // RecursionError instead of overflowing the native stack. - // See issue #4862. vm.with_recursion("while traversing AST node", || { T::ast_from_object(vm, source_file, object).map(Self::new) }) diff --git a/crates/vm/src/stdlib/_ast/operator.rs b/crates/vm/src/stdlib/_ast/operator.rs index 09e63b5d6ce..e05e490bb84 100644 --- a/crates/vm/src/stdlib/_ast/operator.rs +++ b/crates/vm/src/stdlib/_ast/operator.rs @@ -16,17 +16,18 @@ impl Node for ast::BoolOp { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeBoolOpAnd::static_type()) { - Self::And - } else if cls.is(pyast::NodeBoolOpOr::static_type()) { - Self::Or - } else { - return Err(vm.new_type_error(format!( - "expected some sort of boolop, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if is_node_instance(vm, &object, pyast::NodeBoolOpAnd::static_type())? { + Self::And + } else if is_node_instance(vm, &object, pyast::NodeBoolOpOr::static_type())? { + Self::Or + } else { + return Err(vm.new_type_error(format!( + "expected some sort of boolop, but got {}", + object.repr(vm)? + ))); + }, + ) } } @@ -56,39 +57,40 @@ impl Node for ast::Operator { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeOperatorAdd::static_type()) { - Self::Add - } else if cls.is(pyast::NodeOperatorSub::static_type()) { - Self::Sub - } else if cls.is(pyast::NodeOperatorMult::static_type()) { - Self::Mult - } else if cls.is(pyast::NodeOperatorMatMult::static_type()) { - Self::MatMult - } else if cls.is(pyast::NodeOperatorDiv::static_type()) { - Self::Div - } else if cls.is(pyast::NodeOperatorMod::static_type()) { - Self::Mod - } else if cls.is(pyast::NodeOperatorPow::static_type()) { - Self::Pow - } else if cls.is(pyast::NodeOperatorLShift::static_type()) { - Self::LShift - } else if cls.is(pyast::NodeOperatorRShift::static_type()) { - Self::RShift - } else if cls.is(pyast::NodeOperatorBitOr::static_type()) { - Self::BitOr - } else if cls.is(pyast::NodeOperatorBitXor::static_type()) { - Self::BitXor - } else if cls.is(pyast::NodeOperatorBitAnd::static_type()) { - Self::BitAnd - } else if cls.is(pyast::NodeOperatorFloorDiv::static_type()) { - Self::FloorDiv - } else { - return Err(vm.new_type_error(format!( - "expected some sort of operator, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if is_node_instance(vm, &object, pyast::NodeOperatorAdd::static_type())? { + Self::Add + } else if is_node_instance(vm, &object, pyast::NodeOperatorSub::static_type())? { + Self::Sub + } else if is_node_instance(vm, &object, pyast::NodeOperatorMult::static_type())? { + Self::Mult + } else if is_node_instance(vm, &object, pyast::NodeOperatorMatMult::static_type())? { + Self::MatMult + } else if is_node_instance(vm, &object, pyast::NodeOperatorDiv::static_type())? { + Self::Div + } else if is_node_instance(vm, &object, pyast::NodeOperatorMod::static_type())? { + Self::Mod + } else if is_node_instance(vm, &object, pyast::NodeOperatorPow::static_type())? { + Self::Pow + } else if is_node_instance(vm, &object, pyast::NodeOperatorLShift::static_type())? { + Self::LShift + } else if is_node_instance(vm, &object, pyast::NodeOperatorRShift::static_type())? { + Self::RShift + } else if is_node_instance(vm, &object, pyast::NodeOperatorBitOr::static_type())? { + Self::BitOr + } else if is_node_instance(vm, &object, pyast::NodeOperatorBitXor::static_type())? { + Self::BitXor + } else if is_node_instance(vm, &object, pyast::NodeOperatorBitAnd::static_type())? { + Self::BitAnd + } else if is_node_instance(vm, &object, pyast::NodeOperatorFloorDiv::static_type())? { + Self::FloorDiv + } else { + return Err(vm.new_type_error(format!( + "expected some sort of operator, but got {}", + object.repr(vm)? + ))); + }, + ) } } @@ -109,21 +111,22 @@ impl Node for ast::UnaryOp { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeUnaryOpInvert::static_type()) { - Self::Invert - } else if cls.is(pyast::NodeUnaryOpNot::static_type()) { - Self::Not - } else if cls.is(pyast::NodeUnaryOpUAdd::static_type()) { - Self::UAdd - } else if cls.is(pyast::NodeUnaryOpUSub::static_type()) { - Self::USub - } else { - return Err(vm.new_type_error(format!( - "expected some sort of unaryop, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if is_node_instance(vm, &object, pyast::NodeUnaryOpInvert::static_type())? { + Self::Invert + } else if is_node_instance(vm, &object, pyast::NodeUnaryOpNot::static_type())? { + Self::Not + } else if is_node_instance(vm, &object, pyast::NodeUnaryOpUAdd::static_type())? { + Self::UAdd + } else if is_node_instance(vm, &object, pyast::NodeUnaryOpUSub::static_type())? { + Self::USub + } else { + return Err(vm.new_type_error(format!( + "expected some sort of unaryop, but got {}", + object.repr(vm)? + ))); + }, + ) } } @@ -150,32 +153,33 @@ impl Node for ast::CmpOp { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeCmpOpEq::static_type()) { - Self::Eq - } else if cls.is(pyast::NodeCmpOpNotEq::static_type()) { - Self::NotEq - } else if cls.is(pyast::NodeCmpOpLt::static_type()) { - Self::Lt - } else if cls.is(pyast::NodeCmpOpLtE::static_type()) { - Self::LtE - } else if cls.is(pyast::NodeCmpOpGt::static_type()) { - Self::Gt - } else if cls.is(pyast::NodeCmpOpGtE::static_type()) { - Self::GtE - } else if cls.is(pyast::NodeCmpOpIs::static_type()) { - Self::Is - } else if cls.is(pyast::NodeCmpOpIsNot::static_type()) { - Self::IsNot - } else if cls.is(pyast::NodeCmpOpIn::static_type()) { - Self::In - } else if cls.is(pyast::NodeCmpOpNotIn::static_type()) { - Self::NotIn - } else { - return Err(vm.new_type_error(format!( - "expected some sort of cmpop, but got {}", - object.repr(vm)? - ))); - }) + Ok( + if is_node_instance(vm, &object, pyast::NodeCmpOpEq::static_type())? { + Self::Eq + } else if is_node_instance(vm, &object, pyast::NodeCmpOpNotEq::static_type())? { + Self::NotEq + } else if is_node_instance(vm, &object, pyast::NodeCmpOpLt::static_type())? { + Self::Lt + } else if is_node_instance(vm, &object, pyast::NodeCmpOpLtE::static_type())? { + Self::LtE + } else if is_node_instance(vm, &object, pyast::NodeCmpOpGt::static_type())? { + Self::Gt + } else if is_node_instance(vm, &object, pyast::NodeCmpOpGtE::static_type())? { + Self::GtE + } else if is_node_instance(vm, &object, pyast::NodeCmpOpIs::static_type())? { + Self::Is + } else if is_node_instance(vm, &object, pyast::NodeCmpOpIsNot::static_type())? { + Self::IsNot + } else if is_node_instance(vm, &object, pyast::NodeCmpOpIn::static_type())? { + Self::In + } else if is_node_instance(vm, &object, pyast::NodeCmpOpNotIn::static_type())? { + Self::NotIn + } else { + return Err(vm.new_type_error(format!( + "expected some sort of cmpop, but got {}", + object.repr(vm)? + ))); + }, + ) } } diff --git a/crates/vm/src/stdlib/_ast/other.rs b/crates/vm/src/stdlib/_ast/other.rs index 5009c588cfc..837c7d12094 100644 --- a/crates/vm/src/stdlib/_ast/other.rs +++ b/crates/vm/src/stdlib/_ast/other.rs @@ -11,14 +11,12 @@ impl Node for ast::ConversionFlag { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - // Python's AST uses ASCII codes: 's', 'r', 'a', -1=None - // Note: 255 is -1i8 as u8 (ruff's ConversionFlag::None) - match i32::try_from_object(vm, object)? { - -1 | 255 => Ok(Self::None), + match node_object_to_i32(vm, object)? { + -1 => Ok(Self::None), x if x == b's' as i32 => Ok(Self::Str), x if x == b'r' as i32 => Ok(Self::Repr), x if x == b'a' as i32 => Ok(Self::Ascii), - _ => Err(vm.new_value_error("invalid conversion flag")), + x => Err(vm.new_system_error(format!("Unrecognized conversion character {x}"))), } } } @@ -34,10 +32,13 @@ impl Node for ast::name::Name { _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - match object.downcast::() { - Ok(name) => Ok(Self::new(name)), - Err(_) => Err(vm.new_value_error("expected str for name")), + if !object.class().is(vm.ctx.types.str_type) { + return Err(vm.new_type_error("AST identifier must be of type str")); } + object + .downcast::() + .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 { Ok(Self { node_index: Default::default(), - name: Node::ast_from_object( - vm, - source_file, - get_node_field_required(vm, &object, "name", "alias")?, - )?, + name: get_required_identifier_field(vm, source_file, &object, "name", "alias")?, asname: get_node_field_opt(vm, &object, "asname")? .map(|obj| Node::ast_from_object(vm, source_file, obj)) .transpose()?, @@ -137,10 +134,12 @@ impl Node for ast::WithItem { ) -> PyResult { Ok(Self { node_index: Default::default(), - context_expr: Node::ast_from_object( + context_expr: get_required_node_field( vm, source_file, - get_node_field_required(vm, &object, "context_expr", "withitem")?, + &object, + "context_expr", + "withitem", )?, optional_vars: get_node_field_opt(vm, &object, "optional_vars")? .map(|obj| Node::ast_from_object(vm, source_file, obj)) diff --git a/crates/vm/src/stdlib/_ast/parameter.rs b/crates/vm/src/stdlib/_ast/parameter.rs index b0c807a2922..1ce4973b494 100644 --- a/crates/vm/src/stdlib/_ast/parameter.rs +++ b/crates/vm/src/stdlib/_ast/parameter.rs @@ -11,10 +11,12 @@ impl Node for ast::Parameters { vararg, kwonlyargs, kwarg, - range, + range: _, + runtime_defaults, } = self; - let (posonlyargs, args, defaults) = + let (posonlyargs, args, mut defaults) = extract_positional_parameter_defaults(posonlyargs, args); + defaults.runtime_defaults = runtime_defaults; let (kwonlyargs, kw_defaults) = extract_keyword_parameter_defaults(kwonlyargs); let node = NodeAst .into_ref_with_type(vm, pyast::NodeArguments::static_type().to_owned()) @@ -40,9 +42,12 @@ impl Node for ast::Parameters { .unwrap(); dict.set_item("kwarg", kwarg.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("defaults", defaults.ast_to_object(vm, source_file), vm) - .unwrap(); - let _ = range; + let runtime_defaults = defaults.runtime_defaults.take(); + let defaults = runtime_defaults.map_or_else( + || defaults.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("defaults", defaults, vm).unwrap(); node.into() } @@ -51,48 +56,65 @@ impl Node for ast::Parameters { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let kwonlyargs = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "kwonlyargs", "arguments")?, - )?; - let kw_defaults = Node::ast_from_object( + let posonlyargs = PositionalParameters::ast_from_field( vm, source_file, - get_node_field(vm, &object, "kw_defaults", "arguments")?, + &object, + "posonlyargs", + "arguments", )?; - let kwonlyargs = merge_keyword_parameter_defaults(vm, kwonlyargs, kw_defaults)?; - - let posonlyargs = Node::ast_from_object( + let args = + PositionalParameters::ast_from_field(vm, source_file, &object, "args", "arguments")?; + let vararg = get_node_field_opt(vm, &object, "vararg")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?; + let kwonlyargs = + KeywordParameters::ast_from_field(vm, source_file, &object, "kwonlyargs", "arguments")?; + let kw_defaults = ParameterDefaults::ast_from_field( vm, source_file, - get_node_field(vm, &object, "posonlyargs", "arguments")?, + &object, + "kw_defaults", + "arguments", )?; - let args = Node::ast_from_object( + 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, - get_node_field(vm, &object, "args", "arguments")?, + &object, + "defaults", + "arguments", )?; - let defaults = Node::ast_from_object( + + validate_parameter_annotations( vm, - source_file, - get_node_field(vm, &object, "defaults", "arguments")?, + &posonlyargs, + &args, + vararg.as_deref(), + &kwonlyargs, + kwarg.as_deref(), )?; + + let ParameterDefaults { + runtime_defaults, + defaults, + _range: _, + } = defaults; let (posonlyargs, args) = merge_positional_parameter_defaults(vm, posonlyargs, args, defaults)?; + let kwonlyargs = merge_keyword_parameter_defaults(vm, kwonlyargs, kw_defaults)?; Ok(Self { node_index: Default::default(), posonlyargs, args, - vararg: get_node_field_opt(vm, &object, "vararg")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, + vararg, kwonlyargs, - kwarg: get_node_field_opt(vm, &object, "kwarg")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, + kwarg, range: Default::default(), + runtime_defaults, }) } @@ -101,64 +123,93 @@ impl Node for ast::Parameters { } } +fn validate_parameter_annotations( + vm: &VirtualMachine, + posonlyargs: &PositionalParameters, + args: &PositionalParameters, + vararg: Option<&ast::Parameter>, + kwonlyargs: &KeywordParameters, + kwarg: Option<&ast::Parameter>, +) -> PyResult<()> { + for parameter in posonlyargs.args.iter().chain(&args.args) { + super::validate::validate_parameter_annotation(vm, parameter)?; + } + if let Some(parameter) = vararg { + super::validate::validate_parameter_annotation(vm, parameter)?; + } + for parameter in &kwonlyargs.keywords { + super::validate::validate_parameter_annotation(vm, parameter)?; + } + if let Some(parameter) = kwarg { + super::validate::validate_parameter_annotation(vm, parameter)?; + } + Ok(()) +} + // product impl Node for ast::Parameter { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, name, annotation, // type_comment, range, + runtime_type_comment, + runtime_type_comment_bytes, } = self; // ruff covers the ** in range but python expects it to start at the ident let range = TextRange::new(name.start(), range.end()); let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeArg::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeArg::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("arg", name.ast_to_object(_vm, source_file), _vm) + dict.set_item("arg", name.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item( - "annotation", - annotation.ast_to_object(_vm, source_file), - _vm, + dict.set_item("annotation", annotation.ast_to_object(vm, source_file), vm) + .unwrap(); + let type_comment = super::constant::runtime_string_object( + vm, + runtime_type_comment, + runtime_type_comment_bytes, ) - .unwrap(); - // Ruff AST doesn't track type_comment, so always set to None - dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap(); - node_add_location(&dict, range, _vm, source_file); + .unwrap_or_else(|| vm.ctx.none()); + dict.set_item("type_comment", type_comment, vm).unwrap(); + node_add_location(&dict, range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { + let name = get_required_identifier_field(vm, source_file, &_object, "arg", "arg")?; + let annotation = get_node_field_opt(vm, &_object, "annotation")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?; + let type_comment = get_ast_string_field_opt(vm, &_object, "type_comment")?; + let (runtime_type_comment, runtime_type_comment_bytes) = type_comment + .map_or((None, None), |type_comment| { + super::constant::runtime_string_from_pyobject(vm, type_comment) + }); + let range = range_from_object(vm, source_file, _object, "arg")?; Ok(Self { node_index: Default::default(), - name: Node::ast_from_object( - _vm, - source_file, - get_node_field_required(_vm, &_object, "arg", "arg")?, - )?, - annotation: get_node_field_opt(_vm, &_object, "annotation")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - // type_comment: get_node_field_opt(_vm, &_object, "type_comment")? - // .map(|obj| Node::ast_from_object(_vm, obj)) - // .transpose()?, - range: range_from_object(_vm, source_file, _object, "arg")?, + name, + annotation, + range, + runtime_type_comment, + runtime_type_comment_bytes, }) } } // product impl Node for ast::Keyword { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, arg, @@ -166,32 +217,28 @@ impl Node for ast::Keyword { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeKeyword::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeKeyword::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("arg", arg.ast_to_object(_vm, source_file), _vm) + dict.set_item("arg", arg.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("value", value.ast_to_object(_vm, source_file), _vm) + dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { Ok(Self { node_index: Default::default(), - arg: get_node_field_opt(_vm, &_object, "arg")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) + arg: get_node_field_opt(vm, &_object, "arg")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) .transpose()?, - value: Node::ast_from_object( - _vm, - source_file, - get_node_field_required(_vm, &_object, "value", "keyword")?, - )?, - range: range_from_object(_vm, source_file, _object, "keyword")?, + value: get_required_node_field(vm, source_file, &_object, "value", "keyword")?, + range: range_from_object(vm, source_file, _object, "keyword")?, }) } } @@ -201,6 +248,21 @@ struct PositionalParameters { pub args: Box<[ast::Parameter]>, } +impl PositionalParameters { + fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + Ok(Self { + args: get_node_boxed_slice_field(vm, source_file, object, field, typ)?, + _range: TextRange::default(), + }) + } +} + impl Node for PositionalParameters { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { BoxedSlice(self.args).ast_to_object(vm, source_file) @@ -214,7 +276,7 @@ impl Node for PositionalParameters { let args: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; Ok(Self { args: args.0, - _range: TextRange::default(), // TODO + _range: TextRange::default(), }) } } @@ -224,6 +286,21 @@ struct KeywordParameters { pub keywords: Box<[ast::Parameter]>, } +impl KeywordParameters { + fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + Ok(Self { + keywords: get_node_boxed_slice_field(vm, source_file, object, field, typ)?, + _range: TextRange::default(), + }) + } +} + impl Node for KeywordParameters { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { BoxedSlice(self.keywords).ast_to_object(vm, source_file) @@ -237,16 +314,55 @@ impl Node for KeywordParameters { let keywords: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; Ok(Self { keywords: keywords.0, - _range: TextRange::default(), // TODO + _range: TextRange::default(), }) } } struct ParameterDefaults { pub _range: TextRange, // TODO: Use this + runtime_defaults: Option>>, defaults: Box<[Option>]>, } +impl ParameterDefaults { + fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + 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 { + let defaults: Vec>> = + get_node_list_field(vm, source_file, object, field, typ)?; + let runtime_defaults = defaults.iter().any(Option::is_none).then(|| { + defaults + .iter() + .map(|default| default.as_deref().cloned()) + .collect() + }); + Ok(Self { + defaults: defaults.into_boxed_slice(), + runtime_defaults, + _range: TextRange::default(), + }) + } +} + impl Node for ParameterDefaults { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { BoxedSlice(self.defaults).ast_to_object(vm, source_file) @@ -260,7 +376,8 @@ impl Node for ParameterDefaults { let defaults: BoxedSlice<_> = Node::ast_from_object(vm, source_file, object)?; Ok(Self { defaults: defaults.0, - _range: TextRange::default(), // TODO + runtime_defaults: None, + _range: TextRange::default(), }) } } @@ -287,6 +404,7 @@ fn extract_positional_parameter_defaults( .map(|item| item.range()) .reduce(|acc, next| acc.cover(next)) .unwrap_or_default(), + runtime_defaults: None, defaults: defaults.into_boxed_slice(), }; @@ -325,14 +443,13 @@ fn merge_positional_parameter_defaults( vm: &VirtualMachine, posonlyargs: PositionalParameters, args: PositionalParameters, - defaults: ParameterDefaults, + defaults: Box<[Option>]>, ) -> PyResult<( Vec, Vec, )> { let posonlyargs = posonlyargs.args; let args = args.args; - let defaults = defaults.defaults; let mut posonlyargs: Vec<_> = as IntoIterator>::into_iter(posonlyargs) .map(|parameter| ast::ParameterWithDefault { @@ -383,6 +500,7 @@ fn extract_keyword_parameter_defaults( .map(|item| item.range()) .reduce(|acc, next| acc.cover(next)) .unwrap_or_default(), + runtime_defaults: None, defaults: defaults.into_boxed_slice(), }; @@ -422,5 +540,5 @@ fn merge_keyword_parameter_defaults( default, range: Default::default(), }) - .collect()) + .collect::>()) } diff --git a/crates/vm/src/stdlib/_ast/pattern.rs b/crates/vm/src/stdlib/_ast/pattern.rs index a383c25a6b7..387fb59004c 100644 --- a/crates/vm/src/stdlib/_ast/pattern.rs +++ b/crates/vm/src/stdlib/_ast/pattern.rs @@ -10,6 +10,7 @@ impl Node for ast::MatchCase { guard, body, range: _, + runtime_body, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeMatchCase::static_type().to_owned()) @@ -19,8 +20,11 @@ impl Node for ast::MatchCase { .unwrap(); dict.set_item("guard", guard.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); node.into() } @@ -29,22 +33,18 @@ impl Node for ast::MatchCase { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { + let body: Vec> = + get_node_list_field(vm, source_file, &object, "body", "match_case")?; + let (runtime_body, body) = runtime_stmt_list_from_values(body); Ok(Self { node_index: Default::default(), - pattern: Node::ast_from_object( - vm, - source_file, - get_node_field_required(vm, &object, "pattern", "match_case")?, - )?, + pattern: get_required_node_field(vm, source_file, &object, "pattern", "match_case")?, guard: get_node_field_opt(vm, &object, "guard")? .map(|obj| Node::ast_from_object(vm, source_file, obj)) .transpose()?, - body: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "body", "match_case")?, - )?, + body, range: Default::default(), + runtime_body, }) } } @@ -68,64 +68,152 @@ impl Node for ast::Pattern { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodePatternMatchValue::static_type()) { - Self::MatchValue(ast::PatternMatchValue::ast_from_object( - vm, - source_file, - object, - )?) - } else if cls.is(pyast::NodePatternMatchSingleton::static_type()) { - Self::MatchSingleton(ast::PatternMatchSingleton::ast_from_object( - vm, - source_file, - object, - )?) - } else if cls.is(pyast::NodePatternMatchSequence::static_type()) { - Self::MatchSequence(ast::PatternMatchSequence::ast_from_object( - vm, - source_file, - object, - )?) - } else if cls.is(pyast::NodePatternMatchMapping::static_type()) { - Self::MatchMapping(ast::PatternMatchMapping::ast_from_object( + if vm.is_none(&object) { + return Err(vm.new_type_error(format!( + "expected some sort of pattern, but got {}", + object.repr(vm)? + ))); + } + enum PatternKind { + Value, + Singleton, + Sequence, + Mapping, + Class, + Star, + As, + Or, + } + let kind = if is_node_instance(vm, &object, pyast::NodePatternMatchValue::static_type())? { + PatternKind::Value + } else if is_node_instance(vm, &object, pyast::NodePatternMatchSingleton::static_type())? { + PatternKind::Singleton + } else if is_node_instance(vm, &object, pyast::NodePatternMatchSequence::static_type())? { + PatternKind::Sequence + } else if is_node_instance(vm, &object, pyast::NodePatternMatchMapping::static_type())? { + PatternKind::Mapping + } else if is_node_instance(vm, &object, pyast::NodePatternMatchClass::static_type())? { + PatternKind::Class + } else if is_node_instance(vm, &object, pyast::NodePatternMatchStar::static_type())? { + PatternKind::Star + } else if is_node_instance(vm, &object, pyast::NodePatternMatchAs::static_type())? { + PatternKind::As + } else if is_node_instance(vm, &object, pyast::NodePatternMatchOr::static_type())? { + PatternKind::Or + } else { + return Err(vm.new_type_error(format!( + "expected some sort of pattern, but got {}", + object.repr(vm)? + ))); + }; + let range = pattern_range_from_object(vm, source_file, object.clone())?; + Ok(match kind { + PatternKind::Value => Self::MatchValue(pattern_match_value_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodePatternMatchClass::static_type()) { - Self::MatchClass(ast::PatternMatchClass::ast_from_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, - )?) - } else if cls.is(pyast::NodePatternMatchStar::static_type()) { - Self::MatchStar(ast::PatternMatchStar::ast_from_object( + range, + )?), + PatternKind::Star => Self::MatchStar(pattern_match_star_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodePatternMatchAs::static_type()) { - Self::MatchAs(ast::PatternMatchAs::ast_from_object( + range, + )?), + PatternKind::As => Self::MatchAs(pattern_match_as_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodePatternMatchOr::static_type()) { - Self::MatchOr(ast::PatternMatchOr::ast_from_object( + range, + )?), + PatternKind::Or => Self::MatchOr(pattern_match_or_from_object_with_range( vm, source_file, object, - )?) - } else { - return Err(vm.new_type_error(format!( - "expected some sort of pattern, but got {}", - object.repr(vm)? - ))); + 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], range: TextRange) -> Vec { + 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], range: TextRange) -> Vec { + values + .iter() + .cloned() + .map(|value| value.unwrap_or_else(|| null_expr_placeholder(range))) + .collect() +} + +type RuntimePatternList = Option>>; +type PatternListField = (RuntimePatternList, Vec); + +fn pattern_list_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + range: TextRange, +) -> PyResult { + let values: Vec> = + get_node_list_field(vm, source_file, object, field, typ)?; + let runtime_patterns = values.iter().any(Option::is_none).then(|| values.clone()); + Ok((runtime_patterns, lower_nullable_patterns(&values, range))) +} + // constructor +fn pattern_match_value_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::PatternMatchValue { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "MatchValue")?, + range, + }) +} + impl Node for ast::PatternMatchValue { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -148,19 +236,29 @@ impl Node for ast::PatternMatchValue { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "MatchValue")?, - )?, - range: range_from_object(vm, source_file, object, "MatchValue")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchValue")?; + pattern_match_value_from_object_with_range(vm, source_file, object, range) } } // constructor +fn pattern_match_singleton_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::PatternMatchSingleton { + node_index: Default::default(), + value: Node::ast_from_object( + vm, + source_file, + get_node_field(vm, &object, "value", "MatchSingleton")?, + )?, + range, + }) +} + impl Node for ast::PatternMatchSingleton { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -186,15 +284,8 @@ impl Node for ast::PatternMatchSingleton { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "MatchSingleton")?, - )?, - range: range_from_object(vm, source_file, object, "MatchSingleton")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchSingleton")?; + pattern_match_singleton_from_object_with_range(vm, source_file, object, range) } } @@ -219,21 +310,35 @@ impl Node for ast::Singleton { } else if object.is(&vm.ctx.false_value) { Ok(Self::False) } else { - Err(vm.new_value_error(format!( - "Expected None, True, or False, got {:?}", - object.class().name() - ))) + Err(vm.new_value_error("MatchSingleton can only contain True, False and None")) } } } // constructor +fn pattern_match_sequence_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let (runtime_patterns, patterns) = + pattern_list_from_field(vm, source_file, &object, "patterns", "MatchSequence", range)?; + Ok(ast::PatternMatchSequence { + node_index: Default::default(), + patterns: patterns.to_vec(), + range, + runtime_patterns, + }) +} + impl Node for ast::PatternMatchSequence { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, patterns, range, + runtime_patterns, } = self; let node = NodeAst .into_ref_with_type( @@ -242,8 +347,11 @@ impl Node for ast::PatternMatchSequence { ) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) - .unwrap(); + let patterns = runtime_patterns.map_or_else( + || patterns.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("patterns", patterns, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -253,19 +361,40 @@ impl Node for ast::PatternMatchSequence { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - patterns: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "patterns", "MatchSequence")?, - )?, - range: range_from_object(vm, source_file, object, "MatchSequence")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchSequence")?; + pattern_match_sequence_from_object_with_range(vm, source_file, object, range) } } // constructor +fn pattern_match_mapping_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let keys: Vec> = + get_node_list_field(vm, source_file, &object, "keys", "MatchMapping")?; + let patterns: Vec> = + get_node_list_field(vm, source_file, &object, "patterns", "MatchMapping")?; + let runtime_keys = keys.iter().any(Option::is_none).then(|| keys.clone()); + let runtime_patterns = patterns + .iter() + .any(Option::is_none) + .then(|| patterns.clone()); + Ok(ast::PatternMatchMapping { + node_index: Default::default(), + keys: lower_nullable_exprs(&keys, range), + patterns: lower_nullable_patterns(&patterns, range), + rest: get_node_field_opt(vm, &object, "rest")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + runtime_keys, + runtime_patterns, + }) +} + impl Node for ast::PatternMatchMapping { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -274,15 +403,23 @@ impl Node for ast::PatternMatchMapping { patterns, rest, range, + runtime_keys, + runtime_patterns, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodePatternMatchMapping::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("keys", keys.ast_to_object(vm, source_file), vm) - .unwrap(); - dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) - .unwrap(); + let keys = runtime_keys.map_or_else( + || keys.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("keys", keys, vm).unwrap(); + let patterns = runtime_patterns.map_or_else( + || patterns.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("patterns", patterns, vm).unwrap(); dict.set_item("rest", rest.ast_to_object(vm, source_file), vm) .unwrap(); node_add_location(&dict, range, vm, source_file); @@ -294,27 +431,57 @@ impl Node for ast::PatternMatchMapping { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - keys: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "keys", "MatchMapping")?, - )?, - patterns: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "patterns", "MatchMapping")?, - )?, - rest: get_node_field_opt(vm, &object, "rest")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "MatchMapping")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchMapping")?; + pattern_match_mapping_from_object_with_range(vm, source_file, object, range) } } // constructor +fn pattern_match_class_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let cls = get_required_node_field(vm, source_file, &object, "cls", "MatchClass")?; + let patterns: Vec> = + 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> = + get_node_list_field(vm, source_file, &object, "kwd_patterns", "MatchClass")?; + let has_runtime_shape = kwd_attrs.0.len() != kwd_patterns.len() + || patterns.iter().any(Option::is_none) + || kwd_patterns.iter().any(Option::is_none); + let runtime_patterns = has_runtime_shape.then(|| patterns.clone()); + let runtime_kwd_attrs = has_runtime_shape.then(|| kwd_attrs.0.clone()); + let runtime_kwd_patterns = has_runtime_shape.then(|| kwd_patterns.clone()); + let patterns = PatternMatchClassPatterns(lower_nullable_patterns(&patterns, range)); + let kwd_patterns = + PatternMatchClassKeywordPatterns(lower_nullable_patterns(&kwd_patterns, range)); + let (patterns, keywords) = merge_pattern_match_class(patterns, kwd_attrs, kwd_patterns); + + Ok(ast::PatternMatchClass { + node_index: Default::default(), + cls, + range, + arguments: ast::PatternArguments { + node_index: Default::default(), + range: Default::default(), + patterns, + keywords, + }, + runtime_patterns, + runtime_kwd_attrs, + runtime_kwd_patterns, + }) +} + impl Node for ast::PatternMatchClass { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -322,24 +489,36 @@ impl Node for ast::PatternMatchClass { cls, arguments, range, + runtime_patterns, + runtime_kwd_attrs, + runtime_kwd_patterns, } = self; - let (patterns, kwd_attrs, kwd_patterns) = split_pattern_match_class(arguments); let node = NodeAst .into_ref_with_type(vm, pyast::NodePatternMatchClass::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); dict.set_item("cls", cls.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) - .unwrap(); - dict.set_item("kwd_attrs", kwd_attrs.ast_to_object(vm, source_file), vm) - .unwrap(); - dict.set_item( - "kwd_patterns", - kwd_patterns.ast_to_object(vm, source_file), - vm, - ) - .unwrap(); + let (patterns, kwd_attrs, kwd_patterns) = + if let (Some(patterns), Some(kwd_attrs), Some(kwd_patterns)) = + (runtime_patterns, runtime_kwd_attrs, runtime_kwd_patterns) + { + ( + patterns.ast_to_object(vm, source_file), + kwd_attrs.ast_to_object(vm, source_file), + kwd_patterns.ast_to_object(vm, source_file), + ) + } else { + let (patterns, kwd_attrs, kwd_patterns) = split_pattern_match_class(arguments); + ( + patterns.ast_to_object(vm, source_file), + kwd_attrs.ast_to_object(vm, source_file), + kwd_patterns.ast_to_object(vm, source_file), + ) + }; + dict.set_item("patterns", patterns, vm).unwrap(); + dict.set_item("kwd_attrs", kwd_attrs, vm).unwrap(); + dict.set_item("kwd_patterns", kwd_patterns, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -349,41 +528,8 @@ impl Node for ast::PatternMatchClass { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let patterns = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "patterns", "MatchClass")?, - )?; - let kwd_attrs: PatternMatchClassKeywordAttributes = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "kwd_attrs", "MatchClass")?, - )?; - let kwd_patterns: PatternMatchClassKeywordPatterns = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "kwd_patterns", "MatchClass")?, - )?; - if kwd_attrs.0.len() != kwd_patterns.0.len() { - return Err(vm.new_value_error("MatchClass has mismatched kwd_attrs and kwd_patterns")); - } - let (patterns, keywords) = merge_pattern_match_class(patterns, kwd_attrs, kwd_patterns); - - Ok(Self { - node_index: Default::default(), - cls: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "cls", "MatchClass")?, - )?, - range: range_from_object(vm, source_file, object, "MatchClass")?, - arguments: ast::PatternArguments { - node_index: Default::default(), - range: Default::default(), - patterns, - keywords, - }, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchClass")?; + pattern_match_class_from_object_with_range(vm, source_file, object, range) } } @@ -405,6 +551,24 @@ impl Node for PatternMatchClassPatterns { struct PatternMatchClassKeywordAttributes(Vec); +impl PatternMatchClassKeywordAttributes { + fn ast_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, + ) -> PyResult { + Ok(Self(get_node_list_field( + vm, + source_file, + object, + field, + typ, + )?)) + } +} + impl Node for PatternMatchClassKeywordAttributes { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { self.0.ast_to_object(vm, source_file) @@ -435,6 +599,21 @@ impl Node for PatternMatchClassKeywordPatterns { } } // constructor +fn pattern_match_star_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::PatternMatchStar { + node_index: Default::default(), + name: get_node_field_opt(vm, &object, "name")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::PatternMatchStar { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -457,17 +636,30 @@ impl Node for ast::PatternMatchStar { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - name: get_node_field_opt(vm, &object, "name")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "MatchStar")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchStar")?; + pattern_match_star_from_object_with_range(vm, source_file, object, range) } } // constructor +fn pattern_match_as_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::PatternMatchAs { + node_index: Default::default(), + pattern: get_node_field_opt(vm, &object, "pattern")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + name: get_node_field_opt(vm, &object, "name")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::PatternMatchAs { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -493,33 +685,45 @@ impl Node for ast::PatternMatchAs { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - pattern: get_node_field_opt(vm, &object, "pattern")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - name: get_node_field_opt(vm, &object, "name")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "MatchAs")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchAs")?; + pattern_match_as_from_object_with_range(vm, source_file, object, range) } } // constructor +fn pattern_match_or_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let (runtime_patterns, patterns) = + pattern_list_from_field(vm, source_file, &object, "patterns", "MatchOr", range)?; + Ok(ast::PatternMatchOr { + node_index: Default::default(), + patterns: patterns.to_vec(), + range, + runtime_patterns, + }) +} + impl Node for ast::PatternMatchOr { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, patterns, range, + runtime_patterns, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodePatternMatchOr::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("patterns", patterns.ast_to_object(vm, source_file), vm) - .unwrap(); + let patterns = runtime_patterns.map_or_else( + || patterns.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("patterns", patterns, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -528,15 +732,8 @@ impl Node for ast::PatternMatchOr { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - patterns: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "patterns", "MatchOr")?, - )?, - range: range_from_object(vm, source_file, object, "MatchOr")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "MatchOr")?; + pattern_match_or_from_object_with_range(vm, source_file, object, range) } } diff --git a/crates/vm/src/stdlib/_ast/pyast.rs b/crates/vm/src/stdlib/_ast/pyast.rs index 7eae6f00986..ebce1a788d2 100644 --- a/crates/vm/src/stdlib/_ast/pyast.rs +++ b/crates/vm/src/stdlib/_ast/pyast.rs @@ -3,7 +3,6 @@ use crate::builtins::{PyGenericAlias, PyTuple, PyTupleRef, PyTypeRef, make_union use crate::common::ascii; use crate::convert::ToPyObject; use crate::function::FuncArgs; -use crate::types::Initializer; macro_rules! impl_node { ( @@ -61,6 +60,12 @@ macro_rules! impl_node { macro_rules! impl_base_node { // Base node without fields/attributes (e.g. NodeMod, NodeExpr) ($name:ident) => { + impl_base_node!($name, attributes: []); + }; + ($name:ident, attributes: [$($attr:expr),* $(,)?]) => { + impl_base_node!($name, attributes: [$($attr),*], optional_end_location: false); + }; + ($name:ident, attributes: [$($attr:expr),* $(,)?], optional_end_location: $optional_end_location:expr) => { #[pyclass(flags(HAS_DICT, BASETYPE))] impl $name { #[pymethod] @@ -83,9 +88,24 @@ macro_rules! impl_base_node { (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); } class.set_attr( - identifier!(ctx, _attributes), + identifier!(ctx, _fields), ctx.empty_tuple.clone().into(), ); + class.set_str_attr("__match_args__", ctx.empty_tuple.clone(), ctx); + class.set_attr( + identifier!(ctx, _attributes), + ctx.new_tuple(vec![ + $( + ctx.new_str(ascii!($attr)).into() + ),* + ]) + .into(), + ); + if $optional_end_location { + let none = ctx.none(); + class.set_str_attr("end_lineno", none.clone(), ctx); + class.set_str_attr("end_col_offset", none, ctx); + } } } }; @@ -179,7 +199,11 @@ impl_node!( #[repr(transparent)] pub(crate) struct NodeStmt(NodeAst); -impl_base_node!(NodeStmt); +impl_base_node!( + NodeStmt, + attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], + optional_end_location: true +); impl_node!( #[pyclass(module = "_ast", name = "FunctionType", base = NodeMod)] @@ -378,7 +402,11 @@ impl_node!( #[repr(transparent)] pub(crate) struct NodeExpr(NodeAst); -impl_base_node!(NodeExpr); +impl_base_node!( + NodeExpr, + attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], + optional_end_location: true +); impl_node!( #[pyclass(module = "_ast", name = "Continue", base = NodeStmt)] @@ -533,12 +561,11 @@ impl_node!( attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -// NodeExprConstant needs custom Initializer to default kind to None #[pyclass(module = "_ast", name = "Constant", base = NodeExpr)] #[repr(transparent)] pub(crate) struct NodeExprConstant(NodeExpr); -#[pyclass(flags(HAS_DICT, BASETYPE), with(Initializer))] +#[pyclass(flags(HAS_DICT, BASETYPE))] impl NodeExprConstant { #[extend_class] fn extend_class_with_fields(ctx: &Context, class: &'static Py) { @@ -580,24 +607,6 @@ impl NodeExprConstant { } } -impl Initializer for NodeExprConstant { - type Args = FuncArgs; - - fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { - ::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, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { - unreachable!("slot_init is defined") - } -} - impl_node!( #[pyclass(module = "_ast", name = "Attribute", base = NodeExpr)] pub(crate) struct NodeExprAttribute, @@ -841,7 +850,11 @@ impl_node!( #[repr(transparent)] pub(crate) struct NodeExceptHandler(NodeAst); -impl_base_node!(NodeExceptHandler); +impl_base_node!( + NodeExceptHandler, + attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], + optional_end_location: true +); impl_node!( #[pyclass(module = "_ast", name = "comprehension", base = NodeAst)] @@ -893,7 +906,10 @@ impl_node!( #[repr(transparent)] pub(crate) struct NodePattern(NodeAst); -impl_base_node!(NodePattern); +impl_base_node!( + NodePattern, + attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"] +); impl_node!( #[pyclass(module = "_ast", name = "match_case", base = NodeAst)] @@ -967,7 +983,10 @@ impl_node!( #[repr(transparent)] pub(crate) struct NodeTypeParam(NodeAst); -impl_base_node!(NodeTypeParam); +impl_base_node!( + NodeTypeParam, + attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"] +); impl_node!( #[pyclass(module = "_ast", name = "TypeIgnore", base = NodeTypeIgnore)] @@ -1681,7 +1700,6 @@ fn populate_field_types(vm: &VirtualMachine, module: &Py) { let field_types_attr = vm.ctx.intern_str("_field_types"); let annotations_attr = vm.ctx.intern_str("__annotations__"); - let empty_dict: PyObjectRef = vm.ctx.new_dict().into(); for &(class_name, fields) in FIELD_TYPES { if fields.is_empty() { @@ -1700,12 +1718,16 @@ fn populate_field_types(vm: &VirtualMachine, module: &Py) { FieldType::ListOf(name) => { let elem = resolve_node(name); let args = PyTuple::new_ref(vec![elem], &vm.ctx); - PyGenericAlias::new(list_type.clone(), args, false, vm).to_pyobject(vm) + PyGenericAlias::new(list_type.clone(), args, false, vm) + .expect("static field types are not nested, so no recursion is possible") + .to_pyobject(vm) } FieldType::ListOfBuiltin(name) => { let elem = resolve_builtin(name); let args = PyTuple::new_ref(vec![elem], &vm.ctx); - PyGenericAlias::new(list_type.clone(), args, false, vm).to_pyobject(vm) + PyGenericAlias::new(list_type.clone(), args, false, vm) + .expect("static field types are not nested, so no recursion is possible") + .to_pyobject(vm) } FieldType::Optional(name) => { let base = resolve_node(name); @@ -1752,36 +1774,6 @@ fn populate_field_types(vm: &VirtualMachine, module: &Py) { 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::() 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) { diff --git a/crates/vm/src/stdlib/_ast/python.rs b/crates/vm/src/stdlib/_ast/python.rs index ee5588acb87..b6f7948293d 100644 --- a/crates/vm/src/stdlib/_ast/python.rs +++ b/crates/vm/src/stdlib/_ast/python.rs @@ -7,18 +7,14 @@ use super::{ #[pymodule] pub(crate) mod _ast { use crate::{ - AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{ - PyDictRef, PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef, PyUtf8Str, PyUtf8StrRef, - }, + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyDictRef, PySet, PyStr, PyTupleRef, PyType, PyTypeRef}, class::{PyClassImpl, StaticType}, - common::wtf8::Wtf8, - function::{FuncArgs, KwArgs, PyMethodDef, PyMethodFlags}, + function::{ArgIterable, FuncArgs, KwArgs, PyMethodDef, PyMethodFlags}, stdlib::_ast::repr, types::{Constructor, Initializer}, warn, }; - use indexmap::IndexMap; #[pyattr] #[pyclass(module = "_ast", name = "AST")] #[derive(Debug, PyPayload)] @@ -117,10 +113,12 @@ pub(crate) mod _ast { let fields = cls.get_attr(vm.ctx.intern_str("_fields")); if let Some(fields) = fields { - let fields: Vec = fields.try_to_value(vm)?; + let fields = fields.sequence_unchecked(); + let numfields = fields.length(vm)?; let mut positional: Vec = Vec::new(); - for field in fields { - if dict.get_item_opt::(field.as_wtf8(), vm)?.is_some() { + for i in 0..numfields { + let field = fields.get_item(i as isize, vm)?; + if dict.get_item_opt(&*field, vm)?.is_some() { positional.push(vm.ctx.none()); } else { break; @@ -136,6 +134,85 @@ pub(crate) mod _ast { .new_tuple(vec![type_obj, vm.ctx.new_tuple(vec![]).into(), dict.into()])) } + fn ast_replace_update_payload( + payload: &PyDictRef, + keys: Option<&PyObjectRef>, + dict: &PyDictRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let Some(keys) = keys else { + return Ok(()); + }; + let keys = keys.sequence_unchecked(); + let num_keys = keys.length(vm)?; + for i in 0..num_keys { + let key = keys.get_item(i as isize, vm)?; + if let Some(value) = dict.get_item_opt(&*key, vm)? { + payload.set_item(&*key, value, vm)?; + } + } + Ok(()) + } + + fn ast_replace_set_update( + expecting: &PyRef, + iterable: Option<&PyObjectRef>, + vm: &VirtualMachine, + ) -> PyResult<()> { + let Some(iterable) = iterable else { + return Ok(()); + }; + let iterable = iterable.clone().try_into_value::(vm)?; + for item in iterable.iter(vm)? { + expecting.add(item?, vm)?; + } + Ok(()) + } + + fn ast_replace_set_discard( + expecting: &PyRef, + key: &PyObject, + vm: &VirtualMachine, + ) -> PyResult { + 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, + iterable: Option<&PyObjectRef>, + vm: &VirtualMachine, + ) -> PyResult<()> { + let Some(iterable) = iterable else { + return Ok(()); + }; + let iterable = iterable.clone().try_into_value::(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, + vm: &VirtualMachine, + ) -> PyResult<()> { + let name = name + .to_owned() + .downcast::() + .map_err(|_| vm.new_type_error("attribute name must be string"))?; + obj.set_attr(&name, value, vm) + } + pub(crate) fn ast_replace(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { if !args.args.is_empty() { return Err(vm.new_type_error("__replace__() takes no positional arguments")); @@ -146,22 +223,13 @@ pub(crate) mod _ast { let attributes = cls.get_attr(vm.ctx.intern_str("_attributes")); let dict = zelf.as_object().dict(); - let mut expecting: std::collections::HashSet = std::collections::HashSet::new(); - if let Some(fields) = fields.clone() { - let fields: Vec = fields.try_to_value(vm)?; - for field in fields { - expecting.insert(field.as_str().to_owned()); - } - } - if let Some(attributes) = attributes.clone() { - let attributes: Vec = attributes.try_to_value(vm)?; - for attr in attributes { - expecting.insert(attr.as_str().to_owned()); - } - } + let expecting = PySet::default().into_ref(&vm.ctx); + ast_replace_set_update(&expecting, fields.as_ref(), vm)?; + ast_replace_set_update(&expecting, attributes.as_ref(), vm)?; for (key, _value) in &args.kwargs { - if !expecting.remove(key) { + let key_obj: PyObjectRef = vm.ctx.new_str(key.as_ref()).into(); + if !ast_replace_set_discard(&expecting, &key_obj, vm)? { return Err(vm.new_type_error(format!( "{}.__replace__ got an unexpected keyword argument '{}'.", cls.name(), @@ -172,16 +240,9 @@ pub(crate) mod _ast { if let Some(dict) = dict.as_ref() { for (key, _value) in dict.items_vec() { - if let Ok(key) = key.downcast::() { - expecting.remove(key.as_str()); - } - } - if let Some(attributes) = attributes.clone() { - let attributes: Vec = attributes.try_to_value(vm)?; - for attr in attributes { - expecting.remove(attr.as_str()); - } + ast_replace_set_discard(&expecting, &key, vm)?; } + ast_replace_set_difference_update(&expecting, attributes.as_ref(), vm)?; } // Discard optional fields (T | None). @@ -189,20 +250,18 @@ pub(crate) mod _ast { && let Ok(field_types) = field_types.downcast::() { for (key, value) in field_types.items_vec() { - let Ok(key) = key.downcast::() else { - continue; - }; if value.fast_isinstance(vm.ctx.types.union_type) { - expecting.remove(key.as_str()); + ast_replace_set_discard(&expecting, &key, vm)?; } } } - if !expecting.is_empty() { - let mut names: Vec = expecting - .into_iter() - .map(|name| format!("{name:?}")) - .collect(); + let remaining = expecting.elements(); + if !remaining.is_empty() { + let mut names = Vec::with_capacity(remaining.len()); + for name in &remaining { + names.push(name.repr(vm)?.to_string()); + } names.sort(); let missing = names.join(", "); let count = names.len(); @@ -217,22 +276,8 @@ pub(crate) mod _ast { let payload = vm.ctx.new_dict(); if let Some(dict) = dict { - if let Some(fields) = fields { - let fields: Vec = fields.try_to_value(vm)?; - for field in fields { - if let Some(value) = dict.get_item_opt::(field.as_wtf8(), vm)? { - payload.set_item(field.as_object(), value, vm)?; - } - } - } - if let Some(attributes) = attributes { - let attributes: Vec = attributes.try_to_value(vm)?; - for attr in attributes { - if let Some(value) = dict.get_item_opt::(attr.as_wtf8(), vm)? { - payload.set_item(attr.as_object(), value, vm)?; - } - } - } + ast_replace_update_payload(&payload, fields.as_ref(), &dict, vm)?; + ast_replace_update_payload(&payload, attributes.as_ref(), &dict, vm)?; } for (key, value) in args.kwargs { payload.set_item(vm.ctx.intern_str(key), value, vm)?; @@ -244,11 +289,11 @@ pub(crate) mod _ast { .into_iter() .map(|(key, value)| { let key = key - .downcast::() + .downcast::() .map_err(|_| vm.new_type_error("keywords must be strings"))?; - Ok((key.as_str().to_owned(), value)) + Ok((key.as_wtf8().to_owned(), value)) }) - .collect::>>()?; + .collect::>>()?; let result = type_obj.call(FuncArgs::new(vec![], KwArgs::new(kwargs)), vm)?; Ok(result) } @@ -327,7 +372,7 @@ pub(crate) mod _ast { } fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { - unimplemented!("use slot_new") + unreachable!("NodeAst construction is handled by slot_new") } } @@ -350,52 +395,55 @@ pub(crate) mod _ast { zelf.class().name() )) })?; - let fields: Vec = fields.try_to_value(vm)?; + let fields_seq = fields.sequence_unchecked(); + let numfields = fields_seq.length(vm)?; + let remaining_fields = PySet::default().into_ref(&vm.ctx); + ast_replace_set_update(&remaining_fields, Some(&fields), vm)?; let n_args = args.args.len(); - if n_args > fields.len() { + if n_args > numfields { return Err(vm.new_type_error(format!( "{} constructor takes at most {} positional argument{}", zelf.class().name(), - fields.len(), - if fields.len() == 1 { "" } else { "s" }, + numfields, + if numfields == 1 { "" } else { "s" }, ))); } - // Track which fields were set - let mut set_fields = std::collections::HashSet::new(); - let mut attributes: Option> = None; + let mut attributes: Option = None; - for (name, arg) in fields.iter().zip(args.args) { - zelf.set_attr(name, arg, vm)?; - set_fields.insert(name.as_str().to_owned()); + for (i, arg) in args.args.into_iter().enumerate() { + let name = fields_seq.get_item(i as isize, vm)?; + ast_set_attr(&zelf, &name, arg, vm)?; + ast_replace_set_discard(&remaining_fields, &name, vm)?; } for (key, value) in args.kwargs { - if let Some(pos) = fields.iter().position(|f| f.as_bytes() == key.as_bytes()) - && pos < n_args - { - return Err(vm.new_type_error(format!( - "{} got multiple values for argument '{}'", - zelf.class().name(), - key - ))); - } - - if fields - .iter() - .all(|field| field.as_bytes() != key.as_bytes()) - { - let attrs = if let Some(attrs) = &attributes { - attrs + let key_obj: PyObjectRef = vm.ctx.new_str(key.as_ref()).into(); + let contains = fields_seq.contains(&key_obj, vm)?; + if contains { + if !ast_replace_set_discard(&remaining_fields, &key_obj, vm)? { + return Err(vm.new_type_error(format!( + "{} got multiple values for argument '{}'", + zelf.class().name(), + key + ))); + } + } else { + let attrs = if let Some(attributes) = &attributes { + attributes } else { let attrs = zelf .class() .get_attr(vm.ctx.intern_str("_attributes")) - .and_then(|attr| attr.try_to_value::>(vm).ok()) - .unwrap_or_default(); + .ok_or_else(|| { + vm.new_attribute_error(format!( + "type object '{}' has no attribute '_attributes'", + zelf.class().name() + )) + })?; attributes = Some(attrs); attributes.as_ref().unwrap() }; - if attrs.iter().all(|attr| attr.as_bytes() != key.as_bytes()) { + if !attrs.sequence_unchecked().contains(&key_obj, vm)? { let message = vm.ctx.new_str(format!( "{}.__init__ got an unexpected keyword argument '{}'. \ Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.", @@ -412,7 +460,6 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt } } - set_fields.insert(key.clone()); zelf.set_attr(vm.ctx.intern_str(key), value, vm)?; } @@ -425,17 +472,14 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt let expr_ctx_type: PyObjectRef = super::super::pyast::NodeExprContext::make_static_type().into(); - for field in &fields { - if set_fields.contains(field.as_str()) { - continue; - } - if let Some(ftype) = ft_dict.get_item_opt::(field.as_wtf8(), vm)? { + for field in remaining_fields.elements() { + if let Some(ftype) = ft_dict.get_item_opt(&*field, vm)? { if ftype.fast_isinstance(vm.ctx.types.union_type) { // Optional field (T | None) — no default } else if ftype.fast_isinstance(vm.ctx.types.generic_alias_type) { // List field (list[T]) — default to [] let empty_list: PyObjectRef = vm.ctx.new_list(vec![]).into(); - zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), empty_list, vm)?; + ast_set_attr(&zelf, &field, empty_list, vm)?; } else if ftype.is(&expr_ctx_type) { // expr_context — default to Load() let load_type = @@ -445,13 +489,15 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt .unwrap_or_else(|| { vm.ctx.new_base_object(load_type, Some(vm.ctx.new_dict())) }); - zelf.set_attr(vm.ctx.intern_str(field.as_wtf8()), load_instance, vm)?; + ast_set_attr(&zelf, &field, load_instance, vm)?; } else { // Required field missing: emit DeprecationWarning. + let field_repr = field.repr(vm)?; let message = vm.ctx.new_str(format!( - "{}.__init__ missing 1 required positional argument: '{}'", + "{}.__init__ missing 1 required positional argument: {}. \ +This will become an error in Python 3.15.", zelf.class().name(), - field.as_wtf8() + field_repr )); warn::warn( message.into(), @@ -461,6 +507,21 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt vm, )?; } + } else { + let field_repr = field.repr(vm)?; + let message = vm.ctx.new_str(format!( + "Field {} is missing from {}._field_types. \ +This will become an error in Python 3.15.", + field_repr, + zelf.class().name() + )); + warn::warn( + message.into(), + Some(vm.ctx.exceptions.deprecation_warning.to_owned()), + 1, + None, + vm, + )?; } } } @@ -510,9 +571,29 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt .map_err(|_| vm.new_type_error("AST is not a type"))?; let ctx = &vm.ctx; let empty_tuple = ctx.empty_tuple.clone(); + let set_empty_annotations = |typ: &Py| { + typ.set_str_attr("__annotations__", ctx.new_dict(), ctx); + }; + set_empty_annotations(&ast_type); ast_type.set_str_attr("_fields", empty_tuple.clone(), ctx); ast_type.set_str_attr("_attributes", empty_tuple.clone(), ctx); ast_type.set_str_attr("__match_args__", empty_tuple, ctx); + for typ in [ + super::super::pyast::NodeMod::static_type(), + super::super::pyast::NodeStmt::static_type(), + super::super::pyast::NodeExpr::static_type(), + super::super::pyast::NodeExprContext::static_type(), + super::super::pyast::NodeBoolOp::static_type(), + super::super::pyast::NodeOperator::static_type(), + super::super::pyast::NodeUnaryOp::static_type(), + super::super::pyast::NodeCmpOp::static_type(), + super::super::pyast::NodeExceptHandler::static_type(), + super::super::pyast::NodePattern::static_type(), + super::super::pyast::NodeTypeIgnore::static_type(), + super::super::pyast::NodeTypeParam::static_type(), + ] { + set_empty_annotations(typ); + } const AST_REDUCE: PyMethodDef = PyMethodDef::new_const( "__reduce__", diff --git a/crates/vm/src/stdlib/_ast/repr.rs b/crates/vm/src/stdlib/_ast/repr.rs index 2897447fbec..57f00c095e0 100644 --- a/crates/vm/src/stdlib/_ast/repr.rs +++ b/crates/vm/src/stdlib/_ast/repr.rs @@ -1,7 +1,8 @@ use crate::{ AsObject, PyObjectRef, PyResult, VirtualMachine, - builtins::{PyList, PyTuple}, + builtins::{PyList, PyStr, PyTuple}, class::PyClassImpl, + recursion::ReprGuard, stdlib::_ast::NodeAst, }; use rustpython_common::wtf8::Wtf8Buf; @@ -33,9 +34,7 @@ fn repr_ast_list(vm: &VirtualMachine, items: Vec, depth: usize) -> rendered.push_wtf8(&parts[0]); } if items.len() > 2 { - if !parts[0].is_empty() { - rendered.push_wtf8(", ...".as_ref()); - } + rendered.push_wtf8(", ...".as_ref()); if parts.len() > 1 { rendered.push_wtf8(", ".as_ref()); rendered.push_wtf8(&parts[1]); @@ -75,9 +74,7 @@ fn repr_ast_tuple(vm: &VirtualMachine, items: Vec, depth: usize) -> rendered.push_wtf8(&parts[0]); } if items.len() > 2 { - if !parts[0].is_empty() { - rendered.push_wtf8(", ...".as_ref()); - } + rendered.push_wtf8(", ...".as_ref()); if parts.len() > 1 { rendered.push_wtf8(", ".as_ref()); rendered.push_wtf8(&parts[1]); @@ -86,9 +83,6 @@ fn repr_ast_tuple(vm: &VirtualMachine, items: Vec, depth: usize) -> rendered.push_wtf8(", ".as_ref()); rendered.push_wtf8(&parts[1]); } - if items.len() == 1 { - rendered.push_wtf8(",".as_ref()); - } rendered.push_wtf8(")".as_ref()); Ok(rendered) } @@ -104,18 +98,24 @@ pub(crate) fn repr_ast_node( s.push_wtf8("(...)".as_ref()); return Ok(s); } + let Some(_guard) = ReprGuard::enter(vm, obj.as_object()) else { + let mut s = Wtf8Buf::from(&*cls.name()); + s.push_wtf8("(...)".as_ref()); + return Ok(s); + }; - let fields = cls.get_attr(vm.ctx.intern_str("_fields")); - let fields = match fields { - Some(fields) => fields.try_to_value::>(vm)?, + let fields = match cls.get_attr(vm.ctx.intern_str("_fields")) { + Some(fields) => fields, None => { let mut s = Wtf8Buf::from(&*cls.name()); s.push_wtf8("(...)".as_ref()); return Ok(s); } }; + let fields = fields.sequence_unchecked(); + let numfields = fields.length(vm)?; - if fields.is_empty() { + if numfields == 0 { let mut s = Wtf8Buf::from(&*cls.name()); s.push_wtf8("()".as_ref()); return Ok(s); @@ -124,8 +124,12 @@ pub(crate) fn repr_ast_node( let mut rendered = Wtf8Buf::from(&*cls.name()); rendered.push_wtf8("(".as_ref()); - for (idx, field) in fields.iter().enumerate() { - let value = obj.get_attr(field, vm)?; + for idx in 0..numfields { + let field = fields.get_item(idx as isize, vm)?; + let field = field + .downcast::() + .map_err(|_| vm.new_type_error("attribute name must be string"))?; + let value = obj.get_attr(&field, vm)?; let value_repr = if value.fast_isinstance(vm.ctx.types.list_type) { let list = value .downcast::() diff --git a/crates/vm/src/stdlib/_ast/statement.rs b/crates/vm/src/stdlib/_ast/statement.rs index 43d1162a402..bf8b0347695 100644 --- a/crates/vm/src/stdlib/_ast/statement.rs +++ b/crates/vm/src/stdlib/_ast/statement.rs @@ -1,7 +1,67 @@ use super::*; -use crate::stdlib::_ast::argument::{merge_class_def_args, split_class_def_args}; +use crate::stdlib::_ast::argument::{ + KeywordArguments, PositionalArguments, merge_class_def_args, split_class_def_args, +}; +use crate::stdlib::_ast::exception::except_handler_from_object_unvalidated_range; +use crate::stdlib::_ast::type_parameters::type_params_from_field; use rustpython_compiler_core::SourceFile; +fn runtime_decorator_expr_list(values: &[Option]) -> Vec> { + values + .iter() + .map(|value| value.as_ref().map(|decorator| decorator.expression.clone())) + .collect() +} + +fn lower_runtime_decorator_list(values: Vec>) -> Vec { + values + .into_iter() + .map(|value| { + value.unwrap_or_else(|| ast::Decorator { + range: Default::default(), + node_index: Default::default(), + expression: runtime_null_expr_placeholder(), + }) + }) + .collect() +} + +fn definition_range_from_name( + source_file: &SourceFile, + name_start: TextSize, + end: TextSize, + keyword: &str, +) -> TextRange { + let source_code = source_file.to_source_code(); + let line = source_code.line_index(name_start); + let line_start = source_code.line_start(line); + let keyword_start = source_code + .slice(TextRange::new(line_start, name_start)) + .rfind(keyword) + .map_or(line_start, |offset| { + line_start + TextSize::new(offset as u32) + }); + TextRange::new(keyword_start, end) +} + +fn runtime_stmt_type_comment( + vm: &VirtualMachine, + type_comment: Option, +) -> (Option>, Option>) { + type_comment.map_or((None, None), |type_comment| { + super::constant::runtime_string_from_pyobject(vm, type_comment) + }) +} + +fn runtime_stmt_type_comment_object( + vm: &VirtualMachine, + value: Option>, + bytes: Option>, +) -> PyObjectRef { + super::constant::runtime_stmt_type_comment_object(vm, value, bytes) + .unwrap_or_else(|| vm.ctx.none()) +} + // sum impl Node for ast::Stmt { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { @@ -31,110 +91,292 @@ impl Node for ast::Stmt { Self::Break(cons) => cons.ast_to_object(vm, source_file), Self::Continue(cons) => cons.ast_to_object(vm, source_file), Self::IpyEscapeCommand(_) => { - unimplemented!("IPython escape command is not allowed in Python AST") + unreachable!("IPython escape command is not part of Python AST") } } } - #[expect(clippy::if_same_then_else, reason = "Looks better here")] fn ast_from_object( vm: &VirtualMachine, source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeStmtFunctionDef::static_type()) { - Self::FunctionDef(ast::StmtFunctionDef::ast_from_object( + if vm.is_none(&object) { + return Err(vm.new_value_error("None disallowed in statement list")); + } + enum StmtKind { + FunctionDef { is_async: bool }, + ClassDef, + Return, + Delete, + Assign, + TypeAlias, + AugAssign, + AnnAssign, + For { is_async: bool }, + While, + If, + With { is_async: bool }, + Match, + Raise, + Try { is_star: bool }, + Assert, + Import, + ImportFrom, + Global, + Nonlocal, + Expr, + Pass, + Break, + Continue, + } + let kind = if is_node_instance(vm, &object, pyast::NodeStmtFunctionDef::static_type())? { + StmtKind::FunctionDef { is_async: false } + } else if is_node_instance(vm, &object, pyast::NodeStmtAsyncFunctionDef::static_type())? { + StmtKind::FunctionDef { is_async: true } + } else if is_node_instance(vm, &object, pyast::NodeStmtClassDef::static_type())? { + StmtKind::ClassDef + } else if is_node_instance(vm, &object, pyast::NodeStmtReturn::static_type())? { + StmtKind::Return + } else if is_node_instance(vm, &object, pyast::NodeStmtDelete::static_type())? { + StmtKind::Delete + } else if is_node_instance(vm, &object, pyast::NodeStmtAssign::static_type())? { + StmtKind::Assign + } else if is_node_instance(vm, &object, pyast::NodeStmtTypeAlias::static_type())? { + StmtKind::TypeAlias + } else if is_node_instance(vm, &object, pyast::NodeStmtAugAssign::static_type())? { + StmtKind::AugAssign + } else if is_node_instance(vm, &object, pyast::NodeStmtAnnAssign::static_type())? { + StmtKind::AnnAssign + } else if is_node_instance(vm, &object, pyast::NodeStmtFor::static_type())? { + StmtKind::For { is_async: false } + } else if is_node_instance(vm, &object, pyast::NodeStmtAsyncFor::static_type())? { + StmtKind::For { is_async: true } + } else if is_node_instance(vm, &object, pyast::NodeStmtWhile::static_type())? { + StmtKind::While + } else if is_node_instance(vm, &object, pyast::NodeStmtIf::static_type())? { + StmtKind::If + } else if is_node_instance(vm, &object, pyast::NodeStmtWith::static_type())? { + StmtKind::With { is_async: false } + } else if is_node_instance(vm, &object, pyast::NodeStmtAsyncWith::static_type())? { + StmtKind::With { is_async: true } + } else if is_node_instance(vm, &object, pyast::NodeStmtMatch::static_type())? { + StmtKind::Match + } else if is_node_instance(vm, &object, pyast::NodeStmtRaise::static_type())? { + StmtKind::Raise + } else if is_node_instance(vm, &object, pyast::NodeStmtTry::static_type())? { + StmtKind::Try { is_star: false } + } else if is_node_instance(vm, &object, pyast::NodeStmtTryStar::static_type())? { + StmtKind::Try { is_star: true } + } else if is_node_instance(vm, &object, pyast::NodeStmtAssert::static_type())? { + StmtKind::Assert + } else if is_node_instance(vm, &object, pyast::NodeStmtImport::static_type())? { + StmtKind::Import + } else if is_node_instance(vm, &object, pyast::NodeStmtImportFrom::static_type())? { + StmtKind::ImportFrom + } else if is_node_instance(vm, &object, pyast::NodeStmtGlobal::static_type())? { + StmtKind::Global + } else if is_node_instance(vm, &object, pyast::NodeStmtNonlocal::static_type())? { + StmtKind::Nonlocal + } else if is_node_instance(vm, &object, pyast::NodeStmtExpr::static_type())? { + StmtKind::Expr + } else if is_node_instance(vm, &object, pyast::NodeStmtPass::static_type())? { + StmtKind::Pass + } else if is_node_instance(vm, &object, pyast::NodeStmtBreak::static_type())? { + StmtKind::Break + } else if is_node_instance(vm, &object, pyast::NodeStmtContinue::static_type())? { + StmtKind::Continue + } else { + return Err(vm.new_type_error(format!( + "expected some sort of stmt, but got {}", + object.repr(vm)? + ))); + }; + let range = stmt_range_from_object(vm, source_file, object.clone())?; + Ok(match kind { + StmtKind::FunctionDef { is_async } => Self::FunctionDef( + stmt_function_def_from_object_with_range(vm, source_file, object, range, is_async)?, + ), + StmtKind::ClassDef => Self::ClassDef(stmt_class_def_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtAsyncFunctionDef::static_type()) { - Self::FunctionDef(ast::StmtFunctionDef::ast_from_object( + range, + )?), + StmtKind::Return => Self::Return(stmt_return_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtClassDef::static_type()) { - Self::ClassDef(ast::StmtClassDef::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtReturn::static_type()) { - Self::Return(ast::StmtReturn::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtDelete::static_type()) { - Self::Delete(ast::StmtDelete::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtAssign::static_type()) { - Self::Assign(ast::StmtAssign::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtTypeAlias::static_type()) { - Self::TypeAlias(ast::StmtTypeAlias::ast_from_object( + range, + )?), + StmtKind::Delete => Self::Delete(stmt_delete_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtAugAssign::static_type()) { - Self::AugAssign(ast::StmtAugAssign::ast_from_object( + range, + )?), + StmtKind::Assign => Self::Assign(stmt_assign_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtAnnAssign::static_type()) { - Self::AnnAssign(ast::StmtAnnAssign::ast_from_object( + range, + )?), + StmtKind::TypeAlias => Self::TypeAlias(stmt_type_alias_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtFor::static_type()) { - Self::For(ast::StmtFor::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtAsyncFor::static_type()) { - Self::For(ast::StmtFor::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtWhile::static_type()) { - Self::While(ast::StmtWhile::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtIf::static_type()) { - Self::If(ast::StmtIf::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtWith::static_type()) { - Self::With(ast::StmtWith::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtAsyncWith::static_type()) { - Self::With(ast::StmtWith::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtMatch::static_type()) { - Self::Match(ast::StmtMatch::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtRaise::static_type()) { - Self::Raise(ast::StmtRaise::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtTry::static_type()) { - Self::Try(ast::StmtTry::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtTryStar::static_type()) { - Self::Try(ast::StmtTry::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtAssert::static_type()) { - Self::Assert(ast::StmtAssert::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtImport::static_type()) { - Self::Import(ast::StmtImport::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtImportFrom::static_type()) { - Self::ImportFrom(ast::StmtImportFrom::ast_from_object( + range, + )?), + StmtKind::AugAssign => Self::AugAssign(stmt_aug_assign_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeStmtGlobal::static_type()) { - Self::Global(ast::StmtGlobal::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtNonlocal::static_type()) { - Self::Nonlocal(ast::StmtNonlocal::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtExpr::static_type()) { - Self::Expr(ast::StmtExpr::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtPass::static_type()) { - Self::Pass(ast::StmtPass::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtBreak::static_type()) { - Self::Break(ast::StmtBreak::ast_from_object(vm, source_file, object)?) - } else if cls.is(pyast::NodeStmtContinue::static_type()) { - Self::Continue(ast::StmtContinue::ast_from_object(vm, source_file, object)?) - } else if vm.is_none(&object) { - return Err(vm.new_value_error("None disallowed in statement list")); - } else { - return Err(vm.new_type_error(format!( - "expected some sort of stmt, but got {}", - object.repr(vm)? - ))); + range, + )?), + StmtKind::AnnAssign => Self::AnnAssign(stmt_ann_assign_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::For { is_async } => Self::For(stmt_for_from_object_with_range( + vm, + source_file, + object, + range, + is_async, + )?), + StmtKind::While => Self::While(stmt_while_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::If => Self::If(elif_else_clause::ast_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::With { is_async } => Self::With(stmt_with_from_object_with_range( + vm, + source_file, + object, + range, + is_async, + )?), + StmtKind::Match => Self::Match(stmt_match_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Raise => Self::Raise(stmt_raise_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Try { is_star } => Self::Try(stmt_try_from_object_with_range( + vm, + source_file, + object, + range, + is_star, + )?), + StmtKind::Assert => Self::Assert(stmt_assert_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Import => Self::Import(stmt_import_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::ImportFrom => Self::ImportFrom(stmt_import_from_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Global => Self::Global(stmt_global_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Nonlocal => Self::Nonlocal(stmt_nonlocal_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Expr => Self::Expr(stmt_expr_from_object_with_range( + vm, + source_file, + object, + range, + )?), + StmtKind::Pass => Self::Pass(stmt_pass_from_object_with_range(range)), + StmtKind::Break => Self::Break(stmt_break_from_object_with_range(range)), + StmtKind::Continue => Self::Continue(stmt_continue_from_object_with_range(range)), }) } } // constructor +fn stmt_function_def_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, + is_async: bool, +) -> PyResult { + let typ = if is_async { + "AsyncFunctionDef" + } else { + "FunctionDef" + }; + let name = get_required_identifier_field(vm, source_file, &object, "name", typ)?; + let parameters = Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "args", typ)?, + )?; + let body: Vec> = get_node_list_field(vm, source_file, &object, "body", typ)?; + let decorator_list: Vec> = + get_node_list_field(vm, source_file, &object, "decorator_list", typ)?; + let runtime_decorator_exprs = runtime_decorator_expr_list(&decorator_list); + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_decorator_list = runtime_expr_list_metadata(&runtime_decorator_exprs); + let body = lower_runtime_stmt_list(body); + let decorator_list = lower_runtime_decorator_list(decorator_list); + let returns = get_node_field_opt(vm, &object, "returns")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?; + let (runtime_type_comment, runtime_type_comment_bytes) = + runtime_stmt_type_comment(vm, get_ast_string_field_opt(vm, &object, "type_comment")?); + let type_params = type_params_from_field(vm, source_file, &object, "type_params", typ)?; + Ok(ast::StmtFunctionDef { + node_index: Default::default(), + name, + parameters, + body, + decorator_list, + returns, + type_params, + range, + is_async, + runtime_decorator_list, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, + }) +} + impl Node for ast::StmtFunctionDef { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -144,14 +386,20 @@ impl Node for ast::StmtFunctionDef { body, decorator_list, returns, - // type_comment, type_params, is_async, - range: _range, + range, + runtime_decorator_list, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, } = self; - let source_code = source_file.to_source_code(); - let def_line = source_code.line_index(name.range.start()); - let range = TextRange::new(source_code.line_start(def_line), _range.end()); + let range = definition_range_from_name( + source_file, + name.range.start(), + range.end(), + if is_async { "async" } else { "def" }, + ); let cls = if !is_async { pyast::NodeStmtFunctionDef::static_type().to_owned() @@ -161,22 +409,32 @@ impl Node for ast::StmtFunctionDef { let node = NodeAst.into_ref_with_type(vm, cls).unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("name", vm.ctx.new_str(name.as_str()).to_pyobject(vm), vm) + dict.set_item("name", name.ast_to_object(vm, source_file), vm) .unwrap(); dict.set_item("args", parameters.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("body", body.ast_to_object(vm, source_file), vm) - .unwrap(); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); dict.set_item( "decorator_list", - decorator_list.ast_to_object(vm, source_file), + runtime_decorator_list.map_or_else( + || decorator_list.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ), vm, ) .unwrap(); dict.set_item("returns", returns.ast_to_object(vm, source_file), vm) .unwrap(); - // Ruff AST doesn't track type_comment, so always set to None - dict.set_item("type_comment", vm.ctx.none(), vm).unwrap(); + dict.set_item( + "type_comment", + runtime_stmt_type_comment_object(vm, runtime_type_comment, runtime_type_comment_bytes), + vm, + ) + .unwrap(); dict.set_item( "type_params", type_params.map_or_else( @@ -191,57 +449,58 @@ impl Node for ast::StmtFunctionDef { } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - let _cls = _object.class(); - let is_async = _cls.is(pyast::NodeStmtAsyncFunctionDef::static_type()); - let range = range_from_object(_vm, source_file, _object.clone(), "FunctionDef")?; - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "name", "FunctionDef")?, - )?, - parameters: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "args", "FunctionDef")?, - )?, - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "FunctionDef")?, - )?, - decorator_list: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "decorator_list", "FunctionDef")?, - )?, - returns: get_node_field_opt(_vm, &_object, "returns")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - // TODO: Ruff ignores type_comment during parsing - // type_comment: get_node_field_opt(_vm, &_object, "type_comment")? - // .map(|obj| Node::ast_from_object(_vm, obj)) - // .transpose()?, - type_params: Node::ast_from_object( - _vm, - source_file, - get_node_field_opt(_vm, &_object, "type_params")? - .unwrap_or_else(|| _vm.ctx.new_list(Vec::new()).into()), - )?, - range, - is_async, - }) + let is_async = + is_node_instance(vm, &_object, pyast::NodeStmtAsyncFunctionDef::static_type())?; + let typ = if is_async { + "AsyncFunctionDef" + } else { + "FunctionDef" + }; + let range = range_from_object(vm, source_file, _object.clone(), typ)?; + stmt_function_def_from_object_with_range(vm, source_file, _object, range, is_async) } } // constructor +fn stmt_class_def_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let name = get_required_identifier_field(vm, source_file, &object, "name", "ClassDef")?; + let bases = PositionalArguments::ast_from_field(vm, source_file, &object, "bases", "ClassDef")?; + let keywords = + KeywordArguments::ast_from_field(vm, source_file, &object, "keywords", "ClassDef")?; + let body: Vec> = + get_node_list_field(vm, source_file, &object, "body", "ClassDef")?; + let decorator_list: Vec> = + get_node_list_field(vm, source_file, &object, "decorator_list", "ClassDef")?; + let runtime_decorator_exprs = runtime_decorator_expr_list(&decorator_list); + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_decorator_list = runtime_expr_list_metadata(&runtime_decorator_exprs); + let body = lower_runtime_stmt_list(body); + let decorator_list = lower_runtime_decorator_list(decorator_list); + let type_params = type_params_from_field(vm, source_file, &object, "type_params", "ClassDef")?; + Ok(ast::StmtClassDef { + node_index: Default::default(), + name, + arguments: merge_class_def_args(Some(bases), Some(keywords)), + body, + decorator_list, + type_params, + range, + runtime_decorator_list, + runtime_body, + }) +} + impl Node for ast::StmtClassDef { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, name, @@ -249,184 +508,213 @@ impl Node for ast::StmtClassDef { body, decorator_list, type_params, - range: _range, + range, + runtime_decorator_list, + runtime_body, } = self; let (bases, keywords) = split_class_def_args(arguments); - let source_code = source_file.to_source_code(); - let class_line = source_code.line_index(name.range.start()); - let range = TextRange::new(source_code.line_start(class_line), _range.end()); + let range = + definition_range_from_name(source_file, name.range.start(), range.end(), "class"); let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtClassDef::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtClassDef::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().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(); dict.set_item( "bases", bases.map_or_else( - || _vm.ctx.new_list(vec![]).into(), - |b| b.ast_to_object(_vm, source_file), + || vm.ctx.new_list(vec![]).into(), + |b| b.ast_to_object(vm, source_file), ), - _vm, + vm, ) .unwrap(); dict.set_item( "keywords", keywords.map_or_else( - || _vm.ctx.new_list(vec![]).into(), - |k| k.ast_to_object(_vm, source_file), + || vm.ctx.new_list(vec![]).into(), + |k| k.ast_to_object(vm, source_file), ), - _vm, + vm, ) .unwrap(); - dict.set_item("body", body.ast_to_object(_vm, source_file), _vm) - .unwrap(); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); dict.set_item( "decorator_list", - decorator_list.ast_to_object(_vm, source_file), - _vm, + runtime_decorator_list.map_or_else( + || decorator_list.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ), + vm, ) .unwrap(); dict.set_item( "type_params", type_params.map_or_else( - || _vm.ctx.new_list(vec![]).into(), - |tp| tp.ast_to_object(_vm, source_file), + || vm.ctx.new_list(vec![]).into(), + |tp| tp.ast_to_object(vm, source_file), ), - _vm, + vm, ) .unwrap(); - node_add_location(&dict, range, _vm, source_file); + node_add_location(&dict, range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - let bases = Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "bases", "ClassDef")?, - )?; - let keywords = Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "keywords", "ClassDef")?, - )?; - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "name", "ClassDef")?, - )?, - arguments: merge_class_def_args(bases, keywords), - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "ClassDef")?, - )?, - decorator_list: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "decorator_list", "ClassDef")?, - )?, - type_params: Node::ast_from_object( - _vm, - source_file, - get_node_field_opt(_vm, &_object, "type_params")? - .unwrap_or_else(|| _vm.ctx.new_list(Vec::new()).into()), - )?, - range: range_from_object(_vm, source_file, _object, "ClassDef")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "ClassDef")?; + stmt_class_def_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_return_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtReturn { + node_index: Default::default(), + value: get_node_field_opt(vm, &object, "value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::StmtReturn { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, value, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtReturn::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtReturn::static_type().to_owned()) .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(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: get_node_field_opt(_vm, &_object, "value")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - range: range_from_object(_vm, source_file, _object, "Return")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Return")?; + stmt_return_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_delete_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let targets: Vec> = + get_node_list_field(vm, source_file, &object, "targets", "Delete")?; + let (runtime_targets, targets) = runtime_expr_list_from_values(targets); + Ok(ast::StmtDelete { + node_index: Default::default(), + targets, + range, + runtime_targets, + }) +} + impl Node for ast::StmtDelete { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, targets, range: _range, + runtime_targets, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtDelete::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtDelete::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("targets", targets.ast_to_object(_vm, source_file), _vm) - .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let targets = runtime_targets.map_or_else( + || targets.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("targets", targets, vm).unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - targets: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "targets", "Delete")?, - )?, - range: range_from_object(_vm, source_file, _object, "Delete")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Delete")?; + stmt_delete_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_assign_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let targets: Vec> = + get_node_list_field(vm, source_file, &object, "targets", "Assign")?; + let (runtime_targets, targets) = runtime_expr_list_from_values(targets); + let value = get_required_node_field(vm, source_file, &object, "value", "Assign")?; + let (runtime_type_comment, runtime_type_comment_bytes) = + runtime_stmt_type_comment(vm, get_ast_string_field_opt(vm, &object, "type_comment")?); + Ok(ast::StmtAssign { + node_index: Default::default(), + targets, + value, + range, + runtime_targets, + runtime_type_comment, + runtime_type_comment_bytes, + }) +} + impl Node for ast::StmtAssign { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, targets, value, - // type_comment, range, + runtime_targets, + runtime_type_comment, + runtime_type_comment_bytes, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeStmtAssign::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("targets", targets.ast_to_object(vm, source_file), vm) - .unwrap(); + let targets = runtime_targets.map_or_else( + || targets.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("targets", targets, vm).unwrap(); dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - // TODO - dict.set_item("type_comment", vm.ctx.none(), vm).unwrap(); + dict.set_item( + "type_comment", + runtime_stmt_type_comment_object(vm, runtime_type_comment, runtime_type_comment_bytes), + vm, + ) + .unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -435,29 +723,29 @@ impl Node for ast::StmtAssign { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - targets: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "targets", "Assign")?, - )?, - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Assign")?, - )?, - // 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, "Assign")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Assign")?; + stmt_assign_from_object_with_range(vm, source_file, object, range) } } // constructor +fn stmt_type_alias_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtTypeAlias { + node_index: Default::default(), + name: get_required_node_field(vm, source_file, &object, "name", "TypeAlias")?, + type_params: type_params_from_field(vm, source_file, &object, "type_params", "TypeAlias")?, + value: get_required_node_field(vm, source_file, &object, "value", "TypeAlias")?, + range, + }) +} + impl Node for ast::StmtTypeAlias { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, name, @@ -466,56 +754,58 @@ impl Node for ast::StmtTypeAlias { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtTypeAlias::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtTypeAlias::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().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(); dict.set_item( "type_params", type_params.map_or_else( - || _vm.ctx.new_list(Vec::new()).into(), - |tp| tp.ast_to_object(_vm, source_file), + || vm.ctx.new_list(Vec::new()).into(), + |tp| tp.ast_to_object(vm, source_file), ), - _vm, + vm, ) .unwrap(); - dict.set_item("value", value.ast_to_object(_vm, source_file), _vm) + dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "name", "TypeAlias")?, - )?, - type_params: Node::ast_from_object( - _vm, - source_file, - get_node_field_opt(_vm, &_object, "type_params")?.unwrap_or_else(|| _vm.ctx.none()), - )?, - value: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "value", "TypeAlias")?, - )?, - range: range_from_object(_vm, source_file, _object, "TypeAlias")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "TypeAlias")?; + stmt_type_alias_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_aug_assign_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtAugAssign { + node_index: Default::default(), + target: get_required_node_field(vm, source_file, &object, "target", "AugAssign")?, + op: Node::ast_from_object( + vm, + source_file, + get_node_field_required(vm, &object, "op", "AugAssign")?, + )?, + value: get_required_node_field(vm, source_file, &object, "value", "AugAssign")?, + range, + }) +} + impl Node for ast::StmtAugAssign { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, target, @@ -524,48 +814,56 @@ impl Node for ast::StmtAugAssign { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtAugAssign::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtAugAssign::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("target", target.ast_to_object(_vm, source_file), _vm) + dict.set_item("target", target.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("op", op.ast_to_object(_vm, source_file), _vm) + dict.set_item("op", op.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("value", value.ast_to_object(_vm, source_file), _vm) + dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - target: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "target", "AugAssign")?, - )?, - op: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "op", "AugAssign")?, - )?, - value: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "value", "AugAssign")?, - )?, - range: range_from_object(_vm, source_file, _object, "AugAssign")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "AugAssign")?; + stmt_aug_assign_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_ann_assign_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let simple = node_object_to_i32(vm, get_node_field(vm, &object, "simple", "AnnAssign")?)?; + let runtime_simple = if simple != 0 && simple != 1 { + Some(simple) + } else { + None + }; + Ok(ast::StmtAnnAssign { + node_index: Default::default(), + target: get_required_node_field(vm, source_file, &object, "target", "AnnAssign")?, + annotation: get_required_node_field(vm, source_file, &object, "annotation", "AnnAssign")?, + value: get_node_field_opt(vm, &object, "value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + simple: simple != 0, + range, + runtime_simple, + }) +} + impl Node for ast::StmtAnnAssign { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, target, @@ -573,59 +871,73 @@ impl Node for ast::StmtAnnAssign { value, simple, range: _range, + runtime_simple, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtAnnAssign::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtAnnAssign::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("target", target.ast_to_object(_vm, source_file), _vm) + dict.set_item("target", target.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item( - "annotation", - annotation.ast_to_object(_vm, source_file), - _vm, - ) - .unwrap(); - dict.set_item("value", value.ast_to_object(_vm, source_file), _vm) + dict.set_item("annotation", annotation.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("simple", simple.ast_to_object(_vm, source_file), _vm) + dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let simple = runtime_simple.map_or_else( + || simple.ast_to_object(vm, source_file), + |simple| vm.ctx.new_int(simple).into(), + ); + dict.set_item("simple", simple, vm).unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - target: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "target", "AnnAssign")?, - )?, - annotation: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "annotation", "AnnAssign")?, - )?, - value: get_node_field_opt(_vm, &_object, "value")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - simple: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "simple", "AnnAssign")?, - )?, - range: range_from_object(_vm, source_file, _object, "AnnAssign")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "AnnAssign")?; + stmt_ann_assign_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_for_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, + is_async: bool, +) -> PyResult { + let typ = if is_async { "AsyncFor" } else { "For" }; + let target = get_required_node_field(vm, source_file, &object, "target", typ)?; + let iter = get_required_node_field(vm, source_file, &object, "iter", typ)?; + let body: Vec> = get_node_list_field(vm, source_file, &object, "body", typ)?; + let orelse: Vec> = + get_node_list_field(vm, source_file, &object, "orelse", typ)?; + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_orelse = runtime_stmt_list_metadata(&orelse); + let body = lower_runtime_stmt_list(body); + let orelse = lower_runtime_stmt_list(orelse); + let (runtime_type_comment, runtime_type_comment_bytes) = + runtime_stmt_type_comment(vm, get_ast_string_field_opt(vm, &object, "type_comment")?); + Ok(ast::StmtFor { + node_index: Default::default(), + target, + iter, + body, + orelse, + range, + is_async, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, + runtime_orelse, + }) +} + impl Node for ast::StmtFor { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, is_async, @@ -633,8 +945,11 @@ impl Node for ast::StmtFor { iter, body, orelse, - // type_comment, range: _range, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, + runtime_orelse, } = self; let cls = if !is_async { @@ -643,133 +958,134 @@ impl Node for ast::StmtFor { pyast::NodeStmtAsyncFor::static_type().to_owned() }; - let node = NodeAst.into_ref_with_type(_vm, cls).unwrap(); + let node = NodeAst.into_ref_with_type(vm, cls).unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("target", target.ast_to_object(_vm, source_file), _vm) + dict.set_item("target", target.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("iter", iter.ast_to_object(_vm, source_file), _vm) + dict.set_item("iter", iter.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("body", body.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("orelse", orelse.ast_to_object(_vm, source_file), _vm) - .unwrap(); - // Ruff AST doesn't track type_comment, so always set to None - dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); + let orelse = runtime_orelse.map_or_else( + || orelse.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("orelse", orelse, vm).unwrap(); + dict.set_item( + "type_comment", + runtime_stmt_type_comment_object(vm, runtime_type_comment, runtime_type_comment_bytes), + vm, + ) + .unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - let _cls = _object.class(); debug_assert!( - _cls.is(pyast::NodeStmtFor::static_type()) - || _cls.is(pyast::NodeStmtAsyncFor::static_type()) + is_node_instance(vm, &_object, pyast::NodeStmtFor::static_type())? + || is_node_instance(vm, &_object, pyast::NodeStmtAsyncFor::static_type())? ); - let is_async = _cls.is(pyast::NodeStmtAsyncFor::static_type()); - Ok(Self { - node_index: Default::default(), - target: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "target", "For")?, - )?, - iter: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "iter", "For")?, - )?, - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "For")?, - )?, - orelse: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "orelse", "For")?, - )?, - // 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, "For")?, - is_async, - }) + let is_async = is_node_instance(vm, &_object, pyast::NodeStmtAsyncFor::static_type())?; + let typ = if is_async { "AsyncFor" } else { "For" }; + let range = range_from_object(vm, source_file, _object.clone(), typ)?; + stmt_for_from_object_with_range(vm, source_file, _object, range, is_async) } } // constructor +fn stmt_while_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let body: Vec> = + get_node_list_field(vm, source_file, &object, "body", "While")?; + let orelse: Vec> = + get_node_list_field(vm, source_file, &object, "orelse", "While")?; + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_orelse = runtime_stmt_list_metadata(&orelse); + Ok(ast::StmtWhile { + node_index: Default::default(), + test: get_required_node_field(vm, source_file, &object, "test", "While")?, + body: lower_runtime_stmt_list(body), + orelse: lower_runtime_stmt_list(orelse), + range, + runtime_body, + runtime_orelse, + }) +} + impl Node for ast::StmtWhile { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, test, body, orelse, range: _range, + runtime_body, + runtime_orelse, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtWhile::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtWhile::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("test", test.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("body", body.ast_to_object(_vm, source_file), _vm) + dict.set_item("test", test.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("orelse", orelse.ast_to_object(_vm, source_file), _vm) - .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); + let orelse = runtime_orelse.map_or_else( + || orelse.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("orelse", orelse, vm).unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - test: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "test", "While")?, - )?, - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "While")?, - )?, - orelse: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "orelse", "While")?, - )?, - range: range_from_object(_vm, source_file, _object, "While")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "While")?; + stmt_while_from_object_with_range(vm, source_file, _object, range) } } // constructor impl Node for ast::StmtIf { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { - node_index: _, + node_index, test, body, range, elif_else_clauses, + runtime_body, } = self; elif_else_clause::ast_to_object( ast::ElifElseClause { - node_index: Default::default(), + node_index, range, test: Some(*test), body, + runtime_body, + runtime_orelse: None, }, elif_else_clauses.into_iter(), - _vm, + vm, source_file, ) } @@ -778,19 +1094,47 @@ impl Node for ast::StmtIf { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - elif_else_clause::ast_from_object(vm, source_file, object) + let range = range_from_object(vm, source_file, object.clone(), "If")?; + elif_else_clause::ast_from_object_with_range(vm, source_file, object, range) } } // constructor +fn stmt_with_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, + is_async: bool, +) -> PyResult { + let typ = if is_async { "AsyncWith" } else { "With" }; + let items = get_node_list_field(vm, source_file, &object, "items", typ)?; + let body: Vec> = get_node_list_field(vm, source_file, &object, "body", typ)?; + let (runtime_body, body) = runtime_stmt_list_from_values(body); + let (runtime_type_comment, runtime_type_comment_bytes) = + runtime_stmt_type_comment(vm, get_ast_string_field_opt(vm, &object, "type_comment")?); + Ok(ast::StmtWith { + node_index: Default::default(), + items, + body, + range, + is_async, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, + }) +} + impl Node for ast::StmtWith { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, is_async, items, body, - // type_comment, range: _range, + runtime_type_comment, + runtime_type_comment_bytes, + runtime_body, } = self; let cls = if !is_async { @@ -799,51 +1143,56 @@ impl Node for ast::StmtWith { pyast::NodeStmtAsyncWith::static_type().to_owned() }; - let node = NodeAst.into_ref_with_type(_vm, cls).unwrap(); + let node = NodeAst.into_ref_with_type(vm, cls).unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("items", items.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("body", body.ast_to_object(_vm, source_file), _vm) + dict.set_item("items", items.ast_to_object(vm, source_file), vm) .unwrap(); - // Ruff AST doesn't track type_comment, so always set to None - dict.set_item("type_comment", _vm.ctx.none(), _vm).unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); + dict.set_item( + "type_comment", + runtime_stmt_type_comment_object(vm, runtime_type_comment, runtime_type_comment_bytes), + vm, + ) + .unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - let _cls = _object.class(); debug_assert!( - _cls.is(pyast::NodeStmtWith::static_type()) - || _cls.is(pyast::NodeStmtAsyncWith::static_type()) + is_node_instance(vm, &_object, pyast::NodeStmtWith::static_type())? + || is_node_instance(vm, &_object, pyast::NodeStmtAsyncWith::static_type())? ); - let is_async = _cls.is(pyast::NodeStmtAsyncWith::static_type()); - Ok(Self { - node_index: Default::default(), - items: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "items", "With")?, - )?, - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "With")?, - )?, - // 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, "With")?, - is_async, - }) + let is_async = is_node_instance(vm, &_object, pyast::NodeStmtAsyncWith::static_type())?; + let typ = if is_async { "AsyncWith" } else { "With" }; + let range = range_from_object(vm, source_file, _object.clone(), typ)?; + stmt_with_from_object_with_range(vm, source_file, _object, range, is_async) } } // constructor +fn stmt_match_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtMatch { + node_index: Default::default(), + subject: get_required_node_field(vm, source_file, &object, "subject", "Match")?, + cases: get_node_list_field(vm, source_file, &object, "cases", "Match")?, + range, + }) +} + impl Node for ast::StmtMatch { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, subject, @@ -851,40 +1200,46 @@ impl Node for ast::StmtMatch { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtMatch::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtMatch::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("subject", subject.ast_to_object(_vm, source_file), _vm) + dict.set_item("subject", subject.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("cases", cases.ast_to_object(_vm, source_file), _vm) + dict.set_item("cases", cases.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - subject: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "subject", "Match")?, - )?, - cases: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "cases", "Match")?, - )?, - range: range_from_object(_vm, source_file, _object, "Match")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Match")?; + stmt_match_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_raise_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtRaise { + node_index: Default::default(), + exc: get_node_field_opt(vm, &object, "exc")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + cause: get_node_field_opt(vm, &object, "cause")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::StmtRaise { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, exc, @@ -892,36 +1247,118 @@ impl Node for ast::StmtRaise { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtRaise::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtRaise::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("exc", exc.ast_to_object(_vm, source_file), _vm) + dict.set_item("exc", exc.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("cause", cause.ast_to_object(_vm, source_file), _vm) + dict.set_item("cause", cause.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - exc: get_node_field_opt(_vm, &_object, "exc")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - cause: get_node_field_opt(_vm, &_object, "cause")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - range: range_from_object(_vm, source_file, _object, "Raise")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Raise")?; + stmt_raise_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_try_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, + is_star: bool, +) -> PyResult { + let typ = if is_star { "TryStar" } else { "Try" }; + let body: Vec> = get_node_list_field(vm, source_file, &object, "body", typ)?; + let orelse: Vec> = + get_node_list_field(vm, source_file, &object, "orelse", typ)?; + let finalbody: Vec> = + get_node_list_field(vm, source_file, &object, "finalbody", typ)?; + let (runtime_handler_values, handlers) = + except_handler_list_from_field(vm, source_file, &object, typ, is_star, range)?; + let runtime_body = runtime_stmt_list_metadata(&body); + let runtime_orelse = runtime_stmt_list_metadata(&orelse); + let runtime_finalbody = runtime_stmt_list_metadata(&finalbody); + let runtime_handlers = runtime_except_handler_list_metadata(&runtime_handler_values); + Ok(ast::StmtTry { + node_index: Default::default(), + body: lower_runtime_stmt_list(body), + handlers, + orelse: lower_runtime_stmt_list(orelse), + finalbody: lower_runtime_stmt_list(finalbody), + range, + is_star, + runtime_body, + runtime_handlers, + runtime_orelse, + runtime_finalbody, + }) +} + +fn except_handler_list_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + typ: &str, + is_try_star: bool, + range: TextRange, +) -> PyResult<(Vec>, Vec)> { + let value = get_node_list_field_object(vm, object, "handlers", typ)?; + let list = value.downcast_ref::().unwrap(); + let len = list.borrow_vec().len(); + let mut result = Vec::with_capacity(len); + let mut runtime_values = Vec::with_capacity(len); + let recursion_context = format!(" while traversing '{typ}' node"); + for i in 0..len { + let item = { + let items = list.borrow_vec(); + if items.len() != len { + return Err(vm.new_runtime_error(format!( + r#"{typ} field "handlers" changed size during iteration"# + ))); + } + items[i].clone() + }; + let runtime_handler = if vm.is_none(&item) { + None + } else { + Some(vm.with_recursion(&recursion_context, || { + if is_try_star { + except_handler_from_object_unvalidated_range(vm, source_file, item) + } else { + Node::ast_from_object(vm, source_file, item) + } + })?) + }; + let handler = runtime_handler.clone().unwrap_or_else(|| { + ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + node_index: Default::default(), + range, + type_: None, + name: None, + body: Vec::new(), + runtime_body: None, + }) + }); + runtime_values.push(runtime_handler); + result.push(handler); + if list.borrow_vec().len() != len { + return Err(vm.new_runtime_error(format!( + r#"{typ} field "handlers" changed size during iteration"# + ))); + } + } + Ok((runtime_values, result)) +} + impl Node for ast::StmtTry { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, body, @@ -930,9 +1367,12 @@ impl Node for ast::StmtTry { finalbody, range: _range, is_star, + runtime_body, + runtime_handlers, + runtime_orelse, + runtime_finalbody, } = self; - // let cls = gen::NodeStmtTry::static_type().to_owned(); let cls = if is_star { pyast::NodeStmtTryStar::static_type() } else { @@ -940,62 +1380,66 @@ impl Node for ast::StmtTry { } .to_owned(); - let node = NodeAst.into_ref_with_type(_vm, cls).unwrap(); + let node = NodeAst.into_ref_with_type(vm, cls).unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("body", body.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("handlers", handlers.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("orelse", orelse.ast_to_object(_vm, source_file), _vm) - .unwrap(); - dict.set_item("finalbody", finalbody.ast_to_object(_vm, source_file), _vm) - .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + let body = runtime_body.map_or_else( + || body.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("body", body, vm).unwrap(); + let handlers = runtime_handlers.map_or_else( + || handlers.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("handlers", handlers, vm).unwrap(); + let orelse = runtime_orelse.map_or_else( + || orelse.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("orelse", orelse, vm).unwrap(); + let finalbody = runtime_finalbody.map_or_else( + || finalbody.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ); + dict.set_item("finalbody", finalbody, vm).unwrap(); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - let _cls = _object.class(); - let is_star = _cls.is(pyast::NodeStmtTryStar::static_type()); - let _cls = _object.class(); + let is_star = is_node_instance(vm, &_object, pyast::NodeStmtTryStar::static_type())?; debug_assert!( - _cls.is(pyast::NodeStmtTry::static_type()) - || _cls.is(pyast::NodeStmtTryStar::static_type()) + is_node_instance(vm, &_object, pyast::NodeStmtTry::static_type())? + || is_node_instance(vm, &_object, pyast::NodeStmtTryStar::static_type())? ); - - Ok(Self { - node_index: Default::default(), - body: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "body", "Try")?, - )?, - handlers: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "handlers", "Try")?, - )?, - orelse: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "orelse", "Try")?, - )?, - finalbody: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "finalbody", "Try")?, - )?, - range: range_from_object(_vm, source_file, _object, "Try")?, - is_star, - }) + let typ = if is_star { "TryStar" } else { "Try" }; + let range = range_from_object(vm, source_file, _object.clone(), typ)?; + stmt_try_from_object_with_range(vm, source_file, _object, range, is_star) } } + // constructor +fn stmt_assert_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtAssert { + node_index: Default::default(), + test: get_required_node_field(vm, source_file, &object, "test", "Assert")?, + msg: get_node_field_opt(vm, &object, "msg")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::StmtAssert { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, test, @@ -1003,38 +1447,42 @@ impl Node for ast::StmtAssert { range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtAssert::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtAssert::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - 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(); - dict.set_item("msg", msg.ast_to_object(_vm, source_file), _vm) + dict.set_item("msg", msg.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - test: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "test", "Assert")?, - )?, - msg: get_node_field_opt(_vm, &_object, "msg")? - .map(|obj| Node::ast_from_object(_vm, source_file, obj)) - .transpose()?, - range: range_from_object(_vm, source_file, _object, "Assert")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Assert")?; + stmt_assert_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_import_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtImport { + node_index: Default::default(), + names: get_node_list_field(vm, source_file, &object, "names", "Import")?, + range, + is_lazy: false, + }) +} + impl Node for ast::StmtImport { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, names, @@ -1042,33 +1490,62 @@ impl Node for ast::StmtImport { is_lazy: _, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtImport::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtImport::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("names", names.ast_to_object(_vm, source_file), _vm) + dict.set_item("names", names.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - names: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "names", "Import")?, - )?, - range: range_from_object(_vm, source_file, _object, "Import")?, - is_lazy: false, // Placeholder - }) + let range = range_from_object(vm, source_file, _object.clone(), "Import")?; + stmt_import_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_import_from_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let (level, raw_level) = import_from_level_from_field(vm, &object)?; + let runtime_level = raw_level.filter(|level| *level < 0); + Ok(ast::StmtImportFrom { + node_index: Default::default(), + module: get_node_field_opt(vm, &object, "module")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + names: get_node_list_field(vm, source_file, &object, "names", "ImportFrom")?, + level, + range, + is_lazy: false, + runtime_level, + }) +} + +fn import_from_level_from_field( + vm: &VirtualMachine, + object: &PyObjectRef, +) -> PyResult<(u32, Option)> { + let Some(value) = get_node_field_opt(vm, object, "level")? else { + return Ok((0, None)); + }; + let level = vm.with_recursion(" while traversing 'ImportFrom' node", || { + node_object_to_i32(vm, value) + })?; + if level < 0 { + return Ok((0, Some(level))); + } + Ok((level as u32, None)) +} + impl Node for ast::StmtImportFrom { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -1078,6 +1555,7 @@ impl Node for ast::StmtImportFrom { level, range, is_lazy: _, + runtime_level, } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeStmtImportFrom::static_type().to_owned()) @@ -1087,8 +1565,11 @@ impl Node for ast::StmtImportFrom { .unwrap(); dict.set_item("names", names.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("level", vm.ctx.new_int(level).to_pyobject(vm), vm) - .unwrap(); + let level = runtime_level.map_or_else( + || vm.ctx.new_int(level).to_pyobject(vm), + |level| vm.ctx.new_int(level).to_pyobject(vm), + ); + dict.set_item("level", level, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -1098,141 +1579,143 @@ impl Node for ast::StmtImportFrom { source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - module: get_node_field_opt(vm, &_object, "module")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - names: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &_object, "names", "ImportFrom")?, - )?, - level: get_node_field_opt(vm, &_object, "level")? - .map(|obj| -> PyResult { - let int: PyRef = obj.try_into_value(vm)?; - let value: i64 = int.try_to_primitive(vm)?; - if value < 0 { - return Err(vm.new_value_error("Negative ImportFrom level")); - } - u32::try_from(value) - .map_err(|_| vm.new_overflow_error("ImportFrom level out of range")) - }) - .transpose()? - .unwrap_or(0), - range: range_from_object(vm, source_file, _object, "ImportFrom")?, - is_lazy: false, // Placeholder - }) + let range = range_from_object(vm, source_file, _object.clone(), "ImportFrom")?; + stmt_import_from_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_global_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtGlobal { + node_index: Default::default(), + names: get_node_list_field(vm, source_file, &object, "names", "Global")?, + range, + }) +} + impl Node for ast::StmtGlobal { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, names, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtGlobal::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtGlobal::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("names", names.ast_to_object(_vm, source_file), _vm) + dict.set_item("names", names.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - names: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "names", "Global")?, - )?, - range: range_from_object(_vm, source_file, _object, "Global")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Global")?; + stmt_global_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_nonlocal_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtNonlocal { + node_index: Default::default(), + names: get_node_list_field(vm, source_file, &object, "names", "Nonlocal")?, + range, + }) +} + impl Node for ast::StmtNonlocal { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, names, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtNonlocal::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtNonlocal::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("names", names.ast_to_object(_vm, source_file), _vm) + dict.set_item("names", names.ast_to_object(vm, source_file), vm) .unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - names: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "names", "Nonlocal")?, - )?, - range: range_from_object(_vm, source_file, _object, "Nonlocal")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Nonlocal")?; + stmt_nonlocal_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_expr_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::StmtExpr { + node_index: Default::default(), + value: get_required_node_field(vm, source_file, &object, "value", "Expr")?, + range, + }) +} + impl Node for ast::StmtExpr { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, value, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtExpr::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtExpr::static_type().to_owned()) .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(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - value: Node::ast_from_object( - _vm, - source_file, - get_node_field(_vm, &_object, "value", "Expr")?, - )?, - range: range_from_object(_vm, source_file, _object, "Expr")?, - }) + let range = range_from_object(vm, source_file, _object.clone(), "Expr")?; + stmt_expr_from_object_with_range(vm, source_file, _object, range) } } // constructor +fn stmt_pass_from_object_with_range(range: TextRange) -> ast::StmtPass { + ast::StmtPass { + node_index: Default::default(), + range, + } +} + impl Node for ast::StmtPass { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtPass::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtPass::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); let location = super::text_range_to_source_range(source_file, _range); @@ -1250,76 +1733,84 @@ impl Node for ast::StmtPass { location.end.column.get() }; - dict.set_item("lineno", _vm.ctx.new_int(start_row).into(), _vm) + dict.set_item("lineno", vm.ctx.new_int(start_row).into(), vm) .unwrap(); - dict.set_item("col_offset", _vm.ctx.new_int(start_col).into(), _vm) + dict.set_item("col_offset", vm.ctx.new_int(start_col).into(), vm) .unwrap(); - dict.set_item("end_lineno", _vm.ctx.new_int(end_row).into(), _vm) + dict.set_item("end_lineno", vm.ctx.new_int(end_row).into(), vm) .unwrap(); - dict.set_item("end_col_offset", _vm.ctx.new_int(end_col).into(), _vm) + dict.set_item("end_col_offset", vm.ctx.new_int(end_col).into(), vm) .unwrap(); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - range: range_from_object(_vm, source_file, _object, "Pass")?, - }) + let range = range_from_object(vm, source_file, _object, "Pass")?; + Ok(stmt_pass_from_object_with_range(range)) } } // constructor +fn stmt_break_from_object_with_range(range: TextRange) -> ast::StmtBreak { + ast::StmtBreak { + node_index: Default::default(), + range, + } +} + impl Node for ast::StmtBreak { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtBreak::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtBreak::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - range: range_from_object(_vm, source_file, _object, "Break")?, - }) + let range = range_from_object(vm, source_file, _object, "Break")?; + Ok(stmt_break_from_object_with_range(range)) } } // constructor +fn stmt_continue_from_object_with_range(range: TextRange) -> ast::StmtContinue { + ast::StmtContinue { + node_index: Default::default(), + range, + } +} + impl Node for ast::StmtContinue { - fn ast_to_object(self, _vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { + fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { node_index: _, range: _range, } = self; let node = NodeAst - .into_ref_with_type(_vm, pyast::NodeStmtContinue::static_type().to_owned()) + .into_ref_with_type(vm, pyast::NodeStmtContinue::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - node_add_location(&dict, _range, _vm, source_file); + node_add_location(&dict, _range, vm, source_file); node.into() } fn ast_from_object( - _vm: &VirtualMachine, + vm: &VirtualMachine, source_file: &SourceFile, _object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - range: range_from_object(_vm, source_file, _object, "Continue")?, - }) + let range = range_from_object(vm, source_file, _object, "Continue")?; + Ok(stmt_continue_from_object_with_range(range)) } } diff --git a/crates/vm/src/stdlib/_ast/string.rs b/crates/vm/src/stdlib/_ast/string.rs index 24cae476694..7a3bb8799b9 100644 --- a/crates/vm/src/stdlib/_ast/string.rs +++ b/crates/vm/src/stdlib/_ast/string.rs @@ -19,6 +19,7 @@ fn ruff_fstring_element_into_iter( } fn ruff_fstring_element_to_joined_str_part( + vm: &VirtualMachine, element: ast::InterpolatedStringElement, ) -> JoinedStrPart { match element { @@ -38,12 +39,21 @@ fn ruff_fstring_element_to_joined_str_part( conversion, format_spec, node_index: _, - }) => JoinedStrPart::FormattedValue(FormattedValue { - value: expression, - conversion, - format_spec: ruff_format_spec_to_joined_str(format_spec), - range, - }), + runtime_str: _, + runtime_interpolation_format_spec: _, + runtime_formatted_value_format_spec, + }) => { + let runtime_format_spec = runtime_formatted_value_format_spec.or_else(|| { + ruff_format_spec_to_joined_str(vm, format_spec) + .map(|joined_str| Box::new(joined_str.into_expr(false))) + }); + JoinedStrPart::FormattedValue(FormattedValue { + value: expression, + conversion, + format_spec: runtime_format_spec, + range, + }) + } } } @@ -235,6 +245,7 @@ fn warn_invalid_escape_sequences_in_format_spec( } fn ruff_format_spec_to_joined_str( + vm: &VirtualMachine, format_spec: Option>, ) -> Option> { match format_spec { @@ -254,10 +265,14 @@ fn ruff_format_spec_to_joined_str( range }; let values: Vec<_> = ruff_fstring_element_into_iter(elements) - .map(ruff_fstring_element_to_joined_str_part) + .map(|element| ruff_fstring_element_to_joined_str_part(vm, element)) .collect(); let values = normalize_joined_str_parts(values).into_boxed_slice(); - Some(Box::new(JoinedStr { range, values })) + Some(Box::new(JoinedStr { + range, + values, + runtime_values: None, + })) } } } @@ -290,40 +305,98 @@ fn ruff_fstring_element_to_ruff_fstring_part( } } -fn joined_str_to_ruff_format_spec( - joined_str: Option>, +fn format_spec_expr_to_ruff_format_spec( + format_spec: Option>, ) -> Option> { - match joined_str { - None => None, - Some(joined_str) => { - let JoinedStr { range, values } = *joined_str; - let elements: Vec<_> = Box::into_iter(values) - .map(joined_str_part_to_ruff_fstring_element) - .collect(); - let format_spec = ast::InterpolatedStringFormatSpec { - node_index: Default::default(), + let format_spec = format_spec?; + let ast::Expr::FString(mut fstring) = *format_spec else { + return None; + }; + let ast::ExprFString { + range, + ref mut value, + node_index: _, + runtime_joined_str: _, + runtime_values: _, + } = fstring; + let default_part = ast::FStringPart::FString(ast::FString { + node_index: Default::default(), + range: Default::default(), + elements: Default::default(), + flags: ast::FStringFlags::empty(), + }); + let mut elements = Vec::new(); + for i in 0..value.as_slice().len() { + let part = core::mem::replace(value.iter_mut().nth(i).unwrap(), default_part.clone()); + match part { + ast::FStringPart::Literal(ast::StringLiteral { range, - elements: elements.into(), - }; - Some(Box::new(format_spec)) + value, + node_index: _, + flags: _, + }) => elements.push(ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { + node_index: Default::default(), + range, + value, + }, + )), + ast::FStringPart::FString(ast::FString { + elements: fstring_elements, + .. + }) => { + elements.extend(ruff_fstring_element_into_iter(fstring_elements)); + } } } + Some(Box::new(ast::InterpolatedStringFormatSpec { + node_index: Default::default(), + range, + elements: elements.into(), + })) } #[derive(Debug)] pub(super) struct JoinedStr { pub(super) range: TextRange, pub(super) values: Box<[JoinedStrPart]>, + pub(super) runtime_values: Option>>, } impl JoinedStr { - pub(super) fn into_expr(self) -> ast::Expr { - let Self { range, values } = self; + pub(super) fn into_expr(self, from_ast_object: bool) -> ast::Expr { + let Self { + range, + values, + runtime_values: mut raw_runtime_values, + } = self; + let values = if values.iter().any(joined_str_part_requires_runtime_values) { + if raw_runtime_values.is_none() { + raw_runtime_values = Some( + values + .into_vec() + .into_iter() + .map(|part| joined_str_part_to_expr(from_ast_object, part)) + .map(Some) + .collect(), + ); + } + Vec::new().into_boxed_slice() + } else { + values + }; + let (runtime_joined_str, runtime_values) = + raw_runtime_values.take().map_or((None, None), |values| { + if values.iter().any(Option::is_none) { + (None, Some(values)) + } else { + (Some(values.into_iter().flatten().collect()), None) + } + }); ast::Expr::FString(ast::ExprFString { node_index: Default::default(), - range: Default::default(), + range, value: match values.len() { - // ruff represents an empty fstring like this: 0 => ast::FStringValue::single(ast::FString { node_index: Default::default(), range, @@ -332,7 +405,8 @@ impl JoinedStr { }), 1 => ast::FStringValue::single( Box::<[_]>::into_iter(values) - .map(joined_str_part_to_ruff_fstring_element) + .map(|part| joined_str_part_to_ruff_fstring_element(from_ast_object, part)) + .map(Option::unwrap) .map(|element| ast::FString { node_index: Default::default(), range, @@ -344,54 +418,108 @@ impl JoinedStr { ), _ => ast::FStringValue::concatenated( Box::<[_]>::into_iter(values) - .map(joined_str_part_to_ruff_fstring_element) + .map(|part| joined_str_part_to_ruff_fstring_element(from_ast_object, part)) + .map(Option::unwrap) .map(ruff_fstring_element_to_ruff_fstring_part) .collect(), ), }, + runtime_joined_str, + runtime_values, }) } } -fn joined_str_part_to_ruff_fstring_element(part: JoinedStrPart) -> ast::InterpolatedStringElement { +fn joined_str_part_requires_runtime_values(part: &JoinedStrPart) -> bool { + matches!( + part, + JoinedStrPart::Constant(Constant { + value, + .. + }) if !matches!(value, ConstantLiteral::Str { .. }) + ) +} + +fn joined_str_part_to_expr(from_ast_object: bool, part: JoinedStrPart) -> ast::Expr { + match part { + JoinedStrPart::FormattedValue(value) => formatted_value_to_expr(from_ast_object, value), + JoinedStrPart::Constant(value) => value.into_expr(), + } +} + +fn joined_str_part_to_ruff_fstring_element( + from_ast_object: bool, + part: JoinedStrPart, +) -> Option { match part { JoinedStrPart::FormattedValue(value) => { - ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { - node_index: Default::default(), - range: value.range, - expression: value.value.clone(), - debug_text: None, // TODO: What is this? - conversion: value.conversion, - format_spec: joined_str_to_ruff_format_spec(value.format_spec), - }) + let format_spec = value.format_spec.clone(); + let runtime_formatted_value_format_spec = (from_ast_object && format_spec.is_some()) + .then_some(format_spec.clone()) + .flatten(); + Some(ast::InterpolatedStringElement::Interpolation( + ast::InterpolatedElement { + node_index: Default::default(), + range: value.range, + expression: value.value.clone(), + debug_text: None, + conversion: value.conversion, + format_spec: format_spec_expr_to_ruff_format_spec(format_spec), + runtime_str: None, + runtime_interpolation_format_spec: None, + runtime_formatted_value_format_spec, + }, + )) } JoinedStrPart::Constant(value) => { - ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { - node_index: Default::default(), - range: value.range, - value: match value.value { - ConstantLiteral::Str { value, .. } => value, - _ => todo!(), + let Constant { range, value, .. } = value; + let ConstantLiteral::Str { value, .. } = value else { + return None; + }; + Some(ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { + node_index: Default::default(), + range, + value, }, - }) + )) } } } // constructor +pub(super) fn joined_str_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let values: Vec> = + get_node_list_field(vm, source_file, &object, "values", "JoinedStr")?; + Ok(JoinedStr { + values: Vec::new().into_boxed_slice(), + runtime_values: Some(values), + range, + }) +} + impl Node for JoinedStr { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { values, range } = self; + let Self { + values, + runtime_values, + range, + } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprJoinedStr::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item( - "values", - BoxedSlice(values).ast_to_object(vm, source_file), - vm, - ) - .unwrap(); + let values = if let Some(runtime_values) = runtime_values { + BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file) + } else { + BoxedSlice(values).ast_to_object(vm, source_file) + }; + dict.set_item("values", values, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -400,15 +528,8 @@ impl Node for JoinedStr { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let values: BoxedSlice<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "values", "JoinedStr")?, - )?; - Ok(Self { - values: values.0, - range: range_from_object(vm, source_file, object, "JoinedStr")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "JoinedStr")?; + joined_str_from_object_with_range(vm, source_file, object, range) } } @@ -431,8 +552,7 @@ impl Node for JoinedStrPart { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - if cls.is(pyast::NodeExprFormattedValue::static_type()) { + if is_node_instance(vm, &object, pyast::NodeExprFormattedValue::static_type())? { Ok(Self::FormattedValue(Node::ast_from_object( vm, source_file, @@ -452,11 +572,31 @@ impl Node for JoinedStrPart { pub(super) struct FormattedValue { value: Box, conversion: ast::ConversionFlag, - format_spec: Option>, + format_spec: Option>, range: TextRange, } // constructor +pub(super) fn formatted_value_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(FormattedValue { + value: get_required_node_field(vm, source_file, &object, "value", "FormattedValue")?, + conversion: Node::ast_from_object( + vm, + source_file, + get_node_field(vm, &object, "conversion", "FormattedValue")?, + )?, + format_spec: get_node_field_opt(vm, &object, "format_spec")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for FormattedValue { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -487,23 +627,22 @@ impl Node for FormattedValue { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "FormattedValue")?, - )?, - conversion: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "conversion", "FormattedValue")?, - )?, - format_spec: get_node_field_opt(vm, &object, "format_spec")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "FormattedValue")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "FormattedValue")?; + formatted_value_from_object_with_range(vm, source_file, object, range) + } +} + +pub(super) fn formatted_value_to_expr( + from_ast_object: bool, + formatted: FormattedValue, +) -> ast::Expr { + let range = formatted.range; + JoinedStr { + range, + values: vec![JoinedStrPart::FormattedValue(formatted)].into_boxed_slice(), + runtime_values: None, } + .into_expr(from_ast_object) } pub(super) fn fstring_to_object( @@ -515,7 +654,27 @@ pub(super) fn fstring_to_object( range, mut value, node_index: _, + runtime_joined_str, + runtime_values, } = expression; + if let Some(joined_str) = runtime_joined_str { + return JoinedStr { + range, + values: Vec::new().into_boxed_slice(), + runtime_values: Some(joined_str.into_iter().map(Some).collect()), + } + .ast_to_object(vm, source_file); + } + + if let Some(values) = runtime_values { + return JoinedStr { + range, + values: Vec::new().into_boxed_slice(), + runtime_values: Some(values), + } + .ast_to_object(vm, source_file); + } + let default_part = ast::FStringPart::FString(ast::FString { node_index: Default::default(), range: Default::default(), @@ -545,7 +704,7 @@ pub(super) fn fstring_to_object( node_index: _, }) => { for element in ruff_fstring_element_into_iter(elements) { - values.push(ruff_fstring_element_to_joined_str_part(element)); + values.push(ruff_fstring_element_to_joined_str_part(vm, element)); } } } @@ -555,12 +714,13 @@ pub(super) fn fstring_to_object( if let JoinedStrPart::FormattedValue(value) = part && let Some(format_spec) = &value.format_spec { - warn_invalid_escape_sequences_in_format_spec(vm, source_file, format_spec.range); + warn_invalid_escape_sequences_in_format_spec(vm, source_file, format_spec.range()); } } let c = JoinedStr { range, values: values.into_boxed_slice(), + runtime_values: None, }; c.ast_to_object(vm, source_file) } @@ -568,8 +728,9 @@ pub(super) fn fstring_to_object( // ===== TString (Template String) Support ===== fn ruff_tstring_element_to_template_str_part( - element: ast::InterpolatedStringElement, + vm: &VirtualMachine, source_file: &SourceFile, + element: ast::InterpolatedStringElement, ) -> TemplateStrPart { match element { ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { @@ -588,6 +749,9 @@ fn ruff_tstring_element_to_template_str_part( conversion, format_spec, node_index: _, + runtime_str, + runtime_interpolation_format_spec, + runtime_formatted_value_format_spec: _, }) => { let expr_range = extend_expr_range_with_wrapping_parens(source_file, range, expression.range()) @@ -604,11 +768,23 @@ fn ruff_tstring_element_to_template_str_part( } else { tstring_interpolation_expr_str(source_file, range, expr_range) }; + let runtime_interpolation = super::constant::runtime_interpolation_object( + vm, + runtime_str, + runtime_interpolation_format_spec, + ); TemplateStrPart::Interpolation(TStringInterpolation { value: expression, - str: expr_str, + str: runtime_interpolation + .as_ref() + .map_or_else(|| vm.ctx.new_str(expr_str).into(), |(str, _)| str.clone()), conversion, - format_spec: ruff_format_spec_to_joined_str(format_spec), + format_spec: runtime_interpolation + .and_then(|(_, format_spec)| format_spec) + .or_else(|| { + ruff_format_spec_to_joined_str(vm, format_spec) + .map(|joined_str| Box::new(joined_str.into_expr(false))) + }), range, }) } @@ -695,34 +871,51 @@ fn strip_interpolation_expr(expr_source: &str) -> String { pub(super) struct TemplateStr { pub(super) range: TextRange, pub(super) values: Box<[TemplateStrPart]>, + pub(super) runtime_values: Option>>, } pub(super) fn template_str_to_expr( vm: &VirtualMachine, + source_file: &SourceFile, template: TemplateStr, ) -> PyResult { - let TemplateStr { range, values } = template; - let elements = template_parts_to_elements(vm, values)?; + let TemplateStr { + range, + values, + runtime_values: raw_runtime_values, + } = template; + let elements = template_parts_to_elements(vm, source_file, values)?; let tstring = ast::TString { range, node_index: Default::default(), elements, flags: ast::TStringFlags::empty(), }; + let (runtime_template_str, runtime_values) = + raw_runtime_values.map_or((None, None), |values| { + if values.iter().any(Option::is_none) { + (None, Some(values)) + } else { + (Some(values.into_iter().flatten().collect()), None) + } + }); Ok(ast::Expr::TString(ast::ExprTString { node_index: Default::default(), range, value: ast::TStringValue::single(tstring), + runtime_template_str, + runtime_values, })) } pub(super) fn interpolation_to_expr( vm: &VirtualMachine, + source_file: &SourceFile, interpolation: TStringInterpolation, ) -> PyResult { + let range = interpolation.range; let part = TemplateStrPart::Interpolation(interpolation); - let elements = template_parts_to_elements(vm, vec![part].into_boxed_slice())?; - let range = TextRange::default(); + let elements = template_parts_to_elements(vm, source_file, vec![part].into_boxed_slice())?; let tstring = ast::TString { range, node_index: Default::default(), @@ -733,22 +926,26 @@ pub(super) fn interpolation_to_expr( node_index: Default::default(), range, value: ast::TStringValue::single(tstring), + runtime_template_str: None, + runtime_values: None, })) } fn template_parts_to_elements( vm: &VirtualMachine, + source_file: &SourceFile, values: Box<[TemplateStrPart]>, ) -> PyResult { let mut elements = Vec::with_capacity(values.len()); for value in values.into_vec() { - elements.push(template_part_to_element(vm, value)?); + elements.push(template_part_to_element(vm, source_file, value)?); } Ok(ast::InterpolatedStringElements::from(elements)) } fn template_part_to_element( vm: &VirtualMachine, + source_file: &SourceFile, part: TemplateStrPart, ) -> PyResult { match part { @@ -767,12 +964,18 @@ fn template_part_to_element( TemplateStrPart::Interpolation(interpolation) => { let TStringInterpolation { value, + str, conversion, format_spec, range, - .. } = interpolation; - let format_spec = joined_str_to_ruff_format_spec(format_spec); + let str_constant = + super::constant::constant_object_to_constant_data(vm, source_file, str)?; + let runtime_str = Some(super::constant::constant_data_to_ast_constant_value( + str_constant, + )); + let runtime_interpolation_format_spec = format_spec.clone(); + let format_spec = format_spec_expr_to_ruff_format_spec(format_spec); Ok(ast::InterpolatedStringElement::Interpolation( ast::InterpolatedElement { range, @@ -781,6 +984,9 @@ fn template_part_to_element( debug_text: None, conversion, format_spec, + runtime_str, + runtime_interpolation_format_spec, + runtime_formatted_value_format_spec: None, }, )) } @@ -788,19 +994,38 @@ fn template_part_to_element( } // constructor +pub(super) fn template_str_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + let values: Vec> = + get_node_list_field(vm, source_file, &object, "values", "TemplateStr")?; + Ok(TemplateStr { + values: Vec::new().into_boxed_slice(), + runtime_values: Some(values), + range, + }) +} + impl Node for TemplateStr { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { values, range } = self; + let Self { + values, + runtime_values, + range, + } = self; let node = NodeAst .into_ref_with_type(vm, pyast::NodeExprTemplateStr::static_type().to_owned()) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item( - "values", - BoxedSlice(values).ast_to_object(vm, source_file), - vm, - ) - .unwrap(); + let values = if let Some(runtime_values) = runtime_values { + BoxedSlice(runtime_values.into_boxed_slice()).ast_to_object(vm, source_file) + } else { + BoxedSlice(values).ast_to_object(vm, source_file) + }; + dict.set_item("values", values, vm).unwrap(); node_add_location(&dict, range, vm, source_file); node.into() } @@ -809,15 +1034,8 @@ impl Node for TemplateStr { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let values: BoxedSlice<_> = Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "values", "TemplateStr")?, - )?; - Ok(Self { - values: values.0, - range: range_from_object(vm, source_file, object, "TemplateStr")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "TemplateStr")?; + template_str_from_object_with_range(vm, source_file, object, range) } } @@ -840,8 +1058,7 @@ impl Node for TemplateStrPart { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - if cls.is(pyast::NodeExprInterpolation::static_type()) { + if is_node_instance(vm, &object, pyast::NodeExprInterpolation::static_type())? { Ok(Self::Interpolation(Node::ast_from_object( vm, source_file, @@ -860,13 +1077,38 @@ impl Node for TemplateStrPart { #[derive(Debug)] pub(super) struct TStringInterpolation { value: Box, - str: String, + str: PyObjectRef, conversion: ast::ConversionFlag, - format_spec: Option>, + format_spec: Option>, range: TextRange, } // constructor +pub(super) fn tstring_interpolation_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + 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> = get_node_field_opt(vm, &object, "format_spec")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?; + Ok(TStringInterpolation { + value, + str, + conversion, + format_spec, + range, + }) +} + impl Node for TStringInterpolation { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -882,8 +1124,7 @@ impl Node for TStringInterpolation { let dict = node.as_object().dict().unwrap(); dict.set_item("value", value.ast_to_object(vm, source_file), vm) .unwrap(); - dict.set_item("str", vm.ctx.new_str(str).into(), vm) - .unwrap(); + dict.set_item("str", str, vm).unwrap(); dict.set_item("conversion", conversion.ast_to_object(vm, source_file), vm) .unwrap(); dict.set_item( @@ -900,25 +1141,8 @@ impl Node for TStringInterpolation { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let str_obj = get_node_field(vm, &object, "str", "Interpolation")?; - let str_val: String = str_obj.try_into_value(vm)?; - Ok(Self { - value: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "value", "Interpolation")?, - )?, - str: str_val, - conversion: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "conversion", "Interpolation")?, - )?, - format_spec: get_node_field_opt(vm, &object, "format_spec")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - range: range_from_object(vm, source_file, object, "Interpolation")?, - }) + let range = range_from_object(vm, source_file, object.clone(), "Interpolation")?; + tstring_interpolation_from_object_with_range(vm, source_file, object, range) } } @@ -931,7 +1155,42 @@ pub(super) fn tstring_to_object( range, mut value, node_index: _, + runtime_template_str, + runtime_values, } = expression; + if let Some(template_str) = runtime_template_str { + return TemplateStr { + range, + values: Vec::new().into_boxed_slice(), + runtime_values: Some(template_str.into_iter().map(Some).collect()), + } + .ast_to_object(vm, source_file); + } + + if let Some(values) = runtime_values { + return TemplateStr { + range, + values: Vec::new().into_boxed_slice(), + runtime_values: Some(values), + } + .ast_to_object(vm, source_file); + } + + if let [tstring] = value.as_slice() + && let Some(ast::InterpolatedStringElement::Interpolation(interp)) = + tstring.elements.iter().next() + && tstring.elements.get(1).is_none() + && let Some((str, format_spec)) = super::constant::runtime_interpolation_object( + vm, + interp.runtime_str.clone(), + interp.runtime_interpolation_format_spec.clone(), + ) + && let Some(interpolation) = + standalone_tstring_interpolation_to_object(vm, source_file, &value, str, format_spec) + { + return interpolation; + } + let default_tstring = ast::TString { node_index: Default::default(), range: Default::default(), @@ -943,8 +1202,9 @@ pub(super) fn tstring_to_object( let tstring = core::mem::replace(value.iter_mut().nth(i).unwrap(), default_tstring.clone()); for element in ruff_fstring_element_into_iter(tstring.elements) { values.push(ruff_tstring_element_to_template_str_part( - element, + vm, source_file, + element, )); } } @@ -952,6 +1212,37 @@ pub(super) fn tstring_to_object( let c = TemplateStr { range, values: values.into_boxed_slice(), + runtime_values: None, }; c.ast_to_object(vm, source_file) } + +fn standalone_tstring_interpolation_to_object( + vm: &VirtualMachine, + source_file: &SourceFile, + value: &ast::TStringValue, + str: PyObjectRef, + format_spec: Option>, +) -> Option { + 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)) +} diff --git a/crates/vm/src/stdlib/_ast/type_ignore.rs b/crates/vm/src/stdlib/_ast/type_ignore.rs index 6e90ba9b80e..d51e54f1c5d 100644 --- a/crates/vm/src/stdlib/_ast/type_ignore.rs +++ b/crates/vm/src/stdlib/_ast/type_ignore.rs @@ -2,6 +2,7 @@ use super::*; use rustpython_compiler_core::SourceFile; pub(super) enum TypeIgnore { + None, TypeIgnore(TypeIgnoreTypeIgnore), } @@ -9,6 +10,7 @@ pub(super) enum TypeIgnore { impl Node for TypeIgnore { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { match self { + Self::None => vm.ctx.none(), Self::TypeIgnore(cons) => cons.ast_to_object(vm, source_file), } } @@ -17,8 +19,9 @@ impl Node for TypeIgnore { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeTypeIgnoreTypeIgnore::static_type()) { + Ok(if vm.is_none(&object) { + Self::None + } else if is_node_instance(vm, &object, pyast::NodeTypeIgnoreTypeIgnore::static_type())? { Self::TypeIgnore(TypeIgnoreTypeIgnore::ast_from_object( vm, source_file, @@ -34,15 +37,14 @@ impl Node for TypeIgnore { } pub(super) struct TypeIgnoreTypeIgnore { - range: TextRange, - lineno: PyRefExact, - tag: PyRefExact, + lineno: i32, + tag: PyObjectRef, } // constructor impl Node for TypeIgnoreTypeIgnore { - fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - let Self { lineno, tag, range } = self; + fn ast_to_object(self, vm: &VirtualMachine, _source_file: &SourceFile) -> PyObjectRef { + let Self { lineno, tag } = self; let node = NodeAst .into_ref_with_type( vm, @@ -50,25 +52,20 @@ impl Node for TypeIgnoreTypeIgnore { ) .unwrap(); let dict = node.as_object().dict().unwrap(); - dict.set_item("lineno", lineno.to_pyobject(vm), vm).unwrap(); - dict.set_item("tag", tag.to_pyobject(vm), vm).unwrap(); - node_add_location(&dict, range, vm, source_file); + dict.set_item("lineno", vm.ctx.new_int(lineno).into(), vm) + .unwrap(); + dict.set_item("tag", tag, vm).unwrap(); node.into() } fn ast_from_object( vm: &VirtualMachine, - source_file: &SourceFile, + _source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { Ok(Self { - lineno: get_node_field(vm, &object, "lineno", "TypeIgnore")? - .downcast_exact(vm) - .unwrap(), - tag: get_node_field(vm, &object, "tag", "TypeIgnore")? - .downcast_exact(vm) - .unwrap(), - range: range_from_object(vm, source_file, object, "TypeIgnore")?, + lineno: get_int_field(vm, &object, "lineno", "TypeIgnore")?, + tag: node_object_to_ast_string(vm, get_node_field(vm, &object, "tag", "TypeIgnore")?)?, }) } } diff --git a/crates/vm/src/stdlib/_ast/type_parameters.rs b/crates/vm/src/stdlib/_ast/type_parameters.rs index 0424ffbd768..8f2296ea76a 100644 --- a/crates/vm/src/stdlib/_ast/type_parameters.rs +++ b/crates/vm/src/stdlib/_ast/type_parameters.rs @@ -3,7 +3,10 @@ use rustpython_compiler_core::SourceFile; impl Node for ast::TypeParams { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { - self.type_params.ast_to_object(vm, source_file) + self.runtime_type_params.map_or_else( + || self.type_params.ast_to_object(vm, source_file), + |values| values.ast_to_object(vm, source_file), + ) } fn ast_from_object( @@ -11,22 +14,51 @@ impl Node for ast::TypeParams { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let type_params: Vec = Node::ast_from_object(vm, source_file, object)?; - let range = Option::zip(type_params.first(), type_params.last()) - .map(|(first, last)| first.range().cover(last.range())) - .unwrap_or_default(); - Ok(Self { - node_index: Default::default(), - type_params, - range, - }) + Ok(type_params_from_values( + vm, + Node::ast_from_object(vm, source_file, object)?, + )) } fn is_none(&self) -> bool { - self.type_params.is_empty() + self.type_params.is_empty() && self.runtime_type_params.is_none() + } +} + +pub(super) fn type_params_from_field( + vm: &VirtualMachine, + source_file: &SourceFile, + object: &PyObject, + field: &'static str, + typ: &str, +) -> PyResult>> { + let type_params: Vec> = + 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>, +) -> 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]) -> Vec { + values.iter().filter_map(Clone::clone).collect() +} + // sum impl Node for ast::TypeParam { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { @@ -42,35 +74,70 @@ impl Node for ast::TypeParam { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - let cls = object.class(); - Ok(if cls.is(pyast::NodeTypeParamTypeVar::static_type()) { - Self::TypeVar(ast::TypeParamTypeVar::ast_from_object( - vm, - source_file, - object, - )?) - } else if cls.is(pyast::NodeTypeParamParamSpec::static_type()) { - Self::ParamSpec(ast::TypeParamParamSpec::ast_from_object( + if vm.is_none(&object) { + return Err(vm.new_type_error(format!( + "expected some sort of type_param, but got {}", + object.repr(vm)? + ))); + } + enum TypeParamKind { + TypeVar, + ParamSpec, + TypeVarTuple, + } + let kind = if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVar::static_type())? { + TypeParamKind::TypeVar + } else if is_node_instance(vm, &object, pyast::NodeTypeParamParamSpec::static_type())? { + TypeParamKind::ParamSpec + } else if is_node_instance(vm, &object, pyast::NodeTypeParamTypeVarTuple::static_type())? { + TypeParamKind::TypeVarTuple + } else { + return Err(vm.new_type_error(format!( + "expected some sort of type_param, but got {}", + object.repr(vm)? + ))); + }; + let range = type_param_range_from_object(vm, source_file, object.clone())?; + Ok(match kind { + TypeParamKind::TypeVar => Self::TypeVar(type_var_from_object_with_range( vm, source_file, object, - )?) - } else if cls.is(pyast::NodeTypeParamTypeVarTuple::static_type()) { - Self::TypeVarTuple(ast::TypeParamTypeVarTuple::ast_from_object( + range, + )?), + TypeParamKind::ParamSpec => Self::ParamSpec(param_spec_from_object_with_range( vm, source_file, object, - )?) - } else { - return Err(vm.new_type_error(format!( - "expected some sort of type_param, but got {}", - object.repr(vm)? - ))); + range, + )?), + TypeParamKind::TypeVarTuple => Self::TypeVarTuple( + type_var_tuple_from_object_with_range(vm, source_file, object, range)?, + ), }) } } // constructor +fn type_var_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::TypeParamTypeVar { + node_index: Default::default(), + name: get_required_identifier_field(vm, source_file, &object, "name", "TypeVar")?, + bound: get_node_field_opt(vm, &object, "bound")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + default: get_node_field_opt(vm, &object, "default_value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::TypeParamTypeVar { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -99,27 +166,28 @@ impl Node for ast::TypeParamTypeVar { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "name", "TypeVar")?, - )?, - bound: get_node_field_opt(vm, &object, "bound")? - .map(|obj| Node::ast_from_object(vm, source_file, obj)) - .transpose()?, - default: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "default_value", "TypeVar")?, - )?, - range: range_from_object(vm, source_file, object, "TypeVar")?, - }) + let range = type_param_range_from_object(vm, source_file, object.clone())?; + type_var_from_object_with_range(vm, source_file, object, range) } } // constructor +fn param_spec_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::TypeParamParamSpec { + node_index: Default::default(), + name: get_required_identifier_field(vm, source_file, &object, "name", "ParamSpec")?, + default: get_node_field_opt(vm, &object, "default_value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::TypeParamParamSpec { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -145,24 +213,28 @@ impl Node for ast::TypeParamParamSpec { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "name", "ParamSpec")?, - )?, - default: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "default_value", "ParamSpec")?, - )?, - range: range_from_object(vm, source_file, object, "ParamSpec")?, - }) + let range = type_param_range_from_object(vm, source_file, object.clone())?; + param_spec_from_object_with_range(vm, source_file, object, range) } } // constructor +fn type_var_tuple_from_object_with_range( + vm: &VirtualMachine, + source_file: &SourceFile, + object: PyObjectRef, + range: TextRange, +) -> PyResult { + Ok(ast::TypeParamTypeVarTuple { + node_index: Default::default(), + name: get_required_identifier_field(vm, source_file, &object, "name", "TypeVarTuple")?, + default: get_node_field_opt(vm, &object, "default_value")? + .map(|obj| Node::ast_from_object(vm, source_file, obj)) + .transpose()?, + range, + }) +} + impl Node for ast::TypeParamTypeVarTuple { fn ast_to_object(self, vm: &VirtualMachine, source_file: &SourceFile) -> PyObjectRef { let Self { @@ -191,19 +263,7 @@ impl Node for ast::TypeParamTypeVarTuple { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - Ok(Self { - node_index: Default::default(), - name: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "name", "TypeVarTuple")?, - )?, - default: Node::ast_from_object( - vm, - source_file, - get_node_field(vm, &object, "default_value", "TypeVarTuple")?, - )?, - range: range_from_object(vm, source_file, object, "TypeVarTuple")?, - }) + let range = type_param_range_from_object(vm, source_file, object.clone())?; + type_var_tuple_from_object_with_range(vm, source_file, object, range) } } diff --git a/crates/vm/src/stdlib/_ast/validate.rs b/crates/vm/src/stdlib/_ast/validate.rs index cad37d38610..e936c9d3fb7 100644 --- a/crates/vm/src/stdlib/_ast/validate.rs +++ b/crates/vm/src/stdlib/_ast/validate.rs @@ -1,8 +1,10 @@ // spell-checker: ignore assignlist ifexp use super::module::Mod; -use crate::{PyResult, VirtualMachine}; +use crate::{PyResult, VirtualMachine, compiler::CompileError}; use ruff_python_ast as ast; +use rustpython_codegen::error::{CodegenError, CodegenErrorType}; +use rustpython_compiler_core::bytecode::ConstantData; fn expr_context_name(ctx: ast::ExprContext) -> &'static str { match ctx { @@ -13,6 +15,17 @@ fn expr_context_name(ctx: ast::ExprContext) -> &'static str { } } +fn invalid_syntax_error(vm: &VirtualMachine) -> crate::builtins::PyBaseExceptionRef { + vm.new_syntax_error( + &CompileError::Codegen(CodegenError { + location: None, + error: CodegenErrorType::SyntaxError("invalid syntax".to_owned()), + source_path: "".to_owned(), + }), + None, + ) +} + fn validate_name(vm: &VirtualMachine, name: &ast::name::Name) -> PyResult<()> { match name.as_str() { "None" | "True" | "False" => Err(vm.new_value_error(format!( @@ -30,6 +43,7 @@ fn validate_comprehension(vm: &VirtualMachine, gens: &[ast::Comprehension]) -> P for comp in gens { validate_expr(vm, &comp.target, ast::ExprContext::Store)?; validate_expr(vm, &comp.iter, ast::ExprContext::Load)?; + validate_runtime_expr_list_slots(vm, comp.runtime_ifs.as_ref(), ast::ExprContext::Load)?; validate_exprs(vm, &comp.ifs, ast::ExprContext::Load, false)?; } Ok(()) @@ -42,30 +56,52 @@ fn validate_keywords(vm: &VirtualMachine, keywords: &[ast::Keyword]) -> PyResult Ok(()) } +pub(super) fn validate_parameter_annotation( + vm: &VirtualMachine, + parameter: &ast::Parameter, +) -> PyResult<()> { + if let Some(annotation) = ¶meter.annotation { + validate_expr(vm, annotation, ast::ExprContext::Load)?; + } + Ok(()) +} + fn validate_parameters(vm: &VirtualMachine, params: &ast::Parameters) -> PyResult<()> { - for param in params - .posonlyargs - .iter() - .chain(¶ms.args) - .chain(¶ms.kwonlyargs) - { - if let Some(annotation) = ¶m.parameter.annotation { - validate_expr(vm, annotation, ast::ExprContext::Load)?; - } - if let Some(default) = ¶m.default { - validate_expr(vm, default, ast::ExprContext::Load)?; - } + for param in params.posonlyargs.iter().chain(¶ms.args) { + validate_parameter_annotation(vm, ¶m.parameter)?; } if let Some(vararg) = ¶ms.vararg && let Some(annotation) = &vararg.annotation { validate_expr(vm, annotation, ast::ExprContext::Load)?; } + for param in ¶ms.kwonlyargs { + validate_parameter_annotation(vm, ¶m.parameter)?; + } if let Some(kwarg) = ¶ms.kwarg && let Some(annotation) = &kwarg.annotation { validate_expr(vm, annotation, ast::ExprContext::Load)?; } + if let Some(defaults) = params.runtime_defaults.as_ref() { + for default in defaults { + let Some(default) = default else { + return Err(vm.new_value_error("None disallowed in expression list")); + }; + validate_expr(vm, default, ast::ExprContext::Load)?; + } + } else { + for param in params.posonlyargs.iter().chain(¶ms.args) { + if let Some(default) = ¶m.default { + validate_expr(vm, default, ast::ExprContext::Load)?; + } + } + } + for param in ¶ms.kwonlyargs { + if let Some(default) = ¶m.default { + validate_expr(vm, default, ast::ExprContext::Load)?; + } + } Ok(()) } @@ -99,8 +135,14 @@ fn validate_assignlist( validate_exprs(vm, targets, ctx, false) } -fn validate_body(vm: &VirtualMachine, body: &[ast::Stmt], owner: &'static str) -> PyResult<()> { +fn validate_body( + vm: &VirtualMachine, + body: &[ast::Stmt], + metadata: Option<&Vec>>, + owner: &'static str, +) -> PyResult<()> { validate_nonempty_seq(vm, body.len(), "body", owner)?; + validate_runtime_stmt_list_slots(vm, metadata)?; validate_stmts(vm, body) } @@ -111,34 +153,83 @@ fn validate_interpolated_elements<'a>( for element in elements { if let ast::InterpolatedStringElementRef::Interpolation(interpolation) = element { validate_expr(vm, &interpolation.expression, ast::ExprContext::Load)?; - if let Some(format_spec) = &interpolation.format_spec { - for spec_element in &format_spec.elements { - if let ast::InterpolatedStringElement::Interpolation(spec_interp) = spec_element - { - validate_expr(vm, &spec_interp.expression, ast::ExprContext::Load)?; - } - } + if let Some(format_spec) = interpolation.runtime_formatted_value_format_spec.as_deref() + { + validate_expr(vm, format_spec, ast::ExprContext::Load)?; + } else if let Some(format_spec) = + interpolation.runtime_interpolation_format_spec.as_deref() + { + validate_expr(vm, format_spec, ast::ExprContext::Load)?; + } else if let Some(format_spec) = &interpolation.format_spec { + validate_interpolated_elements( + vm, + format_spec + .elements + .iter() + .map(ast::InterpolatedStringElementRef::from), + )?; } } } Ok(()) } +fn ensure_literal_number(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool { + let ast::Expr::NumberLiteral(number) = expr else { + return false; + }; + match number.value { + ast::Number::Int(_) | ast::Number::Float(_) => allow_real, + ast::Number::Complex { .. } => allow_imaginary, + } +} + +fn ensure_literal_negative(expr: &ast::Expr, allow_real: bool, allow_imaginary: bool) -> bool { + let ast::Expr::UnaryOp(unary) = expr else { + return false; + }; + if unary.op != ast::UnaryOp::USub { + return false; + } + ensure_literal_number(&unary.operand, allow_real, allow_imaginary) +} + +fn ensure_literal_complex(expr: &ast::Expr) -> bool { + let ast::Expr::BinOp(bin) = expr else { + return false; + }; + if !matches!(bin.op, ast::Operator::Add | ast::Operator::Sub) { + return false; + } + let real_left = ensure_literal_number(&bin.left, true, false) + || ensure_literal_negative(&bin.left, true, false); + real_left && ensure_literal_number(&bin.right, false, true) +} + +fn ast_constant_value(expr: &ast::Expr) -> Option { + expr.as_constant_expr() + .map(|expr| super::constant::ast_constant_value_to_constant_data(expr.value.clone())) +} + fn validate_pattern_match_value(vm: &VirtualMachine, expr: &ast::Expr) -> PyResult<()> { validate_expr(vm, expr, ast::ExprContext::Load)?; + if let Some(constant) = ast_constant_value(expr) { + return match &constant { + ConstantData::Integer { .. } + | ConstantData::Float { .. } + | ConstantData::Bytes { .. } + | ConstantData::Complex { .. } + | ConstantData::Str { .. } => Ok(()), + _ => Err(vm.new_value_error("unexpected constant inside of a literal pattern")), + }; + } match expr { ast::Expr::NumberLiteral(_) | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) => { Ok(()) } ast::Expr::Attribute(_) => Ok(()), - ast::Expr::UnaryOp(op) => match &*op.operand { - ast::Expr::NumberLiteral(_) => Ok(()), - _ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")), - }, - ast::Expr::BinOp(bin) => match (&*bin.left, &*bin.right) { - (ast::Expr::NumberLiteral(_), ast::Expr::NumberLiteral(_)) => Ok(()), - _ => Err(vm.new_value_error("patterns may only match literals and attribute lookups")), - }, + ast::Expr::UnaryOp(_) if ensure_literal_negative(expr, true, true) => Ok(()), + ast::Expr::BinOp(_) if ensure_literal_complex(expr) => Ok(()), ast::Expr::FString(_) | ast::Expr::TString(_) => Ok(()), ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) @@ -162,7 +253,10 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool) ast::Pattern::MatchSingleton(singleton) => match singleton.value { ast::Singleton::None | ast::Singleton::True | ast::Singleton::False => Ok(()), }, - ast::Pattern::MatchSequence(seq) => validate_patterns(vm, &seq.patterns, true), + ast::Pattern::MatchSequence(seq) => { + validate_runtime_pattern_list_slots(vm, seq.runtime_patterns.as_ref())?; + validate_patterns(vm, &seq.patterns, true) + } ast::Pattern::MatchMapping(mapping) => { if mapping.keys.len() != mapping.patterns.len() { return Err(vm.new_value_error( @@ -172,15 +266,34 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool) if let Some(rest) = &mapping.rest { validate_capture(vm, rest)?; } + validate_runtime_expr_option_list_slots(vm, mapping.runtime_keys.as_ref())?; for key in &mapping.keys { - if let ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) = key { + if matches!( + key, + ast::Expr::BooleanLiteral(_) + | ast::Expr::NoneLiteral(_) + | ast::Expr::Constant(ast::ExprConstant { + value: ast::ConstantValue::Boolean(_) | ast::ConstantValue::None, + .. + }) + ) { continue; } validate_pattern_match_value(vm, key)?; } + validate_runtime_pattern_list_slots(vm, mapping.runtime_patterns.as_ref())?; validate_patterns(vm, &mapping.patterns, false) } ast::Pattern::MatchClass(match_class) => { + if let (Some(kwd_attrs), Some(kwd_patterns)) = ( + match_class.runtime_kwd_attrs.as_ref(), + match_class.runtime_kwd_patterns.as_ref(), + ) && kwd_attrs.len() != kwd_patterns.len() + { + return Err(vm.new_value_error( + "MatchClass doesn't have the same number of keyword attributes as patterns", + )); + } validate_expr(vm, &match_class.cls, ast::ExprContext::Load)?; let mut cls = match_class.cls.as_ref(); loop { @@ -199,7 +312,13 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool) for keyword in &match_class.arguments.keywords { validate_name(vm, keyword.attr.id())?; } + if let Some(patterns) = &match_class.runtime_patterns { + validate_runtime_nullable_patterns(vm, patterns)?; + } validate_patterns(vm, &match_class.arguments.patterns, false)?; + if let Some(kwd_patterns) = &match_class.runtime_kwd_patterns { + validate_runtime_nullable_patterns(vm, kwd_patterns)?; + } for keyword in &match_class.arguments.keywords { validate_pattern(vm, &keyword.pattern, false)?; } @@ -234,11 +353,80 @@ fn validate_pattern(vm: &VirtualMachine, pattern: &ast::Pattern, star_ok: bool) if match_or.patterns.len() < 2 { return Err(vm.new_value_error("MatchOr requires at least 2 patterns")); } + validate_runtime_pattern_list_slots(vm, match_or.runtime_patterns.as_ref())?; validate_patterns(vm, &match_or.patterns, false) } } } +fn validate_runtime_pattern_list_slots( + vm: &VirtualMachine, + values: Option<&Vec>>, +) -> 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>>, +) -> 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>>, + 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>>, +) -> 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>>, +) -> 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], +) -> PyResult<()> { + if patterns.iter().any(Option::is_none) { + return Err(vm.new_value_error("unexpected pattern")); + } + Ok(()) +} + fn validate_patterns( vm: &VirtualMachine, patterns: &[ast::Pattern], @@ -282,6 +470,12 @@ fn validate_type_params( type_params: Option<&ast::TypeParams>, ) -> PyResult<()> { if let Some(type_params) = type_params { + if let Some(values) = type_params.runtime_type_params.as_ref() { + for tp in values.iter().flatten() { + validate_typeparam(vm, tp)?; + } + return Ok(()); + } for tp in &type_params.type_params { validate_typeparam(vm, tp)?; } @@ -337,6 +531,11 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - if op.values.len() < 2 { return Err(vm.new_value_error("BoolOp with less than 2 values")); } + validate_runtime_expr_list_slots( + vm, + op.runtime_values.as_ref(), + ast::ExprContext::Load, + )?; validate_exprs(vm, &op.values, ast::ExprContext::Load, false) } ast::Expr::Named(named) => { @@ -362,6 +561,11 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - validate_expr(vm, &ifexp.orelse, ast::ExprContext::Load) } ast::Expr::Dict(dict) => { + validate_runtime_expr_list_slots( + vm, + dict.runtime_values.as_ref(), + ast::ExprContext::Load, + )?; for item in &dict.items { if let Some(key) = &item.key { validate_expr(vm, key, ast::ExprContext::Load)?; @@ -370,7 +574,14 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - } Ok(()) } - ast::Expr::Set(set) => validate_exprs(vm, &set.elts, ast::ExprContext::Load, false), + ast::Expr::Set(set) => { + validate_runtime_expr_list_slots( + vm, + set.runtime_elts.as_ref(), + ast::ExprContext::Load, + )?; + validate_exprs(vm, &set.elts, ast::ExprContext::Load, false) + } ast::Expr::ListComp(list) => { validate_comprehension(vm, &list.generators)?; validate_expr(vm, &list.elt, ast::ExprContext::Load) @@ -409,34 +620,73 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - "Compare has a different number of comparators and operands", )); } + validate_runtime_expr_list_slots( + vm, + compare.runtime_comparators.as_ref(), + ast::ExprContext::Load, + )?; validate_exprs(vm, &compare.comparators, ast::ExprContext::Load, false)?; validate_expr(vm, &compare.left, ast::ExprContext::Load) } ast::Expr::Call(call) => { validate_expr(vm, &call.func, ast::ExprContext::Load)?; + validate_runtime_expr_list_slots( + vm, + call.arguments.runtime_args.as_ref(), + ast::ExprContext::Load, + )?; validate_exprs(vm, &call.arguments.args, ast::ExprContext::Load, false)?; validate_keywords(vm, &call.arguments.keywords) } - ast::Expr::FString(fstring) => validate_interpolated_elements( - vm, - fstring - .value - .elements() - .map(ast::InterpolatedStringElementRef::from), - ), - ast::Expr::TString(tstring) => validate_interpolated_elements( - vm, - tstring - .value - .elements() - .map(ast::InterpolatedStringElementRef::from), - ), + ast::Expr::FString(fstring) => { + validate_runtime_expr_list_slots( + vm, + fstring.runtime_values.as_ref(), + ast::ExprContext::Load, + )?; + if let Some(joined_str) = fstring.runtime_joined_str.as_ref() { + validate_exprs(vm, joined_str, ast::ExprContext::Load, false) + } else { + validate_interpolated_elements( + vm, + fstring + .value + .elements() + .map(ast::InterpolatedStringElementRef::from), + ) + } + } + ast::Expr::TString(tstring) => { + validate_runtime_expr_list_slots( + vm, + tstring.runtime_values.as_ref(), + ast::ExprContext::Load, + )?; + if let Some(template_str) = tstring.runtime_template_str.as_ref() { + validate_exprs(vm, template_str, ast::ExprContext::Load, false) + } else { + validate_interpolated_elements( + vm, + tstring + .value + .elements() + .map(ast::InterpolatedStringElementRef::from), + ) + } + } ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) | ast::Expr::NumberLiteral(_) + | ast::Expr::Constant(_) | ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_) - | ast::Expr::EllipsisLiteral(_) => Ok(()), + | ast::Expr::EllipsisLiteral(_) => { + if let Some(invalid_type) = super::constant::invalid_constant_type(expr) { + Err(vm.new_type_error(format!("got an invalid type in Constant: {invalid_type}"))) + } else { + Ok(()) + } + } ast::Expr::Attribute(attr) => validate_expr(vm, &attr.value, ast::ExprContext::Load), ast::Expr::Subscript(sub) => { validate_expr(vm, &sub.slice, ast::ExprContext::Load)?; @@ -444,8 +694,14 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - } ast::Expr::Starred(star) => validate_expr(vm, &star.value, ctx), ast::Expr::Name(_) => Ok(()), - ast::Expr::List(list) => validate_exprs(vm, &list.elts, ctx, false), - ast::Expr::Tuple(tuple) => validate_exprs(vm, &tuple.elts, ctx, false), + ast::Expr::List(list) => { + validate_runtime_expr_list_slots(vm, list.runtime_elts.as_ref(), ctx)?; + validate_exprs(vm, &list.elts, ctx, false) + } + ast::Expr::Tuple(tuple) => { + validate_runtime_expr_list_slots(vm, tuple.runtime_elts.as_ref(), ctx)?; + validate_exprs(vm, &tuple.elts, ctx, false) + } ast::Expr::Slice(slice) => { if let Some(lower) = &slice.lower { validate_expr(vm, lower, ast::ExprContext::Load)?; @@ -458,7 +714,7 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) - } Ok(()) } - ast::Expr::IpyEscapeCommand(_) => Ok(()), + ast::Expr::IpyEscapeCommand(_) => Err(invalid_syntax_error(vm)), } } @@ -477,9 +733,14 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { } else { "FunctionDef" }; - validate_body(vm, &func.body, owner)?; + validate_body(vm, &func.body, func.runtime_body.as_ref(), owner)?; validate_type_params(vm, func.type_params.as_deref())?; validate_parameters(vm, &func.parameters)?; + validate_runtime_expr_list_slots( + vm, + func.runtime_decorator_list.as_ref(), + ast::ExprContext::Load, + )?; validate_decorators(vm, &func.decorator_list)?; if let Some(returns) = &func.returns { validate_expr(vm, returns, ast::ExprContext::Load)?; @@ -487,12 +748,27 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { Ok(()) } ast::Stmt::ClassDef(class_def) => { - validate_body(vm, &class_def.body, "ClassDef")?; + validate_body( + vm, + &class_def.body, + class_def.runtime_body.as_ref(), + "ClassDef", + )?; validate_type_params(vm, class_def.type_params.as_deref())?; if let Some(arguments) = &class_def.arguments { + validate_runtime_expr_list_slots( + vm, + arguments.runtime_bases.as_ref(), + ast::ExprContext::Load, + )?; validate_exprs(vm, &arguments.args, ast::ExprContext::Load, false)?; validate_keywords(vm, &arguments.keywords)?; } + validate_runtime_expr_list_slots( + vm, + class_def.runtime_decorator_list.as_ref(), + ast::ExprContext::Load, + )?; validate_decorators(vm, &class_def.decorator_list) } ast::Stmt::Return(ret) => { @@ -501,8 +777,20 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { } Ok(()) } - ast::Stmt::Delete(del) => validate_assignlist(vm, &del.targets, ast::ExprContext::Del), + ast::Stmt::Delete(del) => { + validate_runtime_expr_list_slots( + vm, + del.runtime_targets.as_ref(), + ast::ExprContext::Del, + )?; + validate_assignlist(vm, &del.targets, ast::ExprContext::Del) + } ast::Stmt::Assign(assign) => { + validate_runtime_expr_list_slots( + vm, + assign.runtime_targets.as_ref(), + ast::ExprContext::Store, + )?; validate_assignlist(vm, &assign.targets, ast::ExprContext::Store)?; validate_expr(vm, &assign.value, ast::ExprContext::Load) } @@ -532,22 +820,30 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { let owner = if for_stmt.is_async { "AsyncFor" } else { "For" }; validate_expr(vm, &for_stmt.target, ast::ExprContext::Store)?; validate_expr(vm, &for_stmt.iter, ast::ExprContext::Load)?; - validate_body(vm, &for_stmt.body, owner)?; + validate_body(vm, &for_stmt.body, for_stmt.runtime_body.as_ref(), owner)?; + validate_runtime_stmt_list_slots(vm, for_stmt.runtime_orelse.as_ref())?; validate_stmts(vm, &for_stmt.orelse) } ast::Stmt::While(while_stmt) => { validate_expr(vm, &while_stmt.test, ast::ExprContext::Load)?; - validate_body(vm, &while_stmt.body, "While")?; + validate_body( + vm, + &while_stmt.body, + while_stmt.runtime_body.as_ref(), + "While", + )?; + validate_runtime_stmt_list_slots(vm, while_stmt.runtime_orelse.as_ref())?; validate_stmts(vm, &while_stmt.orelse) } ast::Stmt::If(if_stmt) => { validate_expr(vm, &if_stmt.test, ast::ExprContext::Load)?; - validate_body(vm, &if_stmt.body, "If")?; + validate_body(vm, &if_stmt.body, if_stmt.runtime_body.as_ref(), "If")?; for clause in &if_stmt.elif_else_clauses { if let Some(test) = &clause.test { validate_expr(vm, test, ast::ExprContext::Load)?; } - validate_body(vm, &clause.body, "If")?; + validate_body(vm, &clause.body, clause.runtime_body.as_ref(), "If")?; + validate_runtime_stmt_list_slots(vm, clause.runtime_orelse.as_ref())?; } Ok(()) } @@ -564,7 +860,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { validate_expr(vm, optional_vars, ast::ExprContext::Store)?; } } - validate_body(vm, &with_stmt.body, owner) + validate_body(vm, &with_stmt.body, with_stmt.runtime_body.as_ref(), owner) } ast::Stmt::Match(match_stmt) => { validate_expr(vm, &match_stmt.subject, ast::ExprContext::Load)?; @@ -574,7 +870,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { if let Some(guard) = &case.guard { validate_expr(vm, guard, ast::ExprContext::Load)?; } - validate_body(vm, &case.body, "match_case")?; + validate_body(vm, &case.body, case.runtime_body.as_ref(), "match_case")?; } Ok(()) } @@ -591,7 +887,7 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { } ast::Stmt::Try(try_stmt) => { let owner = if try_stmt.is_star { "TryStar" } else { "Try" }; - validate_body(vm, &try_stmt.body, owner)?; + validate_body(vm, &try_stmt.body, try_stmt.runtime_body.as_ref(), owner)?; if try_stmt.handlers.is_empty() && try_stmt.finalbody.is_empty() { return Err(vm.new_value_error(format!( "{owner} has neither except handlers nor finalbody" @@ -602,14 +898,22 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { vm.new_value_error(format!("{owner} has orelse but no except handlers")) ); } + validate_runtime_except_handler_list_slots(vm, try_stmt.runtime_handlers.as_ref())?; for handler in &try_stmt.handlers { let ast::ExceptHandler::ExceptHandler(handler) = handler; if let Some(type_expr) = &handler.type_ { validate_expr(vm, type_expr, ast::ExprContext::Load)?; } - validate_body(vm, &handler.body, "ExceptHandler")?; - } + validate_body( + vm, + &handler.body, + handler.runtime_body.as_ref(), + "ExceptHandler", + )?; + } + validate_runtime_stmt_list_slots(vm, try_stmt.runtime_finalbody.as_ref())?; validate_stmts(vm, &try_stmt.finalbody)?; + validate_runtime_stmt_list_slots(vm, try_stmt.runtime_orelse.as_ref())?; validate_stmts(vm, &try_stmt.orelse) } ast::Stmt::Assert(assert_stmt) => { @@ -624,6 +928,11 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { Ok(()) } ast::Stmt::ImportFrom(import) => { + if let Some(level) = import.runtime_level + && level < 0 + { + return Err(vm.new_value_error("Negative ImportFrom level")); + } validate_nonempty_seq(vm, import.names.len(), "names", "ImportFrom")?; Ok(()) } @@ -636,10 +945,8 @@ fn validate_stmt(vm: &VirtualMachine, stmt: &ast::Stmt) -> PyResult<()> { Ok(()) } ast::Stmt::Expr(expr) => validate_expr(vm, &expr.value, ast::ExprContext::Load), - ast::Stmt::Pass(_) - | ast::Stmt::Break(_) - | ast::Stmt::Continue(_) - | ast::Stmt::IpyEscapeCommand(_) => Ok(()), + ast::Stmt::Pass(_) | ast::Stmt::Break(_) | ast::Stmt::Continue(_) => Ok(()), + ast::Stmt::IpyEscapeCommand(_) => Err(invalid_syntax_error(vm)), } } @@ -652,10 +959,17 @@ fn validate_stmts(vm: &VirtualMachine, stmts: &[ast::Stmt]) -> PyResult<()> { pub(super) fn validate_mod(vm: &VirtualMachine, module: &Mod) -> PyResult<()> { match module { - Mod::Module(module) => validate_stmts(vm, &module.body), - Mod::Interactive(module) => validate_stmts(vm, &module.body), + Mod::Module(module) => { + validate_runtime_stmt_list_slots(vm, module.module.runtime_body.as_ref())?; + validate_stmts(vm, &module.module.body) + } + Mod::Interactive(module) => { + validate_runtime_stmt_list_slots(vm, module.runtime_body.as_ref())?; + validate_stmts(vm, &module.body) + } Mod::Expression(expr) => validate_expr(vm, &expr.body, ast::ExprContext::Load), Mod::FunctionType(func_type) => { + validate_runtime_expr_option_list_slots(vm, func_type.runtime_argtypes.as_ref())?; validate_exprs(vm, &func_type.argtypes, ast::ExprContext::Load, false)?; validate_expr(vm, &func_type.returns, ast::ExprContext::Load) } diff --git a/crates/vm/src/stdlib/_codecs.rs b/crates/vm/src/stdlib/_codecs.rs index a9402edc3a2..8f6ea5f1900 100644 --- a/crates/vm/src/stdlib/_codecs.rs +++ b/crates/vm/src/stdlib/_codecs.rs @@ -6,6 +6,8 @@ use crate::common::static_cell::StaticCell; #[pymodule(with(#[cfg(windows)] _codecs_windows))] mod _codecs { + use core::hint::cold_path; + use crate::codecs::{ErrorsHandler, PyDecodeContext, PyEncodeContext}; use crate::common::encodings; use crate::common::wtf8::Wtf8Buf; @@ -13,7 +15,7 @@ mod _codecs { AsObject, PyObjectRef, PyResult, VirtualMachine, builtins::{PyStrRef, PyUtf8StrRef}, codecs, - exceptions::cstring_error, + exceptions::nul_char_error, function::{ArgBytesLike, FuncArgs}, }; @@ -29,8 +31,9 @@ mod _codecs { #[pyfunction] fn lookup(encoding: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if encoding.as_str().contains('\0') { - return Err(cstring_error(vm)); + if encoding.as_pystr().contains_nuls() { + cold_path(); + return Err(nul_char_error(vm)); } vm.state .codec_registry @@ -105,16 +108,18 @@ mod _codecs { #[pyfunction] fn lookup_error(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if name.as_str().contains('\0') { - return Err(cstring_error(vm)); + if name.as_pystr().contains_nuls() { + cold_path(); + return Err(nul_char_error(vm)); } vm.state.codec_registry.lookup_error(name.as_str(), vm) } #[pyfunction] fn _unregister_error(errors: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if errors.as_str().contains('\0') { - return Err(cstring_error(vm)); + if errors.as_pystr().contains_nuls() { + cold_path(); + return Err(nul_char_error(vm)); } vm.state .codec_registry @@ -377,6 +382,23 @@ mod _codecs_windows { use crate::{builtins::PyStrRef, builtins::PyUtf8StrRef, function::ArgBytesLike}; use rustpython_host_env::windows as host_windows; + fn string_from_utf16( + encoding: &str, + data: &[u8], + wide: &[u16], + vm: &VirtualMachine, + ) -> PyResult { + String::from_utf16(wide).map_err(|err| { + vm.new_unicode_decode_error( + vm.ctx.new_str(encoding), + vm.ctx.new_bytes(data.to_vec()), + 0, + data.len(), + vm.ctx.new_str(format!("{encoding}_decode failed: {err}")), + ) + }) + } + #[derive(FromArgs)] struct MbcsEncodeArgs { #[pyarg(positional)] @@ -394,9 +416,7 @@ mod _codecs_windows { Some(s) => s, None => { // String contains surrogates - not encodable with mbcs - return Err(vm.new_unicode_encode_error( - "'mbcs' codec can't encode character: surrogates not allowed", - )); + return encode_code_page_errors(host_windows::CP_ACP, &args.s, errors, "mbcs", vm); } }; let char_len = args.s.char_len(); @@ -428,9 +448,7 @@ mod _codecs_windows { .map_err(|err| vm.new_os_error(format!("mbcs_encode failed: {err}")))?; if errors == "strict" && used_default_char { - return Err(vm.new_unicode_encode_error( - "'mbcs' codec can't encode characters: invalid character", - )); + return encode_code_page_errors(host_windows::CP_ACP, &args.s, errors, "mbcs", vm); } buffer.truncate(result); @@ -479,8 +497,7 @@ mod _codecs_windows { ) .map_err(|err| vm.new_os_error(format!("mbcs_decode failed: {err}")))?; buffer.truncate(result); - let s = String::from_utf16(&buffer) - .map_err(|e| vm.new_unicode_decode_error(format!("mbcs_decode failed: {e}")))?; + let s = string_from_utf16("mbcs", data.as_ref(), &buffer, vm)?; return Ok((s, len)); } @@ -495,8 +512,7 @@ mod _codecs_windows { ) .map_err(|err| vm.new_os_error(format!("mbcs_decode failed: {err}")))?; buffer.truncate(result); - let s = String::from_utf16(&buffer) - .map_err(|e| vm.new_unicode_decode_error(format!("mbcs_decode failed: {e}")))?; + let s = string_from_utf16("mbcs", data.as_ref(), &buffer, vm)?; Ok((s, len)) } @@ -518,9 +534,7 @@ mod _codecs_windows { Some(s) => s, None => { // String contains surrogates - not encodable with oem - return Err(vm.new_unicode_encode_error( - "'oem' codec can't encode character: surrogates not allowed", - )); + return encode_code_page_errors(host_windows::CP_OEMCP, &args.s, errors, "oem", vm); } }; let char_len = args.s.char_len(); @@ -552,9 +566,7 @@ mod _codecs_windows { .map_err(|err| vm.new_os_error(format!("oem_encode failed: {err}")))?; if errors == "strict" && used_default_char { - return Err(vm.new_unicode_encode_error( - "'oem' codec can't encode characters: invalid character", - )); + return encode_code_page_errors(host_windows::CP_OEMCP, &args.s, errors, "oem", vm); } buffer.truncate(result); @@ -604,8 +616,7 @@ mod _codecs_windows { ) .map_err(|err| vm.new_os_error(format!("oem_decode failed: {err}")))?; buffer.truncate(result); - let s = String::from_utf16(&buffer) - .map_err(|e| vm.new_unicode_decode_error(format!("oem_decode failed: {e}")))?; + let s = string_from_utf16("oem", data.as_ref(), &buffer, vm)?; return Ok((s, len)); } @@ -620,8 +631,7 @@ mod _codecs_windows { ) .map_err(|err| vm.new_os_error(format!("oem_decode failed: {err}")))?; buffer.truncate(result); - let s = String::from_utf16(&buffer) - .map_err(|e| vm.new_unicode_decode_error(format!("oem_decode failed: {e}")))?; + let s = string_from_utf16("oem", data.as_ref(), &buffer, vm)?; Ok((s, len)) } @@ -786,19 +796,18 @@ mod _codecs_windows { // Convert code point to UTF-16 let mut wchars = [0u16; 2]; - let wchar_len; let is_surrogate = (0xD800..=0xDFFF).contains(&ch); - if is_surrogate { - wchar_len = 0; // Can't encode surrogates normally + let wchar_len = if is_surrogate { + 0 // Can't encode surrogates normally } else if ch < 0x10000 { wchars[0] = ch as u16; - wchar_len = 1; + 1 } else { wchars[0] = ((ch - 0x10000) >> 10) as u16 + 0xD800; wchars[1] = ((ch - 0x10000) & 0x3FF) as u16 + 0xDC00; - wchar_len = 2; - } + 2 + }; if !is_surrogate { let mut buf = [0u8; 8]; @@ -1020,7 +1029,7 @@ mod _codecs_windows { } } let object = vm.ctx.new_bytes(data.to_vec()); - return Err(vm.new_unicode_decode_error_real( + return Err(vm.new_unicode_decode_error( encoding_str, object, fail_pos, @@ -1111,7 +1120,7 @@ mod _codecs_windows { } "strict" => { let object = vm.ctx.new_bytes(data.to_vec()); - return Err(vm.new_unicode_decode_error_real( + return Err(vm.new_unicode_decode_error( encoding_str, object, pos, @@ -1122,7 +1131,7 @@ mod _codecs_windows { _ => { // Custom error handler let object = vm.ctx.new_bytes(data.to_vec()); - let exc = vm.new_unicode_decode_error_real( + let exc = vm.new_unicode_decode_error( encoding_str.clone(), object, pos, diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index 4f580ab5dff..b48c0e670ac 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -7,27 +7,35 @@ mod _collections { atomic_func, builtins::{ IterStatus::{Active, Exhausted}, - PositionIterInternal, PyGenericAlias, PyInt, PyStr, PyType, PyTypeRef, + PositionIterInternal, PyDict, PyGenericAlias, PyInt, PyStr, PyType, PyTypeRef, }, common::lock::{PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}, - function::{KwArgs, OptionalArg, PyComparisonValue}, + convert::ToPyObject, + function::{FuncArgs, KwArgs, OptionalArg, PyComparisonValue}, iter::PyExactSizeIterator, - protocol::{PyIterReturn, PyNumberMethods, PySequenceMethods}, + object::{Traverse, TraverseFn}, + protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, sequence::{MutObjectSequenceOp, OptionalRangeArgs}, sliceable::SequenceIndexOp, types::{ - AsNumber, AsSequence, Comparable, Constructor, DefaultConstructor, Initializer, - IterNext, Iterable, PyComparisonOp, Representable, SelfIter, + AsMapping, AsNumber, AsSequence, Comparable, Constructor, DefaultConstructor, + Initializer, IterNext, Iterable, PyComparisonOp, Representable, SelfIter, }, utils::collection_repr, + vm::MAX_MEMORY_SIZE, }; use alloc::collections::VecDeque; - use core::cmp::max; + use core::{cmp::max, mem::size_of}; use crossbeam_utils::atomic::AtomicCell; #[pyattr] - #[pyclass(module = "collections", name = "deque", unhashable = true)] + #[pyclass( + module = "collections", + name = "deque", + unhashable = true, + traverse = "manual" + )] #[derive(Debug, Default, PyPayload)] struct PyDeque { deque: PyRwLock>, @@ -35,6 +43,21 @@ mod _collections { state: AtomicCell, // incremented whenever the indices move } + // SAFETY: Traverse visits each owned Python reference at most once. + unsafe impl Traverse for PyDeque { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + if let Some(deque) = self.deque.try_read_recursive() { + for obj in deque.iter() { + obj.traverse(tracer_fn); + } + } + } + + fn clear(&mut self, out: &mut Vec) { + out.extend(self.deque.get_mut().drain(..)); + } + } + type PyDequeRef = PyRef; #[derive(FromArgs)] @@ -317,6 +340,10 @@ mod _collections { let deque = self.borrow_deque(); let n = vm.check_repeat_or_overflow_error(deque.len(), n)?; let mul_len = n * deque.len(); + let result_len = self.maxlen.map_or(mul_len, |maxlen| mul_len.min(maxlen)); + if n > 1 && result_len.saturating_mul(size_of::()) >= MAX_MEMORY_SIZE { + return Err(vm.new_memory_error("")); + } let iter = deque.iter().cycle().take(mul_len); let skipped = self .maxlen @@ -399,7 +426,7 @@ mod _collections { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -575,9 +602,10 @@ mod _collections { let closing_part = zelf .maxlen .map_or_else(|| "]".to_owned(), |maxlen| format!("], maxlen={maxlen}")); + let empty = format!("{class_name}([{closing_part})"); if zelf.__len__() == 0 { - return Ok(vm.ctx.new_str(format!("{class_name}([{closing_part})"))); + return Ok(vm.ctx.new_str(empty)); } if let Some(_guard) = ReprGuard::enter(vm, zelf.as_object()) { @@ -585,6 +613,7 @@ mod _collections { Some(&class_name), "[", &closing_part, + &empty, deque.iter(), vm, )?)) @@ -746,4 +775,210 @@ mod _collections { }) } } + + #[pyattr] + #[pyclass( + module = "collections", + name = "defaultdict", + base = PyDict, + unhashable = true, + traverse = "manual" + )] + #[derive(Debug, Default)] + struct PyDefaultDict { + dict: PyDict, + default_factory: PyRwLock>, + } + + // SAFETY: Traverse visits each owned Python reference at most once. + unsafe impl Traverse for PyDefaultDict { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.dict.traverse(tracer_fn); + self.default_factory.traverse(tracer_fn); + } + + fn clear(&mut self, out: &mut Vec) { + Traverse::clear(&mut self.dict, out); + if let Some(factory) = self.default_factory.get_mut().take() { + out.push(factory); + } + } + } + + #[pyclass( + with(AsMapping, AsNumber, Constructor, Initializer, Representable), + flags(BASETYPE, MAPPING, HAS_DICT) + )] + impl PyDefaultDict { + #[pygetset] + fn default_factory(&self) -> Option { + self.default_factory.read().clone() + } + + #[pygetset(name = "default_factory", setter)] + fn default_factory_setter(&self, value: PyObjectRef, vm: &VirtualMachine) { + *self.default_factory.write() = if value.is(&vm.ctx.none()) { + None + } else { + Some(value) + }; + } + + #[pymethod] + fn __missing__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let factory = self.default_factory(); + + if let Some(f) = factory { + let value = f.call((), vm)?; + self.dict.setdefault(key, value.into(), vm) + } else { + Err(vm.new_key_error(key)) + } + } + + #[pymethod] + #[pymethod(name = "__copy__")] + fn copy(&self) -> Self { + let default_factory = self.default_factory(); + + Self { + dict: self.dict.copy(), + default_factory: PyRwLock::new(default_factory), + } + } + + #[pymethod] + fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let cls = zelf.class().to_owned(); + + let default_factory = zelf.default_factory(); + let factory_tuple_elements = + default_factory.map_or_else(Vec::new, |factory| vec![factory]); + let factory_tuple = vm.ctx.new_tuple(factory_tuple_elements); + + let items_fn = zelf.as_object().get_attr("items", vm)?; + let items_iter = items_fn.call((), vm)?; + let iter = items_iter.get_iter(vm)?; + let none = vm.ctx.none(); + + Ok(vm + .ctx + .new_tuple(vec![ + cls.into(), + factory_tuple.into(), + none.clone(), + none, + iter.into(), + ]) + .into()) + } + } + + impl PyDefaultDict { + fn __or__(lhs: PyObjectRef, rhs: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let not_implemented = || Ok(vm.ctx.not_implemented.clone().into()); + + let (default_factory, dict) = if let Some(zelf) = lhs.downcast_ref::() { + if !rhs.fast_isinstance(vm.ctx.types.dict_type) { + return not_implemented(); + } + + (zelf.default_factory(), zelf.dict.copy()) + } else if let Some(zelf) = rhs.downcast_ref::() { + let Some(dict) = lhs.downcast_ref::() else { + return not_implemented(); + }; + + (zelf.default_factory(), dict.copy()) + } else { + return Err(vm.new_type_error(format!( + "unsupported operand type(s) for |: '{}' and '{}'", + lhs.class().name(), + rhs.class().name() + ))); + }; + + dict.update(rhs.into(), KwArgs::default(), vm)?; + + Ok(Self { + dict, + default_factory: PyRwLock::new(default_factory), + } + .to_pyobject(vm)) + } + } + + impl DefaultConstructor for PyDefaultDict {} + + impl Initializer for PyDefaultDict { + type Args = FuncArgs; + + fn init(zelf: PyRef, mut args: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + let default_factory = args.take_positional().map_or(Ok(None), |factory| { + let is_none = factory.is(&vm.ctx.none()); + + if !is_none && !factory.is_callable() { + Err(vm.new_type_error("first argument must be callable or None")) + } else if is_none { + Ok(None) + } else { + Ok(Some(factory)) + } + })?; + + *zelf.default_factory.write() = default_factory; + + zelf.dict.update( + OptionalArg::from_option(args.take_positional()), + args.kwargs, + vm, + )?; + + Ok(()) + } + } + + impl Representable for PyDefaultDict { + fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let default_factory = zelf.default_factory.read(); + + let factory_repr = match default_factory.as_ref() { + Some(factory) => { + if let Some(_guard) = ReprGuard::enter(vm, factory) { + factory.repr(vm)?.to_string() + } else { + String::from("...") + } + } + None => String::from("None"), + }; + + let dict_repr = Representable::repr(&zelf.dict.copy().into_ref(&vm.ctx), vm)?; + + Ok(format!( + "{}({}, {})", + zelf.class().name(), + factory_repr, + dict_repr + )) + } + } + + impl AsMapping for PyDefaultDict { + fn as_mapping() -> &'static PyMappingMethods { + PyDict::as_mapping() + } + } + + impl AsNumber for PyDefaultDict { + fn as_number() -> &'static PyNumberMethods { + static AS_NUMBER: PyNumberMethods = PyNumberMethods { + or: Some(|a, b, vm| { + PyDefaultDict::__or__(a.to_pyobject(vm), b.to_pyobject(vm), vm) + }), + ..PyNumberMethods::NOT_IMPLEMENTED + }; + &AS_NUMBER + } + } } diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index 6370bc42b3d..e4857d0ee06 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -16,7 +16,7 @@ use crate::{ }; pub(super) use array::PyCArray; -pub(super) use base::{FfiArgValue, PyCData, PyCField, StgInfo, StgInfoFlags}; +pub(super) use base::{CArgValue, PyCData, PyCField, StgInfo, StgInfoFlags}; pub(super) use pointer::PyCPointer; pub(super) use simple::{PyCSimple, PyCSimpleType}; pub(super) use structure::PyCStructure; @@ -107,10 +107,13 @@ pub(crate) mod _ctypes { pub(crate) struct CArgObject { /// Type tag ('P', 'V', 'i', 'd', etc.) pub tag: u8, - /// The actual FFI value (mirrors union value) - pub value: super::FfiArgValue, + /// The actual foreign-call value (mirrors union value) + pub value: super::CArgValue, /// Reference to original object (for memory safety) pub obj: PyObjectRef, + /// Owner keeping a `Pointer` value's target memory alive (e.g. a + /// null-terminated buffer copy created by `from_param`), if any. + pub keep: Option, /// Size for struct/union ('V' tag) #[allow(dead_code)] pub size: usize, @@ -126,72 +129,67 @@ pub(crate) mod _ctypes { impl Representable for CArgObject { // PyCArg_repr - use tag and value fields directly fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - use super::base::FfiArgValue; + use rustpython_host_env::ctypes::{FfiValue, ffi_value_from_type_code}; let tag_char = zelf.tag as char; + // Reconstruct the scalar the value lowers to, so the formatting + // matches the value passed to the foreign call exactly. + let ffi_val = match &zelf.value { + super::CArgValue::Typed { code, bytes } => { + let mut buf = [0u8; 4]; + ffi_value_from_type_code(code.encode_utf8(&mut buf), bytes) + } + super::CArgValue::Int(v) => FfiValue::I32(*v), + super::CArgValue::Pointer(v) => FfiValue::Pointer(*v), + // 'V' aggregates format via the object-address default arm below. + super::CArgValue::Aggregate { .. } => FfiValue::Pointer(0), + }; + // Format value based on tag match zelf.tag { b'b' | b'h' | b'i' | b'l' | b'q' => { // Signed integers - let n = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I8(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I16(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I32(v)) => { - v as i64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I64(v)) => v, + let n = match ffi_val { + FfiValue::I8(v) => v as i64, + FfiValue::I16(v) => v as i64, + FfiValue::I32(v) => v as i64, + FfiValue::I64(v) => v, _ => 0, }; Ok(format!("")) } b'B' | b'H' | b'I' | b'L' | b'Q' => { // Unsigned integers - let n = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U8(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U16(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U32(v)) => { - v as u64 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U64(v)) => v, + let n = match ffi_val { + FfiValue::U8(v) => v as u64, + FfiValue::U16(v) => v as u64, + FfiValue::U32(v) => v as u64, + FfiValue::U64(v) => v, _ => 0, }; Ok(format!("")) } b'f' => { - let v = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F32(v)) => { - v as f64 - } + let v = match ffi_val { + FfiValue::F32(v) => v as f64, _ => 0.0, }; Ok(format!("")) } b'd' | b'g' => { - let v = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F64(v)) => v, - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::F32(v)) => { - v as f64 - } + let v = match ffi_val { + FfiValue::F64(v) => v, + FfiValue::F32(v) => v as f64, _ => 0.0, }; Ok(format!("")) } b'c' => { // c_char - single byte - let byte = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::I8(v)) => { - v as u8 - } - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::U8(v)) => v, + let byte = match ffi_val { + FfiValue::I8(v) => v as u8, + FfiValue::U8(v) => v, _ => 0, }; if is_literal_char(byte) { @@ -200,11 +198,10 @@ pub(crate) mod _ctypes { Ok(format!("")) } } - b'z' | b'Z' | b'P' | b'V' => { + b'z' | b'Z' | b'P' => { // Pointer types - let ptr = match zelf.value { - FfiArgValue::Scalar(rustpython_host_env::ctypes::FfiValue::Pointer(v)) => v, - FfiArgValue::OwnedPointer(v, _) => v, + let ptr = match ffi_val { + FfiValue::Pointer(v) => v, _ => 0, }; if ptr == 0 { @@ -600,7 +597,7 @@ pub(crate) mod _ctypes { offset: OptionalArg, vm: &VirtualMachine, ) -> PyResult { - use super::FfiArgValue; + use super::CArgValue; // Check if obj is a ctypes instance if !obj.fast_isinstance(PyCData::static_type()) @@ -628,8 +625,9 @@ pub(crate) mod _ctypes { // Create CArgObject to hold the reference Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj, + keep: None, size: 0, offset: offset_val, } diff --git a/crates/vm/src/stdlib/_ctypes/array.rs b/crates/vm/src/stdlib/_ctypes/array.rs index f7abc834564..d4674f33b07 100644 --- a/crates/vm/src/stdlib/_ctypes/array.rs +++ b/crates/vm/src/stdlib/_ctypes/array.rs @@ -511,7 +511,11 @@ impl AsMapping for PyCArray { )] impl PyCArray { #[pyclassmethod] - fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { + fn __class_getitem__( + cls: PyTypeRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } @@ -641,6 +645,14 @@ impl PyCArray { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); zelf.0.keep_alive(index, kept_alive); (ptr, Some(value.to_owned())) + } else if let Some(simple) = value.downcast_ref::() + && value.class().type_code(vm).as_deref() == Some("z") + { + let buffer = simple.0.buffer.read(); + ( + rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer), + None, + ) } else if let Ok(int_val) = value.try_index(vm) { (int_val.as_bigint().to_usize().unwrap_or(0), None) } else { @@ -667,6 +679,14 @@ impl PyCArray { } else if let Some(s) = value.downcast_ref::() { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); (ptr, Some(holder)) + } else if let Some(simple) = value.downcast_ref::() + && value.class().type_code(vm).as_deref() == Some("Z") + { + let buffer = simple.0.buffer.read(); + ( + rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer), + None, + ) } else if let Ok(int_val) = value.try_index(vm) { (int_val.as_bigint().to_usize().unwrap_or(0), None) } else { @@ -974,13 +994,19 @@ impl PyCArray { let (range, step, slice_len) = sat_slice.adjust_indices(length); // other_len = PySequence_Length(value); - let items: Vec = vm.extract_elements_with(&value, Ok)?; - let other_len = items.len(); + // Size the operand before consuming it so an unbounded iterable is + // rejected without being materialized. + let other_len = value + .sequence_unchecked() + .length(vm) + .map_err(|_| vm.new_value_error("Can only assign sequence of same size"))?; if other_len != slice_len { return Err(vm.new_value_error("Can only assign sequence of same size")); } + let items: Vec = vm.extract_elements_with(&value, Ok)?; + // Use SaturatedSliceIter for correct index iteration (handles negative step) let iter = SaturatedSliceIter::from_adjust_indices(range, step, slice_len); @@ -1035,6 +1061,7 @@ impl AsBuffer for PyCArray { dim_desc.reverse(); BufferDescriptor { + offset: 0, len: buffer_len, readonly: false, itemsize, diff --git a/crates/vm/src/stdlib/_ctypes/base.rs b/crates/vm/src/stdlib/_ctypes/base.rs index 62856c4cef8..6067fc61bf0 100644 --- a/crates/vm/src/stdlib/_ctypes/base.rs +++ b/crates/vm/src/stdlib/_ctypes/base.rs @@ -18,8 +18,8 @@ use num_traits::{Signed, ToPrimitive}; use rustpython_common::lock::PyRwLock; use rustpython_common::wtf8::Wtf8; use rustpython_host_env::ctypes::{ - CTypeParamKind, FfiArg, FfiType, FfiValue, char_array_assignment_bytes, char_array_field_value, - ffi_arg_from_value, ffi_type_for_layout, wchar_array_field_value, write_cow_bytes_at_offset, + CTypeLayout, char_array_assignment_bytes, char_array_field_value, wchar_array_field_value, + write_cow_bytes_at_offset, }; // StgInfo - Storage information for ctypes types @@ -99,8 +99,9 @@ pub struct StgInfo { // Byte order (for _swappedbytes_) pub big_endian: bool, // true if big endian, false if little endian - // FFI field types for structure/union passing (inherited from base class) - pub ffi_field_types: Vec, + // Call layouts of the struct/union fields, in declaration order (inherited + // from base class). Drives by-value aggregate passing. + pub field_layouts: Vec, // Cached pointer type (non-inheritable via descriptor) pub pointer_type: Option, @@ -127,7 +128,7 @@ impl core::fmt::Debug for StgInfo { .field("shape", &self.shape) .field("paramfunc", &self.paramfunc) .field("big_endian", &self.big_endian) - .field("ffi_field_types", &self.ffi_field_types.len()) + .field("field_layouts", &self.field_layouts.len()) .finish() } } @@ -147,7 +148,7 @@ impl Default for StgInfo { shape: Vec::new(), paramfunc: ParamFunc::None, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } @@ -168,7 +169,7 @@ impl StgInfo { shape: Vec::new(), paramfunc: ParamFunc::None, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } @@ -220,30 +221,11 @@ impl StgInfo { shape, paramfunc: ParamFunc::Array, big_endian: cfg!(target_endian = "big"), // native endian by default - ffi_field_types: Vec::new(), + field_layouts: Vec::new(), pointer_type: None, } } - /// Get libffi type for this StgInfo - /// Note: For very large types, returns pointer type to avoid overflow - pub fn to_ffi_type(&self) -> FfiType { - let kind = match self.paramfunc { - ParamFunc::Structure => CTypeParamKind::Structure, - ParamFunc::Union => CTypeParamKind::Union, - ParamFunc::Array => CTypeParamKind::Array, - ParamFunc::Pointer => CTypeParamKind::Pointer, - _ => CTypeParamKind::Simple, - }; - ffi_type_for_layout( - kind, - &self.ffi_field_types, - self.size, - self.length, - self.format.as_deref(), - ) - } - /// Check if this type is finalized (cannot set _fields_ again) pub fn is_final(&self) -> bool { self.flags.contains(StgInfoFlags::DICTFLAG_FINAL) @@ -255,6 +237,44 @@ impl StgInfo { } } +/// Build the host_env call layout for a ctypes type from its already-borrowed +/// `StgInfo`. Aggregate layouts come straight from the type's `field_layouts` +/// (built incrementally from the base class, so struct inheritance is +/// reflected); array elements recurse into the element type; simple types read +/// their `_type_` code. The caller passes the borrowed `stg` so this never +/// re-locks `ty`'s own type data. +pub(super) fn type_layout(ty: &Py, stg: &StgInfo, vm: &VirtualMachine) -> CTypeLayout { + match stg.paramfunc { + ParamFunc::Structure => CTypeLayout::Struct { + fields: stg.field_layouts.clone(), + size: stg.size, + }, + ParamFunc::Union => CTypeLayout::Union { + fields: stg.field_layouts.clone(), + size: stg.size, + }, + ParamFunc::Array => { + let element = stg + .element_type + .as_ref() + .and_then(|et| et.stg_info_opt().map(|et_stg| type_layout(et, &et_stg, vm))) + .unwrap_or(CTypeLayout::Opaque { + size: stg.element_size, + }); + CTypeLayout::Array { + element: Box::new(element), + length: stg.length, + size: stg.size, + } + } + ParamFunc::Pointer => CTypeLayout::Pointer, + ParamFunc::Simple | ParamFunc::None => ty + .type_code(vm) + .and_then(|code| code.chars().next()) + .map_or(CTypeLayout::Opaque { size: stg.size }, CTypeLayout::Simple), + } +} + /// __pointer_type__ getter for ctypes metaclasses. /// Reads from StgInfo.pointer_type (non-inheritable). pub(super) fn pointer_type_get(zelf: &Py, vm: &VirtualMachine) -> PyResult { @@ -604,7 +624,9 @@ impl PyCData { // Get buffer pointer - the memory is owned by source let ptr = { - let bytes = buffer.obj_bytes(); + // Contiguity is checked above, so this is the view's own bytes rather + // than the whole exporter's. + let bytes = unsafe { buffer.contiguous_unchecked() }; bytes.as_ptr().wrapping_add(offset) }; @@ -1828,12 +1850,12 @@ fn simple_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult // Read value from buffer: memcpy(&parg->value, self->b_ptr, self->b_size) let buffer = simple.0.buffer.read(); - let ffi_value = buffer_to_ffi_value(&type_code, &buffer); Ok(CArgObject { tag, - value: ffi_value, + value: CArgValue::typed(tag as char, &buffer), obj: obj.to_owned(), + keep: None, size: 0, offset: 0, }) @@ -1853,8 +1875,9 @@ fn array_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: obj.to_owned(), + keep: None, size: 0, offset: 0, }) @@ -1873,8 +1896,9 @@ fn pointer_paramfunc(obj: &PyObject, vm: &VirtualMachine) -> PyResult PyResult CArgObject { - // Get buffer pointer - // For large structs (> sizeof(void*)), we'd need to allocate and copy. - // For now, just point to buffer directly and keep obj reference for memory safety. - let buffer = if let Some(cdata) = obj.downcast_ref::() { - cdata.buffer.read() + // Snapshot the instance bytes and pass the aggregate by value. The layout + // is built here from the already-borrowed `stg_info` to avoid re-locking. + let (bytes, size) = if let Some(cdata) = obj.downcast_ref::() { + let buffer = cdata.buffer.read(); + (buffer.to_vec(), buffer.len()) } else { - return CArgObject { - tag: b'V', - value: FfiArgValue::pointer(0), - obj: obj.to_owned(), - size: stg_info.size, - offset: 0, - }; + (Vec::new(), stg_info.size) }; - let ptr_val = buffer.as_ptr() as usize; - let size = buffer.len(); + let layout = if matches!(stg_info.paramfunc, ParamFunc::Union) { + CTypeLayout::Union { + fields: stg_info.field_layouts.clone(), + size: stg_info.size, + } + } else { + CTypeLayout::Struct { + fields: stg_info.field_layouts.clone(), + size: stg_info.size, + } + }; CArgObject { tag: b'V', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::aggregate(layout, bytes), obj: obj.to_owned(), + keep: None, size, offset: 0, } } -// FfiArgValue - Owned FFI argument value +// CArgValue - Owned foreign-call argument value -/// Owned FFI argument value. Keeps the value alive for the duration of the FFI call. +/// A foreign-call argument in a form the unified `call` entry point accepts: a +/// simple-typed scalar (its ctypes code plus a native-endian bytes snapshot), +/// an untyped int, or an address. Any object whose memory an address +/// refers to is kept alive by the enclosing `Argument`/`CArgObject`, not here. #[derive(Debug, Clone)] -pub enum FfiArgValue { - Scalar(FfiValue), - /// Pointer with owned data. The PyObjectRef keeps the pointed data alive. - OwnedPointer(usize, #[allow(dead_code)] PyObjectRef), +pub enum CArgValue { + /// A value typed by its ctypes simple-type code, snapshotted as its bytes. + Typed { code: char, bytes: Vec }, + /// Untyped Python int (ConvParam default: C int). + Int(i32), + /// Address-valued argument (pointer decay, byref, buffer copies, NULL = 0). + Pointer(usize), + /// By-value aggregate: its call layout plus a snapshot of its bytes. + Aggregate { layout: CTypeLayout, bytes: Vec }, } -impl FfiArgValue { +impl CArgValue { pub fn pointer(value: usize) -> Self { - Self::Scalar(FfiValue::Pointer(value)) + Self::Pointer(value) } - /// Create an Arg reference to this owned value - pub fn as_arg(&self) -> FfiArg<'_> { - match self { - Self::Scalar(value) => ffi_arg_from_value(value), - Self::OwnedPointer(v, _) => rustpython_host_env::ctypes::ffi_arg( - rustpython_host_env::ctypes::FfiArgRef::Pointer(v), - ), + /// Snapshot a simple-typed value from its code and buffer bytes. + pub(super) fn typed(code: char, buffer: &[u8]) -> Self { + Self::Typed { + code, + bytes: buffer.to_vec(), } } -} -/// Convert buffer bytes to FfiArgValue based on type code -pub(super) fn buffer_to_ffi_value(type_code: &str, buffer: &[u8]) -> FfiArgValue { - FfiArgValue::Scalar(rustpython_host_env::ctypes::ffi_value_from_type_code( - type_code, buffer, - )) + /// Snapshot an aggregate value from its call layout and buffer bytes. + pub(super) fn aggregate(layout: CTypeLayout, bytes: Vec) -> Self { + Self::Aggregate { layout, bytes } + } + + /// Lower to a [`CallArg`], borrowing `code_buf` for the code's `&str`. + pub(super) fn as_call_arg<'a>( + &'a self, + code_buf: &'a mut [u8; 4], + ) -> rustpython_host_env::ctypes::CallArg<'a> { + use rustpython_host_env::ctypes::CallArg; + match self { + Self::Typed { code, bytes } => CallArg::Typed { + code: code.encode_utf8(code_buf), + buffer: bytes, + }, + Self::Int(value) => CallArg::Int(*value), + Self::Pointer(value) => CallArg::Pointer(*value), + Self::Aggregate { layout, bytes } => CallArg::Aggregate { + layout, + buffer: bytes, + }, + } + } } /// Convert bytes to appropriate Python object based on ctypes type diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index 25cbcdcd9a1..afbe0ae76ea 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -3,13 +3,13 @@ use super::{ _ctypes::CArgObject, - PyCArray, PyCData, PyCPointer, PyCStructure, StgInfo, - base::{CDATA_BUFFER_METHODS, FfiArgValue, ParamFunc, StgInfoFlags}, + PyCArray, PyCData, PyCPointer, PyCStructure, PyCUnion, StgInfo, + base::{CArgValue, CDATA_BUFFER_METHODS, ParamFunc, StgInfoFlags}, simple::PyCSimple, }; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBytes, PyDict, PyNone, PyStr, PyTuple, PyType, PyTypeRef}, + builtins::{PyBytes, PyDict, PyInt, PyStr, PyTuple, PyType, PyTypeRef}, class::StaticType, function::FuncArgs, protocol::{BufferDescriptor, PyBuffer, PyNumberMethods}, @@ -19,16 +19,16 @@ use crate::{ use alloc::borrow::Cow; use core::ffi::c_void; use core::fmt::Debug; +use core::ptr::NonNull; use num_traits::{Signed, ToPrimitive}; use rustpython_common::lock::PyRwLock; #[cfg(windows)] use rustpython_host_env::ctypes::ComMethodError; use rustpython_host_env::ctypes::{ - CallResult as RawResult, FfiCif, FfiCodePtr, FfiType, FfiValue, RawMemoryView, - RawMemoryViewError, StringAtError, ffi_f64_type, ffi_i32_type, ffi_pointer_type, - ffi_type_for_return_size, ffi_type_from_code, ffi_type_from_tag, ffi_void_type, - has_pointer_width, null_code_ptr, offset_address, pointer_bytes, pointer_format, pointer_size, - write_pointer_to_buffer_at, write_prefix_limited, + CTypeLayout, CallError, CallOptions, CallRet, CallValue, FfiCif, FfiCodePtr, FfiType, + RawMemoryView, RawMemoryViewError, StringAtError, call, ffi_pointer_type, ffi_type_from_code, + ffi_void_type, has_pointer_width, offset_address, pointer_bytes, pointer_format, pointer_size, + simple_type_is_pointer, write_pointer_to_buffer_at, write_prefix_limited, }; // Internal function addresses for special ctypes functions @@ -41,7 +41,7 @@ pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4; /// Convert any object to a pointer value for c_void_p arguments /// Follows ConvParam logic for pointer types -fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { +fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 0. CArgObject (from byref()) -> buffer address + offset if let Some(carg) = value.downcast_ref::() { // Get buffer address from the underlying object @@ -54,29 +54,29 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult NULL if value.is(&vm.ctx.none) { - return Ok(FfiArgValue::pointer(0)); + return Ok(CArgValue::pointer(0)); } // 2. PyCArray -> buffer address (PyCArrayType_paramfunc) if let Some(array) = value.downcast_ref::() { let addr = array.0.buffer.read().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 3. PyCPointer -> stored pointer value if let Some(ptr) = value.downcast_ref::() { - return Ok(FfiArgValue::pointer(ptr.get_ptr_value())); + return Ok(CArgValue::pointer(ptr.get_ptr_value())); } // 4. PyCStructure -> buffer address if let Some(struct_obj) = value.downcast_ref::() { let addr = struct_obj.0.buffer.read().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 5. PyCSimple (c_void_p, c_char_p, etc.) -> value from buffer @@ -84,14 +84,14 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult buffer address (PyBytes_AsString) if let Some(bytes) = value.downcast_ref::() { let addr = bytes.as_bytes().as_ptr() as usize; - return Ok(FfiArgValue::pointer(addr)); + return Ok(CArgValue::pointer(addr)); } // 7. Integer -> direct value (PyLong_AsVoidPtr behavior) @@ -100,10 +100,10 @@ fn convert_to_pointer(value: &PyObject, vm: &VirtualMachine) -> PyResult PyResult PyResult { - // 1. CArgObject (from byref() or paramfunc) -> use stored type and value + // 1. CArgObject (from byref() or paramfunc) -> use stored value if let Some(carg) = value.downcast_ref::() { - let ffi_type = ffi_type_from_tag(carg.tag); return Ok(Argument { - ffi_type, - keep: None, + keep: carg.keep.clone(), value: carg.value.clone(), }); } @@ -136,18 +134,15 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { // 2. None -> NULL pointer if value.is(&vm.ctx.none) { return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: None, - value: FfiArgValue::pointer(0), + value: CArgValue::pointer(0), }); } // 3. ctypes objects -> use paramfunc if let Ok(carg) = super::base::call_paramfunc(value, vm) { - let ffi_type = ffi_type_from_tag(carg.tag); return Ok(Argument { - ffi_type, - keep: None, + keep: carg.keep, value: carg.value, }); } @@ -158,9 +153,8 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { let keep = vm.ctx.new_bytes(wide_bytes); let addr = keep.as_bytes().as_ptr() as usize; return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: Some(keep.into()), - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); } @@ -171,32 +165,24 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { let keep = vm.ctx.new_bytes(buffer); let addr = keep.as_bytes().as_ptr() as usize; return Ok(Argument { - ffi_type: ffi_pointer_type(), keep: Some(keep.into()), - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); } // 10. Python int -> i32 (default integer type) - if let Ok(int_val) = value.try_int(vm) { + // PyLong_Check: only an int (or a subclass) converts. Going through + // `__int__` would accept a float and pass its truncated value where the + // callee expects a pointer. + if let Some(int_val) = value.downcast_ref::() { let val = int_val.as_bigint().to_i32().unwrap_or(0); return Ok(Argument { - ffi_type: ffi_i32_type(), - keep: None, - value: FfiArgValue::Scalar(FfiValue::I32(val)), - }); - } - - // 11. Python float -> f64 - if let Ok(float_val) = value.try_float(vm) { - return Ok(Argument { - ffi_type: ffi_f64_type(), keep: None, - value: FfiArgValue::Scalar(FfiValue::F64(float_val.to_f64())), + value: CArgValue::Int(val), }); } - // 12. Check _as_parameter_ attribute + // 11. Check _as_parameter_ attribute if let Ok(as_param) = value.get_attr("_as_parameter_", vm) { return conv_param(&as_param, vm); } @@ -208,47 +194,47 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult { } trait ArgumentType { - fn to_ffi_type(&self, vm: &VirtualMachine) -> PyResult; - fn convert_object(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult; + /// Convert an argument for this type into a foreign-call value plus an + /// optional owner keeping any referenced memory alive. + fn convert_object( + &self, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<(CArgValue, Option)>; } impl ArgumentType for PyTypeRef { - fn to_ffi_type(&self, vm: &VirtualMachine) -> PyResult { - use super::pointer::PyCPointer; - use super::structure::PyCStructure; - - // CArgObject (from byref()) should be treated as pointer - if self.fast_issubclass(CArgObject::static_type()) { - return Ok(ffi_pointer_type()); - } - - // Pointer types (POINTER(T)) are always pointer FFI type - // Check if type is a subclass of _Pointer (PyCPointer) - if self.fast_issubclass(PyCPointer::static_type()) { - return Ok(ffi_pointer_type()); - } - - // Structure types are passed as pointers - if self.fast_issubclass(PyCStructure::static_type()) { - return Ok(ffi_pointer_type()); - } + fn convert_object( + &self, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<(CArgValue, Option)> { + // Validate the argument type up front (mirrors the pre-conversion + // check): pointer-like ctypes types are always acceptable; a simple + // type must carry a known _type_ code; anything else is unsupported. + let type_code = if self.fast_issubclass(CArgObject::static_type()) + || self.fast_issubclass(PyCPointer::static_type()) + || self.fast_issubclass(PyCStructure::static_type()) + || self.fast_issubclass(PyCUnion::static_type()) + { + None + } else { + // Use get_attr to traverse MRO (for subclasses like MyInt(c_int)) + let typ = self + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + .ok() + .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; + let typ = typ + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("Unsupported argument type"))? + .to_string(); + if ffi_type_from_code(&typ).is_none() { + return Err(vm.new_type_error(format!("Unsupported argument type: {typ}"))); + } + Some(typ) + }; - // Use get_attr to traverse MRO (for subclasses like MyInt(c_int)) - let typ = self - .as_object() - .get_attr(vm.ctx.intern_str("_type_"), vm) - .ok() - .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - let typ = typ - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - let typ = typ.to_string(); - let typ = typ.as_str(); - ffi_type_from_code(typ) - .ok_or_else(|| vm.new_type_error(format!("Unsupported argument type: {typ}"))) - } - - fn convert_object(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { // Call from_param first to convert the value // converter = PyTuple_GET_ITEM(argtypes, i); // v = PyObject_CallOneArg(converter, arg); @@ -258,88 +244,62 @@ impl ArgumentType for PyTypeRef { let converted = from_param.call((value,), vm)?; // Then pass the converted value to ConvParam logic - // CArgObject (from from_param) -> use stored value directly + // CArgObject (from from_param) -> use stored value and keepalive directly if let Some(carg) = converted.downcast_ref::() { - return Ok(carg.value.clone()); + return Ok((carg.value.clone(), carg.keep.clone())); } // None -> NULL pointer if vm.is_none(&converted) { - return Ok(FfiArgValue::pointer(0)); + return Ok((CArgValue::pointer(0), None)); } // For pointer types (POINTER(T)), we need to pass the pointer VALUE stored in buffer if self.fast_issubclass(PyCPointer::static_type()) { if let Some(pointer) = converted.downcast_ref::() { - return Ok(FfiArgValue::pointer(pointer.get_ptr_value())); + return Ok((CArgValue::pointer(pointer.get_ptr_value()), None)); } - return convert_to_pointer(&converted, vm); + return Ok((convert_to_pointer(&converted, vm)?, None)); } - // For structure types, convert to pointer to structure - if self.fast_issubclass(PyCStructure::static_type()) { - return convert_to_pointer(&converted, vm); + // For structure/union types, pass the aggregate by value: snapshot the + // instance bytes and build its call layout from the argtype. A byref() + // result is a CArgObject and was already handled above (stays a pointer). + if self.fast_issubclass(PyCStructure::static_type()) + || self.fast_issubclass(PyCUnion::static_type()) + { + if let Some(cdata) = converted.downcast_ref::() { + let bytes = cdata.buffer.read().to_vec(); + let layout = self.stg_info_opt().map_or_else( + || CTypeLayout::Opaque { size: bytes.len() }, + |stg| super::base::type_layout(self, &stg, vm), + ); + // Keep the converted instance alive through the call: the + // snapshot may embed pointers into buffers its keep-alive set + // owns, which must outlive the foreign call. + return Ok((CArgValue::aggregate(layout, bytes), Some(converted.clone()))); + } + return Ok((convert_to_pointer(&converted, vm)?, None)); } - // Get the type code for this argument type - let type_code = self - .as_object() - .get_attr(vm.ctx.intern_str("_type_"), vm) - .ok() - .and_then(|t| t.downcast_ref::().map(|s| s.to_string())); - // For pointer types (c_void_p, c_char_p, c_wchar_p), handle as pointer if matches!(type_code.as_deref(), Some("P" | "z" | "Z")) { - return convert_to_pointer(&converted, vm); + return Ok((convert_to_pointer(&converted, vm)?, None)); } // PyCSimple (already a ctypes instance from from_param) if let Ok(simple) = converted.downcast::() { - let typ = ArgumentType::to_ffi_type(self, vm)?; - let ffi_value = simple - .to_ffi_value(typ, vm) + let code = type_code + .as_deref() + .and_then(|s| s.chars().next()) .ok_or_else(|| vm.new_type_error("Unsupported argument type"))?; - return Ok(ffi_value); + return Ok((simple.to_carg_value(code), None)); } Err(vm.new_type_error("Unsupported argument type")) } } -trait ReturnType { - fn to_ffi_type(&self, vm: &VirtualMachine) -> Option; -} - -impl ReturnType for PyTypeRef { - fn to_ffi_type(&self, vm: &VirtualMachine) -> Option { - // Try to get _type_ attribute first (for ctypes types like c_void_p) - if let Ok(type_attr) = self.as_object().get_attr(vm.ctx.intern_str("_type_"), vm) - && let Some(s) = type_attr.downcast_ref::() - && let Some(ffi_type) = s.to_str().and_then(ffi_type_from_code) - { - return Some(ffi_type); - } - - // Check for Structure/Array types (have StgInfo but no _type_) - // _ctypes_get_ffi_type: returns appropriately sized type for struct returns - if let Some(stg_info) = self.stg_info_opt() { - let size = stg_info.size; - // Small structs can be returned in registers - // Match can_return_struct_as_int/can_return_struct_as_sint64 - return Some(ffi_type_for_return_size(size)); - } - - // Fallback to class name - ffi_type_from_code(self.name().to_string().as_str()) - } -} - -impl ReturnType for PyNone { - fn to_ffi_type(&self, _vm: &VirtualMachine) -> Option { - ffi_type_from_code("void") - } -} - // PyCFuncPtrType - Metaclass for function pointer types // PyCFuncPtrType_init @@ -675,12 +635,6 @@ impl PyCFuncPtr { rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer) } - /// Get CodePtr from buffer for FFI calls - fn get_code_ptr(&self) -> Option { - let addr = self.get_func_ptr(); - rustpython_host_env::ctypes::code_ptr_from_addr(addr) - } - /// Create buffer with function pointer address fn make_ptr_buffer(addr: usize) -> Vec { pointer_bytes(addr) @@ -812,7 +766,7 @@ impl Constructor for PyCFuncPtr { .as_bigint() .clone(), }; - let terminated = format!("{}\0", &name); + let terminated = format!("{name}\0"); let ptr_val = match rustpython_host_env::ctypes::lookup_function_symbol_addr( handle .to_usize() @@ -960,13 +914,103 @@ fn handle_internal_func(addr: usize, args: &FuncArgs, vm: &VirtualMachine) -> Op None } +/// How the foreign call's return value is retrieved (mirrors `CallRet`). +enum RetSpec { + /// restype is None: void return. + Void, + /// Pointer-valued return (a `TYPEFLAG_ISPOINTER` restype, or an oversized + /// by-value struct approximated as a pointer-sized register). + Pointer, + /// A scalar retrieved as the given ctypes simple-type code. + Code(char), + /// A by-value aggregate (struct/union) return with the given call layout. + Aggregate(CTypeLayout), +} + /// Call information extracted from PyCFuncPtr (argtypes, restype, etc.) struct CallInfo { explicit_arg_types: Option>, restype_obj: Option, + ret: RetSpec, +} + +fn extract_arg_types(argtypes: &PyObject, vm: &VirtualMachine) -> PyResult> { + let error = || vm.new_type_error("_argtypes_ must be a sequence of types"); + let sequence = argtypes.try_sequence(vm).map_err(|_| error())?; + let length = sequence.length(vm).map_err(|_| error())?; + let mut types = Vec::new(); + types + .try_reserve(length) + .map_err(|_| vm.new_memory_error(""))?; + + for index in 0..length { + let item = sequence.get_item(index as isize, vm).map_err(|_| error())?; + types.push(item.downcast::().map_err(|_| error())?); + } + + Ok(types) +} + +/// Determine how to retrieve the return value from restype, reproducing the +/// prior `ffi_return_type` + `is_pointer_return` dispatch. +fn compute_ret_spec( restype_is_none: bool, - ffi_return_type: FfiType, - is_pointer_return: bool, + restype_obj: Option<&PyObjectRef>, + vm: &VirtualMachine, +) -> RetSpec { + if restype_is_none { + return RetSpec::Void; + } + let Some(restype_type) = restype_obj.and_then(|t| t.clone().downcast::().ok()) else { + return RetSpec::Code('i'); + }; + + // Pointer return via TYPEFLAG_ISPOINTER (c_void_p, c_char_p, c_wchar_p, POINTER(T)) + if restype_type + .stg_info_opt() + .is_some_and(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER)) + { + return RetSpec::Pointer; + } + + // Simple type via its _type_ code (traversing MRO) + if let Ok(type_attr) = restype_type + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + && let Some(s) = type_attr.downcast_ref::() + && let Some(code) = s.to_str() + && ffi_type_from_code(code).is_some() + { + return if simple_type_is_pointer(code) { + RetSpec::Pointer + } else { + RetSpec::Code(code.chars().next().unwrap_or('i')) + }; + } + + // Structure/Union (StgInfo, no _type_): returned by value as an aggregate. + // The layout is built from the held guard to avoid re-locking the type. + if let Some(stg_info) = restype_type.stg_info_opt() { + return match stg_info.paramfunc { + ParamFunc::Structure | ParamFunc::Union => { + RetSpec::Aggregate(super::base::type_layout(&restype_type, &stg_info, vm)) + } + // Any other aggregate-ish StgInfo without a code: size-approximated + // register return, as before. + _ => { + let size = stg_info.size; + if size <= 4 { + RetSpec::Code('i') + } else if size <= 8 { + RetSpec::Code('q') + } else { + RetSpec::Pointer + } + } + }; + } + + RetSpec::Code('i') } /// Extract call information (argtypes, restype) from PyCFuncPtr @@ -975,13 +1019,7 @@ fn extract_call_info(zelf: &Py, vm: &VirtualMachine) -> PyResult> = if let Some(argtypes_obj) = zelf.argtypes.read().as_ref() { if !vm.is_none(argtypes_obj) { - Some( - argtypes_obj - .try_to_value::>(vm)? - .into_iter() - .filter_map(|obj| obj.downcast::().ok()) - .collect(), - ) + Some(extract_arg_types(argtypes_obj, vm)?) } else { None // argtypes is None -> use ConvParam } @@ -991,13 +1029,7 @@ fn extract_call_info(zelf: &Py, vm: &VirtualMachine) -> PyResult>(vm)? - .into_iter() - .filter_map(|obj| obj.downcast::().ok()) - .collect(), - ) + Some(extract_arg_types(&class_argtypes, vm)?) } else { None // No argtypes -> use ConvParam }; @@ -1011,33 +1043,12 @@ fn extract_call_info(zelf: &Py, vm: &VirtualMachine) -> PyResult().ok()) - .and_then(|t| ReturnType::to_ffi_type(&t, vm)) - .unwrap_or_else(ffi_i32_type) - }; - - // Check if return type is a pointer type via TYPEFLAG_ISPOINTER - // This handles c_void_p, c_char_p, c_wchar_p, and POINTER(T) types - let is_pointer_return = restype_obj - .as_ref() - .and_then(|t| t.clone().downcast::().ok()) - .and_then(|t| { - t.stg_info_opt() - .map(|info| info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER)) - }) - .unwrap_or(false); + let ret = compute_ret_spec(restype_is_none, restype_obj.as_ref(), vm); Ok(CallInfo { explicit_arg_types, restype_obj, - restype_is_none, - ffi_return_type, - is_pointer_return, + ret, }) } @@ -1134,8 +1145,7 @@ fn resolve_com_method( /// Single argument for FFI call // struct argument struct Argument { - ffi_type: FfiType, - value: FfiArgValue, + value: CArgValue, #[allow(dead_code)] keep: Option, // Object to keep alive during call } @@ -1197,13 +1207,8 @@ fn build_callargs_simple( let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - let value = arg_type.convert_object(arg.clone(), vm)?; - Ok(Argument { - ffi_type, - keep: None, - value, - }) + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + Ok(Argument { value, keep }) }) .collect::>>()?; Ok((arguments, Vec::new())) @@ -1240,17 +1245,14 @@ fn build_callargs_with_paramflags( let is_out = (*direction & 2) != 0; // OUT flag let is_in = (*direction & 1) != 0 || *direction == 0; // IN flag or default - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - if is_out && !is_in { // Pure OUT parameter: create buffer, don't consume caller arg let buffer = create_out_buffer(arg_type, vm)?; let addr = get_buffer_addr(&buffer) .ok_or_else(|| vm.new_type_error("Cannot create OUT buffer for this type"))?; arguments.push(Argument { - ffi_type, keep: None, - value: FfiArgValue::pointer(addr), + value: CArgValue::pointer(addr), }); out_buffers.push((param_idx, buffer)); } else { @@ -1268,12 +1270,8 @@ fn build_callargs_with_paramflags( // IN|OUT: track for return out_buffers.push((param_idx, arg.clone())); } - let value = arg_type.convert_object(arg, vm)?; - arguments.push(Argument { - ffi_type, - keep: None, - value, - }); + let (value, keep) = arg_type.convert_object(arg, vm)?; + arguments.push(Argument { value, keep }); } } @@ -1306,13 +1304,8 @@ fn build_callargs( let arg_type = arg_types .get(n) .ok_or_else(|| vm.new_type_error("argument amount mismatch"))?; - let ffi_type = ArgumentType::to_ffi_type(arg_type, vm)?; - let value = arg_type.convert_object(arg.clone(), vm)?; - arguments.push(Argument { - ffi_type, - keep: None, - value, - }); + let (value, keep) = arg_type.convert_object(arg.clone(), vm)?; + arguments.push(Argument { value, keep }); } Ok((arguments, Vec::new())) } else { @@ -1321,22 +1314,31 @@ fn build_callargs( } } -/// Execute FFI call +/// Execute the foreign call through the unified `call` entry point. fn ctypes_callproc( - code_ptr: FfiCodePtr, + addr: usize, arguments: &[Argument], - call_info: &CallInfo, -) -> RawResult { - let ffi_arg_types: Vec = arguments.iter().map(|a| a.ffi_type.clone()).collect(); - let ffi_args: Vec<_> = arguments.iter().map(|a| a.value.as_arg()).collect(); - rustpython_host_env::ctypes::callproc( - code_ptr, - ffi_arg_types, - call_info.ffi_return_type.clone(), - &ffi_args, - call_info.restype_is_none, - call_info.is_pointer_return, - ) + ret: &RetSpec, + options: CallOptions, +) -> Result { + // Encode each simple-type code into its own buffer so the `&str` borrowed + // by `CallArg::Typed` outlives the call. + let mut code_bufs = vec![[0u8; 4]; arguments.len()]; + let call_args: Vec<_> = arguments + .iter() + .zip(code_bufs.iter_mut()) + .map(|(arg, code_buf)| arg.value.as_call_arg(code_buf)) + .collect(); + + let mut ret_code_buf = [0u8; 4]; + let call_ret = match ret { + RetSpec::Void => CallRet::Void, + RetSpec::Pointer => CallRet::Pointer, + RetSpec::Code(code) => CallRet::Code(code.encode_utf8(&mut ret_code_buf)), + RetSpec::Aggregate(layout) => CallRet::Aggregate(layout), + }; + + call(addr, &call_args, call_ret, options) } /// Check and handle HRESULT errors (Windows) @@ -1359,7 +1361,7 @@ fn check_hresult(hresult: i32, zelf: &Py, vm: &VirtualMachine) -> Py .new_str(format!("HRESULT: 0x{:08X}", hresult as u32)) .into(); let details: PyObjectRef = vm.ctx.none(); - let exc = vm.invoke_exception(com_error_type, vec![text.clone(), details.clone()])?; + let exc = vm.invoke_exception(&com_error_type, vec![text.clone(), details.clone()])?; let _ = exc.as_object().set_attr("hresult", hresult_obj, vm); let _ = exc.as_object().set_attr("text", text, vm); let _ = exc.as_object().set_attr("details", details, vm); @@ -1374,26 +1376,38 @@ fn check_hresult(hresult: i32, zelf: &Py, vm: &VirtualMachine) -> Py } } -/// Convert raw FFI result to Python object +/// Convert the foreign-call result to a Python object // = GetResult fn convert_raw_result( - raw_result: &mut RawResult, + result: &CallValue, call_info: &CallInfo, vm: &VirtualMachine, ) -> Option { - // Get result as bytes for type conversion - let (result_bytes, result_size) = rustpython_host_env::ctypes::call_result_bytes(raw_result)?; + // Result register image as bytes + size (None for void): pointer/scalar + // returns are pointer/register sized. + let (result_bytes, result_size) = match result { + CallValue::Void => return None, + CallValue::Pointer(ptr) => (ptr.to_ne_bytes().to_vec(), size_of::()), + CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => (bytes.clone(), bytes.len()), + }; + + // Integer view of the return register, for the fallback branches below. + let result_word: usize = match result { + CallValue::Pointer(ptr) => *ptr, + CallValue::Scalar(bytes) | CallValue::Aggregate(bytes) => { + let mut word = [0u8; size_of::()]; + let n = bytes.len().min(word.len()); + word[..n].copy_from_slice(&bytes[..n]); + usize::from_ne_bytes(word) + } + CallValue::Void => 0, + }; // 1. No restype → return as int let restype = match &call_info.restype_obj { None => { // Default: return as int - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return Some(vm.ctx.new_int(val).into()); + return Some(vm.ctx.new_int(result_word as isize).into()); } Some(r) => r, }; @@ -1408,12 +1422,7 @@ fn convert_raw_result( Ok(t) => t, Err(_) => { // Not a type, call it with int result - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return restype.call((val,), vm).ok(); + return restype.call((result_word as isize,), vm).ok(); } }; @@ -1422,15 +1431,36 @@ fn convert_raw_result( // No StgInfo → call restype with int if stg_info.is_none() { - let val = match raw_result { - RawResult::Pointer(p) => *p as isize, - RawResult::Value(v) => *v as isize, - RawResult::Void => return None, - }; - return restype_type.as_object().call((val,), vm).ok(); + return restype_type + .as_object() + .call((result_word as isize,), vm) + .ok(); } let info = stg_info.unwrap(); + // Extract what's needed and release the read guard before constructing any + // instance below: instance construction write-locks the type's StgInfo (to + // finalize it), which would self-deadlock against a held read guard. + let is_pointer_type = info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER); + let has_proto = info.proto.is_some(); + drop(info); + + // py_object: interpret return value as PyObject* and materialize it. + if let Ok(type_attr) = restype_type + .as_object() + .get_attr(vm.ctx.intern_str("_type_"), vm) + && let Some(type_str) = type_attr.downcast_ref::() + && type_str.to_str() == Some("O") + { + let ptr = NonNull::new(result_word as *mut PyObject).or_else(|| { + vm.set_exception(Some(vm.new_value_error("PyObject is NULL"))); + None + })?; + unsafe { + let obj = PyObjectRef::from_raw(ptr); + return Some(obj); + } + } // 5. Simple type with getfunc → use bytes_to_pyobject (info->getfunc) // is_simple_instance returns TRUE for c_int, c_void_p, etc. @@ -1446,9 +1476,9 @@ fn convert_raw_result( // This handles POINTER(T), Structure, Array, etc. // Special handling for POINTER(T) types - set pointer value directly - if info.flags.contains(StgInfoFlags::TYPEFLAG_ISPOINTER) - && info.proto.is_some() - && let RawResult::Pointer(ptr) = raw_result + if is_pointer_type + && has_proto + && let CallValue::Pointer(ptr) = result && let Ok(instance) = restype_type.as_object().call((), vm) { if let Some(pointer) = instance.downcast_ref::() { @@ -1493,7 +1523,7 @@ fn extract_out_values( /// Build final result (main function) fn build_result( - mut raw_result: RawResult, + call_result: CallValue, call_info: &CallInfo, out_buffers: OutBuffers, zelf: &Py, @@ -1502,26 +1532,33 @@ fn build_result( ) -> PyResult { // Check HRESULT on Windows #[cfg(windows)] - if let RawResult::Value(val) = raw_result { + if let CallValue::Scalar(bytes) = &call_result { let is_hresult = call_info .restype_obj .as_ref() .and_then(|t| t.clone().downcast::().ok()) .is_some_and(|t| t.name().to_string() == "HRESULT"); if is_hresult { - check_hresult(val as i32, zelf, vm)?; + let mut word = [0u8; size_of::()]; + let n = bytes.len().min(word.len()); + word[..n].copy_from_slice(&bytes[..n]); + check_hresult(usize::from_ne_bytes(word) as i32, zelf, vm)?; } } - // Convert raw result to Python object - let mut result = convert_raw_result(&mut raw_result, call_info, vm); + // Convert the foreign-call result to a Python object + let mut result = convert_raw_result(&call_result, call_info, vm); // Apply errcheck if set if let Some(errcheck) = zelf.errcheck.read().as_ref() { let args_tuple = PyTuple::new_ref(args.args.clone(), &vm.ctx); let func_obj = zelf.as_object().to_owned(); let result_obj = result.clone().unwrap_or_else(|| vm.ctx.none()); - result = Some(errcheck.call((result_obj, func_obj, args_tuple), vm)?); + let checked = errcheck.call((result_obj, func_obj, args_tuple.clone()), vm)?; + // Returning the original args tuple requests normal result processing. + if !checked.is(&args_tuple) { + result = Some(checked); + } } // Handle OUT parameter return values @@ -1560,44 +1597,34 @@ impl Callable for PyCFuncPtr { let (arguments, out_buffers) = build_callargs(&args, &call_info, paramflags.as_ref(), is_com_method, vm)?; - // 6. Get code pointer - let code_ptr = match func_ptr.or_else(|| zelf.get_code_ptr()) { - Some(cp) => cp, - None => { - debug_assert!(false, "NULL function pointer"); - // In release mode, this will crash - null_code_ptr() - } + // 6. Function address (usize); the unified `call` rejects a NULL address. + let addr = match func_ptr { + Some(cp) => cp.0 as usize, + None => zelf.get_func_ptr(), }; - // 7. Get flags to check for use_last_error/use_errno + // 7. Errno / last-error swap options from flags let flags = Self::_flags_(zelf, vm); - - // 8. Call the function (with use_last_error/use_errno handling) - #[cfg(not(windows))] - let raw_result = { - if flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0 { - rustpython_host_env::ctypes::with_swapped_errno(|| { - ctypes_callproc(code_ptr, &arguments, &call_info) - }) - } else { - ctypes_callproc(code_ptr, &arguments, &call_info) - } + let options = CallOptions { + use_errno: flags & super::base::StgInfoFlags::FUNCFLAG_USE_ERRNO.bits() != 0, + use_last_error: flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0, }; - #[cfg(windows)] - let raw_result = { - if flags & super::base::StgInfoFlags::FUNCFLAG_USE_LASTERROR.bits() != 0 { - rustpython_host_env::ctypes::with_swapped_last_error(|| { - ctypes_callproc(code_ptr, &arguments, &call_info) - }) - } else { - ctypes_callproc(code_ptr, &arguments, &call_info) - } - }; + // 8. Call the function through the unified entry point. + let call_result = ctypes_callproc(addr, &arguments, &call_info.ret, options).map_err( + |err| match err { + CallError::NullFunctionPointer => vm.new_value_error("NULL function pointer"), + CallError::UnknownTypeCode(code) => { + vm.new_type_error(format!("Unsupported argument type: {code}")) + } + CallError::BufferTooSmall { expected, got } => vm.new_value_error(format!( + "argument buffer too small: expected {expected}, got {got}" + )), + }, + )?; // 9. Build result - build_result(raw_result, &call_info, out_buffers, zelf, &args, vm) + build_result(call_result, &call_info, out_buffers, zelf, &args, vm) } } @@ -1628,6 +1655,7 @@ impl AsBuffer for PyCFuncPtr { (Cow::Borrowed(pointer_format()), pointer_size()) }; let desc = BufferDescriptor { + offset: 0, len: itemsize, readonly: false, itemsize, @@ -1917,14 +1945,7 @@ impl PyCThunk { vm: &VirtualMachine, ) -> PyResult { let arg_type_vec: Vec = match arg_types { - Some(args) if !vm.is_none(&args) => args - .try_to_value::>(vm)? - .into_iter() - .map(|item| { - item.downcast::() - .map_err(|_| vm.new_type_error("_argtypes_ must be a sequence of types")) - }) - .collect::>>()?, + Some(args) if !vm.is_none(&args) => extract_arg_types(&args, vm)?, _ => Vec::new(), }; diff --git a/crates/vm/src/stdlib/_ctypes/pointer.rs b/crates/vm/src/stdlib/_ctypes/pointer.rs index 36d86282efd..bcc39fd5745 100644 --- a/crates/vm/src/stdlib/_ctypes/pointer.rs +++ b/crates/vm/src/stdlib/_ctypes/pointer.rs @@ -524,13 +524,12 @@ impl PyCPointer { // c_wchar → str if type_code.as_deref() == Some("u") { - if len == 0 { - return Ok(vm.ctx.new_str("").into()); + if len > 0 + && let Some(s) = unsafe { read_pointer_wchar_slice(ptr_value, start, len, step) } + { + return Ok(vm.ctx.new_str(s).into()); } - return Ok(vm - .ctx - .new_str(unsafe { read_pointer_wchar_slice(ptr_value, start, len, step) }) - .into()); + return Ok(vm.ctx.new_str("").into()); } // other types → list with Pointer_item for each @@ -669,7 +668,7 @@ impl PyCPointer { let ptr_val = if vm.is_none(value) { 0usize } else if let Ok(int_val) = value.try_index(vm) { - int_val.as_bigint().to_usize().unwrap_or(0) + super::simple::bigint_to_i128_wrapping(int_val.as_bigint()) as usize } else { return Err(vm.new_type_error("bytes/string or integer address expected")); }; @@ -685,12 +684,13 @@ impl PyCPointer { // Use write_unaligned for safety on strict-alignment architectures if let Ok(int_val) = value.try_int(vm) { let i = int_val.as_bigint(); + let wrapped = super::simple::bigint_to_i128_wrapping(i); let bytes; let write_value = match size { - 1 => AddressWriteValue::U8(i.to_u8().expect("int too large")), - 2 => AddressWriteValue::I16(i.to_i16().expect("int too large")), - 4 => AddressWriteValue::I32(i.to_i32().expect("int too large")), - 8 => AddressWriteValue::I64(i.to_i64().expect("int too large")), + 1 => AddressWriteValue::U8(wrapped as u8), + 2 => AddressWriteValue::I16(wrapped as i16), + 4 => AddressWriteValue::I32(wrapped as i32), + 8 => AddressWriteValue::I64(wrapped as i64), _ => { bytes = i.to_signed_bytes_le(); AddressWriteValue::Bytes(&bytes) @@ -712,7 +712,8 @@ impl PyCPointer { } // Try bytes - if let Ok(bytes) = value.try_bytes_like(vm, |b| b.to_vec()) { + if value.check_buffer() { + let bytes = value.try_bytes_like(vm, |b| b.to_vec())?; rustpython_host_env::ctypes::write_value_to_address( addr, size, @@ -776,6 +777,7 @@ impl AsBuffer for PyCPointer { let itemsize = stg_info.size; // Pointer types are scalars with ndim=0, shape=() let desc = BufferDescriptor { + offset: 0, len: itemsize, readonly: false, itemsize, diff --git a/crates/vm/src/stdlib/_ctypes/simple.rs b/crates/vm/src/stdlib/_ctypes/simple.rs index 122c23cc25c..9699cef984b 100644 --- a/crates/vm/src/stdlib/_ctypes/simple.rs +++ b/crates/vm/src/stdlib/_ctypes/simple.rs @@ -1,8 +1,7 @@ use super::_ctypes::CArgObject; use super::array::PyCArray; use super::base::{ - CDATA_BUFFER_METHODS, FfiArgValue, PyCData, StgInfo, StgInfoFlags, buffer_to_ffi_value, - bytes_to_pyobject, + CArgValue, CDATA_BUFFER_METHODS, PyCData, StgInfo, StgInfoFlags, bytes_to_pyobject, }; use super::function::PyCFuncPtr; use super::pointer::PyCPointer; @@ -73,6 +72,17 @@ fn new_simple_type( Ok(PyCSimple(PyCData::from_bytes(zeroed_bytes(size), None))) } +pub(super) fn bigint_to_i128_wrapping(value: &malachite_bigint::BigInt) -> i128 { + let bytes = value.to_signed_bytes_le(); + let fill = bytes + .last() + .map_or(0, |byte| if *byte & 0x80 == 0 { 0 } else { u8::MAX }); + let mut wrapped = [fill; 16]; + let len = bytes.len().min(wrapped.len()); + wrapped[..len].copy_from_slice(&bytes[..len]); + i128::from_le_bytes(wrapped) +} + fn set_primitive(_type_: &str, value: &PyObject, vm: &VirtualMachine) -> PyResult { match _type_ { "c" => { @@ -263,11 +273,11 @@ impl PyCSimpleType { let simple_obj: PyObjectRef = simple.into_ref_with_type(vm, cls.clone())?.into(); // from_param returns CArgObject, not the simple type itself let tag = type_str.as_bytes().first().copied().unwrap_or(b'?'); - let ffi_value = buffer_to_ffi_value(type_str, &buffer_bytes); Ok(CArgObject { tag, - value: ffi_value, + value: CArgValue::typed(tag as char, &buffer_bytes), obj: simple_obj, + keep: None, size: 0, offset: 0, } @@ -319,8 +329,9 @@ impl PyCSimpleType { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); return Ok(CArgObject { tag: b'z', - value: FfiArgValue::OwnedPointer(ptr, kept_alive), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(kept_alive), size: 0, offset: 0, } @@ -344,8 +355,9 @@ impl PyCSimpleType { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::OwnedPointer(ptr, holder), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(holder), size: 0, offset: 0, } @@ -373,8 +385,9 @@ impl PyCSimpleType { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); return Ok(CArgObject { tag: b'z', - value: FfiArgValue::OwnedPointer(ptr, kept_alive), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(kept_alive), size: 0, offset: 0, } @@ -385,8 +398,9 @@ impl PyCSimpleType { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::OwnedPointer(ptr, holder), + value: CArgValue::pointer(ptr), obj: value.clone(), + keep: Some(holder), size: 0, offset: 0, } @@ -412,8 +426,9 @@ impl PyCSimpleType { }; return Ok(CArgObject { tag: b'P', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: value.clone(), + keep: None, size: 0, offset: 0, } @@ -429,8 +444,9 @@ impl PyCSimpleType { }; return Ok(CArgObject { tag: b'Z', - value: FfiArgValue::pointer(ptr_val), + value: CArgValue::pointer(ptr_val), obj: value.clone(), + keep: None, size: 0, offset: 0, } @@ -442,8 +458,9 @@ impl PyCSimpleType { Some("O") => { return Ok(CArgObject { tag: b'O', - value: FfiArgValue::pointer(value.get_id()), + value: CArgValue::pointer(value.get_id()), obj: value, + keep: None, size: 0, offset: 0, } @@ -750,7 +767,7 @@ fn value_to_bytes_endian( "b" => { // c_byte - signed char (1 byte) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -758,7 +775,7 @@ fn value_to_bytes_endian( "B" => { // c_ubyte - unsigned char (1 byte) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -766,7 +783,7 @@ fn value_to_bytes_endian( "h" => { // c_short (2 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -774,7 +791,7 @@ fn value_to_bytes_endian( "H" => { // c_ushort (2 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -782,7 +799,7 @@ fn value_to_bytes_endian( "i" => { // c_int (4 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -790,7 +807,7 @@ fn value_to_bytes_endian( "I" => { // c_uint (4 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -798,7 +815,7 @@ fn value_to_bytes_endian( "l" => { // c_long (platform dependent) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -806,7 +823,7 @@ fn value_to_bytes_endian( "L" => { // c_ulong (platform dependent) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -814,7 +831,7 @@ fn value_to_bytes_endian( "q" => { // c_longlong (8 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -822,7 +839,7 @@ fn value_to_bytes_endian( "Q" => { // c_ulonglong (8 bytes) if let Ok(int_val) = value.try_index(vm) { - SimpleStorageValue::Signed(int_val.as_bigint().to_i128().expect("int too large")) + SimpleStorageValue::Signed(bigint_to_i128_wrapping(int_val.as_bigint())) } else { SimpleStorageValue::Zero } @@ -883,10 +900,7 @@ fn value_to_bytes_endian( "P" => { // c_void_p - pointer type (platform pointer size) if let Ok(int_val) = value.try_index(vm) { - let v = int_val - .as_bigint() - .to_usize() - .expect("int too large for pointer"); + let v = bigint_to_i128_wrapping(int_val.as_bigint()) as usize; SimpleStorageValue::Pointer(v) } else { SimpleStorageValue::Zero @@ -896,10 +910,7 @@ fn value_to_bytes_endian( // c_char_p - pointer to char (stores pointer value from int) // PyBytes case is handled in slot_new/set_value with make_z_buffer() if let Ok(int_val) = value.try_index(vm) { - let v = int_val - .as_bigint() - .to_usize() - .expect("int too large for pointer"); + let v = bigint_to_i128_wrapping(int_val.as_bigint()) as usize; SimpleStorageValue::Pointer(v) } else { SimpleStorageValue::Zero @@ -909,10 +920,7 @@ fn value_to_bytes_endian( // c_wchar_p - pointer to wchar_t (stores pointer value from int) // PyStr case is handled in slot_new/set_value with make_wchar_buffer() if let Ok(int_val) = value.try_index(vm) { - let v = int_val - .as_bigint() - .to_usize() - .expect("int too large for pointer"); + let v = bigint_to_i128_wrapping(int_val.as_bigint()) as usize; SimpleStorageValue::Pointer(v) } else { SimpleStorageValue::Zero @@ -1255,17 +1263,10 @@ impl PyCSimple { } impl PyCSimple { - /// Extract the value from this ctypes object as an owned FfiArgValue. - /// The value must be kept alive until after the FFI call completes. - pub(crate) fn to_ffi_value( - &self, - ty: rustpython_host_env::ctypes::FfiType, - _vm: &VirtualMachine, - ) -> Option { + /// Snapshot this object's buffer as a simple-typed foreign-call value. + pub(crate) fn to_carg_value(&self, code: char) -> CArgValue { let buffer = self.0.buffer.read(); - Some(FfiArgValue::Scalar( - rustpython_host_env::ctypes::ffi_value_from_type(&buffer, ty)?, - )) + CArgValue::typed(code, &buffer) } } @@ -1282,6 +1283,7 @@ impl AsBuffer for PyCSimple { let itemsize = stg_info.size; // Simple types are scalars with ndim=0, shape=() let desc = BufferDescriptor { + offset: 0, len: itemsize, readonly: false, itemsize, diff --git a/crates/vm/src/stdlib/_ctypes/structure.rs b/crates/vm/src/stdlib/_ctypes/structure.rs index 96321cd7d55..12ddf8b5dee 100644 --- a/crates/vm/src/stdlib/_ctypes/structure.rs +++ b/crates/vm/src/stdlib/_ctypes/structure.rs @@ -269,14 +269,14 @@ impl PyCStructType { // Determine byte order for format string let big_endian = super::base::is_big_endian(is_swapped); - // Initialize offset, alignment, type flags, and ffi_field_types from base class + // Initialize offset, alignment, type flags, and field_layouts from base class let ( mut offset, mut max_align, mut has_pointer, mut has_union, mut has_bitfield, - mut ffi_field_types, + mut field_layouts, ) = { let bases = cls.bases.read(); if let Some(base) = bases.first() @@ -288,7 +288,7 @@ impl PyCStructType { baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASPOINTER), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASUNION), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD), - baseinfo.ffi_field_types.clone(), + baseinfo.field_layouts.clone(), ) } else { (0, forced_alignment, false, false, false, Vec::new()) @@ -366,8 +366,8 @@ impl PyCStructType { if field_stg.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD) { has_bitfield = true; } - // Collect FFI type for this field - ffi_field_types.push(field_stg.to_ffi_type()); + // Collect the call layout for this field + field_layouts.push(super::base::type_layout(type_obj, &field_stg, vm)); } // Mark field type as finalized (using type as field finalizes it) @@ -552,8 +552,8 @@ impl PyCStructType { stg_info.paramfunc = super::base::ParamFunc::Structure; // Set byte order: swap if _swappedbytes_ is defined stg_info.big_endian = super::base::is_big_endian(is_swapped); - // Store FFI field types for structure passing - stg_info.ffi_field_types = ffi_field_types; + // Store field call layouts for by-value structure passing + stg_info.field_layouts = field_layouts; super::base::set_or_init_stginfo(cls, stg_info); // Process _anonymous_ fields @@ -712,7 +712,7 @@ impl PyCStructure { self_obj: &Py, type_obj: &Py, args: &[PyObjectRef], - kwargs: &indexmap::IndexMap, + kwargs: &crate::function::KwArgsMap, index: usize, vm: &VirtualMachine, ) -> PyResult { @@ -746,7 +746,7 @@ impl PyCStructure { && let Some(name) = tuple.first() && let Some(name_str) = name.downcast_ref::() { - let field_name = name_str.as_str().to_owned(); + let field_name = name_str.as_wtf8().to_owned(); // Check for duplicate in kwargs if kwargs.contains_key(&field_name) { return Err( @@ -784,9 +784,9 @@ impl Initializer for PyCStructure { } // 2. Process keyword arguments - for (key, value) in &args.kwargs { + for (key, value) in args.kwargs { zelf.as_object() - .set_attr(vm.ctx.intern_str(key.as_str()), value.clone(), vm)?; + .set_attr(vm.ctx.intern_str(key), value, vm)?; } Ok(()) @@ -822,6 +822,7 @@ impl AsBuffer for PyCStructure { let buf = PyBuffer::new( zelf.to_owned().into(), BufferDescriptor { + offset: 0, len: buffer_len, readonly: false, itemsize: buffer_len, diff --git a/crates/vm/src/stdlib/_ctypes/union.rs b/crates/vm/src/stdlib/_ctypes/union.rs index e0b4900cbd5..326e1fbd704 100644 --- a/crates/vm/src/stdlib/_ctypes/union.rs +++ b/crates/vm/src/stdlib/_ctypes/union.rs @@ -184,9 +184,9 @@ impl PyCUnionType { let forced_alignment = super::base::get_usize_attr(cls.as_object(), "_align_", 1, vm)?.max(1); - // Initialize size, alignment, type flags, and ffi_field_types from base class + // Initialize size, alignment, type flags, and field_layouts from base class // Note: Union fields always start at offset 0, but we inherit base size/align - let (mut max_size, mut max_align, mut has_pointer, mut has_bitfield, mut ffi_field_types) = { + let (mut max_size, mut max_align, mut has_pointer, mut has_bitfield, mut field_layouts) = { let bases = cls.bases.read(); if let Some(base) = bases.first() && let Some(baseinfo) = base.stg_info_opt() @@ -196,7 +196,7 @@ impl PyCUnionType { core::cmp::max(baseinfo.align, forced_alignment), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASPOINTER), baseinfo.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD), - baseinfo.ffi_field_types.clone(), + baseinfo.field_layouts.clone(), ) } else { (0, forced_alignment, false, false, Vec::new()) @@ -256,8 +256,8 @@ impl PyCUnionType { if field_stg.flags.contains(StgInfoFlags::TYPEFLAG_HASBITFIELD) { has_bitfield = true; } - // Collect FFI type for this field - ffi_field_types.push(field_stg.to_ffi_type()); + // Collect the call layout for this field + field_layouts.push(super::base::type_layout(type_obj, &field_stg, vm)); } // Mark field type as finalized (using type as field finalizes it) @@ -345,8 +345,8 @@ impl PyCUnionType { stg_info.paramfunc = super::base::ParamFunc::Union; // Set byte order: swap if _swappedbytes_ is defined stg_info.big_endian = super::base::is_big_endian(is_swapped); - // Store FFI field types for union passing - stg_info.ffi_field_types = ffi_field_types; + // Store field call layouts for by-value union passing + stg_info.field_layouts = field_layouts; super::base::set_or_init_stginfo(cls, stg_info); // Process _anonymous_ fields @@ -581,7 +581,7 @@ impl PyCUnion { self_obj: &Py, type_obj: &Py, args: &[PyObjectRef], - kwargs: &indexmap::IndexMap, + kwargs: &crate::function::KwArgsMap, index: usize, vm: &VirtualMachine, ) -> PyResult { @@ -617,7 +617,7 @@ impl PyCUnion { && let Some(name) = tuple.first() && let Some(name_str) = name.downcast_ref::() { - let field_name = name_str.as_str().to_owned(); + let field_name = name_str.as_wtf8().to_owned(); // Check for duplicate in kwargs if kwargs.contains_key(&field_name) { return Err( @@ -655,9 +655,9 @@ impl Initializer for PyCUnion { } // 2. Process keyword arguments - for (key, value) in &args.kwargs { + for (key, value) in args.kwargs { zelf.as_object() - .set_attr(vm.ctx.intern_str(key.as_str()), value.clone(), vm)?; + .set_attr(vm.ctx.intern_str(key), value, vm)?; } Ok(()) @@ -685,6 +685,7 @@ impl AsBuffer for PyCUnion { let buf = PyBuffer::new( zelf.to_owned().into(), BufferDescriptor { + offset: 0, len: buffer_len, readonly: false, itemsize: buffer_len, diff --git a/crates/vm/src/stdlib/_functools.rs b/crates/vm/src/stdlib/_functools.rs index 7c6914c2fb4..944a2e8abdb 100644 --- a/crates/vm/src/stdlib/_functools.rs +++ b/crates/vm/src/stdlib/_functools.rs @@ -15,7 +15,6 @@ mod _functools { recursion::ReprGuard, types::{Callable, Constructor, GetDescriptor, Representable}, }; - use indexmap::IndexMap; use rustpython_common::wtf8::Wtf8Buf; #[derive(FromArgs)] @@ -302,7 +301,7 @@ mod _functools { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -373,7 +372,7 @@ mod _functools { // Add new keywords for (key, value) in args.kwargs { - final_keywords.set_item(vm.ctx.intern_str(key.as_str()), value, vm)?; + final_keywords.set_item(vm.ctx.intern_str(key), value, vm)?; } Ok(Self { @@ -432,14 +431,15 @@ mod _functools { combined_args.extend(new_args_iter.cloned()); // Merge keywords from self.keywords and args.kwargs - let mut final_kwargs = IndexMap::new(); + let mut final_kwargs = crate::function::KwArgsMap::default(); // Add keywords from self.keywords for (key, value) in &*keywords { + // `expect_str()` would panic on surrogate keys; keep them as WTF-8. let key_str = key .downcast_ref::() .ok_or_else(|| vm.new_type_error("keywords must be strings"))?; - final_kwargs.insert(key_str.expect_str().to_owned(), value); + final_kwargs.insert(key_str.as_wtf8().to_owned(), value); } // Add keywords from args.kwargs (these override self.keywords) diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index 7021895c9f7..838012a1d0a 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -15,8 +15,14 @@ mod lock { static IMP_LOCK: RawRMutex = RawRMutex::INIT; #[pyfunction] - fn acquire_lock(_vm: &VirtualMachine) { - acquire_lock_for_fork() + fn acquire_lock(vm: &VirtualMachine) { + // Detach while blocking on IMP_LOCK. The import lock is held across + // bytecode by the importlib bootstrap, so its holder can be parked at a + // safepoint mid-hold. Blocking here while attached would keep this + // thread from honoring a stop-the-world request, so a requester could + // wait for this thread while this thread waits for the parked holder. + // Detaching makes the wait park-friendly. + vm.allow_threads(acquire_lock_for_fork); } #[pyfunction] @@ -76,9 +82,14 @@ mod lock { } /// Re-export for fork safety code in posix.rs +/// +/// Runs pre-fork on a normal attached VM thread. Detach while blocking so the +/// wait honors a concurrent stop-the-world request instead of pinning this +/// thread attached on IMP_LOCK; re-attach completes before `stop_the_world`, so +/// the fork requester protocol is unaffected. #[cfg(all(unix, feature = "threading", feature = "host_env"))] -pub(crate) fn acquire_imp_lock_for_fork() { - lock::acquire_lock_for_fork(); +pub(crate) fn acquire_imp_lock_for_fork(vm: &VirtualMachine) { + vm.allow_threads(lock::acquire_lock_for_fork); } #[cfg(all(unix, feature = "threading", feature = "host_env"))] @@ -167,7 +178,6 @@ mod _imp { use crate::{ PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyBytesRef, PyCode, PyMemoryView, PyModule, PyStrRef, PyUtf8StrRef}, - convert::TryFromBorrowedObject, function::OptionalArg, import, version, }; @@ -253,21 +263,26 @@ mod _imp { if let OptionalArg::Present(data) = data && !vm.is_none(&data) { - let buf = crate::protocol::PyBuffer::try_from_borrowed_object(vm, &data)?; - let contiguous = buf.as_contiguous().ok_or_else(|| { - vm.new_buffer_error("get_frozen_object() requires a contiguous buffer") - })?; let invalid_err = || { vm.new_import_error( format!("Frozen object named '{}' is invalid", name.as_str()), name.clone().into_wtf8(), ) }; - let bag = crate::builtins::code::PyVmBag(vm); - let code = - rustpython_compiler_core::marshal::deserialize_code(&mut &contiguous[..], bag) - .map_err(|_| invalid_err())?; - return Ok(PyCode::new_ref_with_bag(vm, code)); + // A non-buffer is a TypeError, not invalid frozen data. The request + // is the one marshal.loads() makes, so that what passes here is + // exactly what it accepts. + crate::protocol::PyBuffer::from_object( + vm, + &data, + crate::protocol::BufferFlags::SIMPLE, + )?; + // The data is a marshalled code object: a whole marshal value, which + // deserialize_code() does not read — it takes the code body alone, + // without the type byte the writer puts in front of it. + let loads = vm.import("marshal", 0)?.get_attr("loads", vm)?; + let code = loads.call((data,), vm).map_err(|_| invalid_err())?; + return code.downcast::().map_err(|_| invalid_err()); } import::make_frozen(vm, name.as_str()) } @@ -306,17 +321,21 @@ mod _imp { .collect() } + #[derive(FromArgs)] + struct FindFrozenArgs { + #[pyarg(positional)] + name: PyUtf8StrRef, + #[pyarg(named, default = false)] + withdata: bool, + } + #[allow(clippy::type_complexity)] #[pyfunction] fn find_frozen( - name: PyUtf8StrRef, - withdata: OptionalArg, + args: FindFrozenArgs, vm: &VirtualMachine, ) -> PyResult>, bool, Option)>> { - if withdata.into_option().is_some() { - // this is keyword-only argument in CPython - unimplemented!(); - } + let FindFrozenArgs { name, withdata } = args; let name_str = name.as_str(); let info = match super::find_frozen(name_str, vm) { @@ -327,6 +346,18 @@ mod _imp { Err(e) => return Err(e.to_pyexception(name_str, vm)), }; + // The data is what get_frozen_object() takes back, i.e. marshalled code. + // Frozen modules are stored in their own encoding, so it has to be + // re-serialized rather than handed out as a view of the stored bytes. + let data = if withdata { + let code = PyCode::new_ref_from_frozen(vm, info.code); + let dumps = vm.import("marshal", 0)?.get_attr("dumps", vm)?; + let bytes = dumps.call((code,), vm)?; + Some(PyMemoryView::from_object(&bytes, vm)?.into_ref(&vm.ctx)) + } else { + None + }; + // When origname is empty (e.g. __hello_only__), return None. // Otherwise return the resolved alias name. let origname_str = super::resolve_frozen_alias(name_str); @@ -335,7 +366,7 @@ mod _imp { } else { Some(vm.ctx.new_utf8_str(origname_str).into()) }; - Ok(Some((None, info.package, origname))) + Ok(Some((data, info.package, origname))) } #[pyfunction] diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index 423d5bc676f..1b4439007b2 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -20,7 +20,8 @@ cfg_select! { } use crate::{ - AsObject, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine, builtins::PyModule, + AsObject, PyObject, PyObjectRef, PyResult, TryFromObject, VirtualMachine, + builtins::{PyModule, PyOSError}, }; pub use _io::{OpenArgs, io_open as open}; use rustpython_host_env::io as host_io; @@ -112,6 +113,10 @@ impl std::os::fd::AsRawFd for Fildes { } #[pymodule] +#[expect( + clippy::std_instead_of_core, + reason = "false positive: core::io items (Cursor, etc.) are unstable (core_io)" +)] mod _io { use super::*; use crate::{ @@ -128,10 +133,10 @@ mod _io { }, common::wtf8::{Wtf8, Wtf8Buf}, convert::ToPyObject, - exceptions::cstring_error, + exceptions::nul_char_error, function::{ - ArgBytesLike, ArgIterable, ArgMemoryBuffer, ArgSize, Either, FsPath, FuncArgs, - IntoFuncArgs, OptionalArg, OptionalOption, PySetterValue, + ArgBytesLike, ArgContiguousBytesLike, ArgIterable, ArgMemoryBuffer, ArgSize, Either, + FsPath, FuncArgs, IntoFuncArgs, OptionalArg, OptionalOption, PySetterValue, }, protocol::{ BufferDescriptor, BufferMethods, BufferResizeGuard, PyBuffer, PyIterReturn, VecBuffer, @@ -146,6 +151,7 @@ mod _io { use alloc::borrow::Cow; use bstr::ByteSlice; use core::{ + hint::cold_path, ops::Range, sync::atomic::{AtomicBool, Ordering}, }; @@ -789,11 +795,41 @@ mod _io { #[pyclass(flags(BASETYPE, HAS_WEAKREF))] impl _TextIOBase { + #[pymethod] + fn read(zelf: PyObjectRef, _size: OptionalArg, vm: &VirtualMachine) -> PyResult { + _unsupported(vm, &zelf, "read") + } + + #[pymethod] + fn write(zelf: PyObjectRef, _b: PyObjectRef, vm: &VirtualMachine) -> PyResult { + _unsupported(vm, &zelf, "write") + } + + #[pymethod] + fn truncate(zelf: PyObjectRef, _pos: OptionalArg, vm: &VirtualMachine) -> PyResult { + _unsupported(vm, &zelf, "truncate") + } + + #[pymethod] + fn readline(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { + _unsupported(vm, &zelf, "readline") + } + + #[pymethod] + fn detach(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { + _unsupported(vm, &zelf, "detach") + } + #[pygetset] fn encoding(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.none() } + #[pygetset] + fn newlines(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + #[pygetset] fn errors(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef { vm.ctx.none() @@ -908,14 +944,17 @@ mod _io { Some(n) => n, None => { // BlockingIOError(errno, msg, characters_written=0) - return Err(vm.invoke_exception( - vm.ctx.exceptions.blocking_io_error.to_owned(), - vec![ - vm.new_pyobj(EAGAIN), - vm.new_pyobj("write could not complete without blocking"), - vm.new_pyobj(0), - ], - )?); + return Err(vm + .new_payload_exception::( + vm.ctx.exceptions.blocking_io_error.to_owned(), + vec![ + vm.new_pyobj(EAGAIN), + vm.new_pyobj("write could not complete without blocking"), + vm.new_pyobj(0), + ] + .into(), + )? + .upcast()); } }; self.write_pos += n as Offset; @@ -1119,14 +1158,17 @@ mod _io { self.buffer[self.write_end as usize..][..avail].copy_from_slice(&buf[..avail]); self.write_end += avail as Offset; self.pos += avail as Offset; - return Err(vm.invoke_exception( - vm.ctx.exceptions.blocking_io_error.to_owned(), - vec![ - vm.new_pyobj(EAGAIN), - vm.new_pyobj("write could not complete without blocking"), - vm.new_pyobj(avail), - ], - )?); + return Err(vm + .new_payload_exception::( + vm.ctx.exceptions.blocking_io_error.to_owned(), + vec![ + vm.new_pyobj(EAGAIN), + vm.new_pyobj("write could not complete without blocking"), + vm.new_pyobj(avail), + ] + .into(), + )? + .upcast()); } Err(e) => return Err(e), } @@ -1165,14 +1207,17 @@ mod _io { self.write_end = buffer_size; // BlockingIOError(errno, msg, characters_written) let chars_written = written + buffer_len; - return Err(vm.invoke_exception( - vm.ctx.exceptions.blocking_io_error.to_owned(), - vec![ - vm.new_pyobj(EAGAIN), - vm.new_pyobj("write could not complete without blocking"), - vm.new_pyobj(chars_written), - ], - )?); + return Err(vm + .new_payload_exception::( + vm.ctx.exceptions.blocking_io_error.to_owned(), + vec![ + vm.new_pyobj(EAGAIN), + vm.new_pyobj("write could not complete without blocking"), + vm.new_pyobj(chars_written), + ] + .into(), + )? + .upcast()); } None => break, } @@ -1213,7 +1258,7 @@ mod _io { let current_size = self.readahead() as usize; - let mut out = vec![0u8; n]; + let mut out = vm.new_zeroed_bytes(n)?; let mut remaining = n; let mut written = 0; if current_size > 0 { @@ -1628,7 +1673,7 @@ mod _io { check_writable(&raw, vm)?; } - data.buffer = vec![0; buffer_size]; + data.buffer = vm.new_zeroed_bytes(buffer_size)?; if Self::READABLE { data.reset_read(); @@ -1893,7 +1938,7 @@ mod _io { if data.writable() { data.flush_rewind(vm)?; } - let mut v = vec![0; n]; + let mut v = vm.new_zeroed_bytes(n)?; data.reset_read(); let r = data .raw_read(Either::A(Some(&mut v)), 0..n, vm)? @@ -2310,7 +2355,7 @@ mod _io { impl Newlines { /// returns position where the new line starts if found, otherwise position at which to /// continue the search after more is read into the buffer - fn find_newline(&self, s: &Wtf8) -> Result { + fn find_newline(self, s: &Wtf8) -> Result { let len = s.len(); match self { Self::Universal | Self::Lf => s.find("\n".as_ref()).map(|p| p + 1).ok_or(len), @@ -2337,10 +2382,10 @@ mod _io { match memchr::memchr(b'\r', remaining) { Some(p) => match remaining.get(p + 1) { Some(&ch_after_cr) => { - let pos_after = p + 2; if ch_after_cr == b'\n' { - break Ok(searched + pos_after); + break Ok(searched + p + 2); } + let pos_after = p + 1; searched += pos_after; remaining = &remaining[pos_after..]; continue; @@ -2820,8 +2865,9 @@ mod _io { } fn validate_errors(errors: &PyRef, vm: &VirtualMachine) -> PyResult<()> { - if errors.as_str().contains('\0') { - return Err(cstring_error(vm)); + if errors.as_pystr().contains_nuls() { + cold_path(); + return Err(nul_char_error(vm)); } vm.state .codec_registry @@ -2860,12 +2906,7 @@ mod _io { } Err(err) => return Err(err), }, - Some(enc) => { - if enc.as_str().contains('\0') { - return Err(cstring_error(vm)); - } - enc - } + Some(enc) => enc, _ => match vm.import("locale", 0) { Ok(locale) => locale .get_attr("getencoding", vm)? @@ -2880,8 +2921,9 @@ mod _io { Err(err) => return Err(err), }, }; - if encoding.as_str().contains('\0') { - return Err(cstring_error(vm)); + if encoding.as_pystr().contains_nuls() { + cold_path(); + return Err(nul_char_error(vm)); } Ok(encoding) } @@ -3033,7 +3075,8 @@ mod _io { let mut write_through = None; if let Some(enc) = args.encoding { - if enc.as_str().contains('\0') && enc.as_str().starts_with("locale") { + if enc.as_pystr().contains_nuls() && enc.as_str().starts_with("locale") { + cold_path(); return Err(vm.new_lookup_error(format!("unknown encoding: {enc}"))); } let resolved = Self::resolve_encoding(Some(enc), vm)?; @@ -3172,19 +3215,45 @@ mod _io { } #[pygetset(setter, name = "_CHUNK_SIZE")] - fn set_chunksize( - &self, - chunk_size: PySetterValue, - vm: &VirtualMachine, - ) -> PyResult<()> { - let mut textio = self.lock(vm)?; - match chunk_size { - PySetterValue::Assign(chunk_size) => textio.chunk_size = chunk_size, - PySetterValue::Delete => Err(vm.new_attribute_error("cannot delete attribute"))?, + fn set_chunksize(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { + { + let textio = self.lock(vm)?; + if vm.is_none(&textio.buffer) { + return Err(vm.new_value_error("underlying buffer has been detached")); + } + } + + let chunk_size: isize = match value { + PySetterValue::Assign(object_value) => { + let integer = object_value.try_index(vm)?; + + integer.try_to_primitive::(vm).map_err(|_| { + let class = object_value.class(); + let type_name = class.name(); + let mut end = type_name.len().min(200); + while !type_name.is_char_boundary(end) { + end -= 1; + } + vm.new_value_error(format!( + "cannot fit '{}' into an index-sized integer", + &type_name[..end] + )) + })? + } + PySetterValue::Delete => { + return Err(vm.new_attribute_error("cannot delete attribute")); + } }; - // TODO: RUSTPYTHON - // Change chunk_size type, validate it manually and throws ValueError if invalid. - // https://github.com/python/cpython/blob/2e9da8e3522764d09f1d6054a2be567e91a30812/Modules/_io/textio.c#L3124-L3143 + + if chunk_size <= 0 { + return Err(vm.new_value_error("a strictly positive integer is required")); + } + + let chunk_size = usize::try_from(chunk_size) + .map_err(|_| vm.new_value_error("a strictly positive integer is required"))?; + + let mut textio = self.lock(vm)?; + textio.chunk_size = chunk_size; Ok(()) } @@ -3260,7 +3329,7 @@ mod _io { use crate::types::PyComparisonOp; if cookie.rich_compare_bool(vm.ctx.new_int(0).as_ref(), PyComparisonOp::Lt, vm)? { return Err( - vm.new_value_error(format!("negative seek position {}", &cookie.repr(vm)?)) + vm.new_value_error(format!("negative seek position {}", cookie.repr(vm)?)) ); } drop(textio); @@ -3295,14 +3364,17 @@ mod _io { *snapshot = Some((cookie.dec_flags, input_chunk.clone())); let decoded = vm.call_method(decoder, "decode", (input_chunk, cookie.need_eof))?; let decoded = check_decoded(decoded, vm)?; - let pos_is_valid = decoded - .as_wtf8() - .is_code_point_boundary(cookie.bytes_to_skip as usize); + // The position is stored both as a count of characters and as + // an offset in bytes, so both have to land inside what was + // just decoded: everything read back from here indexes it. + let num_to_skip = cookie.num_to_skip(); + let pos_is_valid = num_to_skip.chars <= decoded.char_len() + && decoded.as_wtf8().is_code_point_boundary(num_to_skip.bytes); textio.set_decoded_chars(Some(decoded)); if !pos_is_valid { return Err(vm.new_os_error("can't restore logical file position")); } - textio.decoded_chars_used = cookie.num_to_skip(); + textio.decoded_chars_used = num_to_skip; } else { textio.snapshot = Some((cookie.dec_flags, PyBytes::from(vec![]).into_ref(&vm.ctx))) } @@ -4009,7 +4081,10 @@ mod _io { vm.new_runtime_error(format!("reentrant call inside {type_name}.__repr__")) ); }; - let Some(data) = zelf.data.lock() else { + // Detach while blocked, like `lock_opt`: another thread can be + // stopped holding this mutex, and blocking on it while attached + // would leave no safepoint for that stop to complete at. + let Some(data) = zelf.data.lock_wrapped(|do_lock| vm.allow_threads(do_lock)) else { // Reentrant call return Ok(vm.ctx.new_str(Wtf8Buf::from(format!("<{type_name}>")))); }; @@ -4118,6 +4193,37 @@ mod _io { } } + impl SeenNewline { + fn observe(&mut self, text: &Wtf8) { + let bytes = text.as_bytes(); + let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes); + while !self.is_all() { + let Some(i) = matches.next() else { break }; + match bytes[i] { + b'\n' => self.insert(Self::LF), + _ if bytes.get(i + 1) == Some(&b'\n') => { + matches.next(); + self.insert(Self::CRLF); + } + _ => self.insert(Self::CR), + } + } + } + + fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { + match self.bits() { + 1 => "\n".to_pyobject(vm), + 2 => "\r".to_pyobject(vm), + 3 => ("\r", "\n").to_pyobject(vm), + 4 => "\r\n".to_pyobject(vm), + 5 => ("\n", "\r\n").to_pyobject(vm), + 6 => ("\r", "\r\n").to_pyobject(vm), + 7 => ("\r", "\n", "\r\n").to_pyobject(vm), + _ => vm.ctx.none(), + } + } + } + impl DefaultConstructor for IncrementalNewlineDecoder {} #[derive(FromArgs)] @@ -4209,16 +4315,7 @@ mod _io { #[pygetset] fn newlines(&self, vm: &VirtualMachine) -> PyResult { let data = self.lock(vm)?; - Ok(match data.seennl.bits() { - 1 => "\n".to_pyobject(vm), - 2 => "\r".to_pyobject(vm), - 3 => ("\r", "\n").to_pyobject(vm), - 4 => "\r\n".to_pyobject(vm), - 5 => ("\n", "\r\n").to_pyobject(vm), - 6 => ("\r", "\r\n").to_pyobject(vm), - 7 => ("\r", "\n", "\r\n").to_pyobject(vm), - _ => vm.ctx.none(), - }) + Ok(data.seennl.to_pyobject(vm)) } } @@ -4265,20 +4362,7 @@ mod _io { self.seennl.insert(SeenNewline::LF); } } else if !self.translate { - let output = output.as_bytes(); - let mut matches = memchr::memchr2_iter(b'\r', b'\n', output); - while !self.seennl.is_all() { - let Some(i) = matches.next() else { break }; - match output[i] { - b'\n' => self.seennl.insert(SeenNewline::LF), - // if c isn't \n, it can only be \r - _ if output.get(i + 1) == Some(&b'\n') => { - matches.next(); - self.seennl.insert(SeenNewline::CRLF); - } - _ => self.seennl.insert(SeenNewline::CR), - } - } + self.seennl.observe(&output); } else { let bytes = output.as_bytes(); let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes); @@ -4320,6 +4404,8 @@ mod _io { struct StringIO { _base: _TextIOBase, buffer: PyRwLock, + newline: AtomicCell, + seennl: AtomicCell, closed: AtomicCell, } @@ -4328,10 +4414,8 @@ mod _io { #[pyarg(positional, optional)] object: OptionalOption, - // TODO: use this #[pyarg(any, default)] - #[allow(dead_code)] - newline: Newlines, + newline: OptionalOption, } impl Constructor for StringIO { @@ -4341,6 +4425,8 @@ mod _io { Ok(Self { _base: Default::default(), buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))), + newline: AtomicCell::new(Newlines::Lf), + seennl: AtomicCell::new(SeenNewline::empty()), closed: AtomicCell::new(false), }) } @@ -4349,16 +4435,26 @@ mod _io { impl Initializer for StringIO { type Args = StringIONewArgs; - #[allow(unused_variables)] fn init( zelf: PyRef, Self::Args { object, newline }: Self::Args, _vm: &VirtualMachine, ) -> PyResult<()> { - let raw_bytes = object - .flatten() - .map_or_else(Vec::new, |v| v.as_bytes().to_vec()); + let newline = match newline { + OptionalArg::Missing => Newlines::Lf, + OptionalArg::Present(None) => Newlines::Universal, + OptionalArg::Present(Some(newline)) => newline, + }; + let object = object.flatten(); + let raw_bytes = object.as_ref().map_or_else(Vec::new, |v| { + Self::translate_newlines(v.as_wtf8(), newline).into_bytes() + }); *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); + zelf.newline.store(newline); + zelf.seennl.store(SeenNewline::empty()); + if let Some(object) = object { + zelf.observe_newlines(object.as_wtf8(), newline); + } Ok(()) } } @@ -4371,6 +4467,54 @@ mod _io { Err(io_closed_error(vm)) } } + + fn translate_newlines(data: &Wtf8, newline: Newlines) -> Wtf8Buf { + match newline { + Newlines::Universal => data + .replace("\r\n".as_ref(), "\n".as_ref()) + .replace("\r".as_ref(), "\n".as_ref()), + Newlines::Cr => data.replace("\n".as_ref(), "\r".as_ref()), + Newlines::Crlf => data.replace("\n".as_ref(), "\r\n".as_ref()), + Newlines::Passthrough | Newlines::Lf => data.to_owned(), + } + } + + fn observe_newlines(&self, data: &Wtf8, newline: Newlines) { + if matches!(newline, Newlines::Universal | Newlines::Passthrough) { + let mut seennl = self.seennl.load(); + seennl.observe(data); + self.seennl.store(seennl); + } + } + + fn text(bytes: &[u8]) -> &Wtf8 { + // SAFETY: StringIO is populated only from PyStr values, which are valid WTF-8. + unsafe { Wtf8::from_bytes_unchecked(bytes) } + } + + fn char_offset_to_byte(bytes: &[u8], char_offset: usize) -> usize { + let text = Self::text(bytes); + crate::common::str::codepoint_range_end(text, char_offset) + .unwrap_or_else(|| bytes.len() + (char_offset - text.code_points().count())) + } + + fn byte_offset_to_char(bytes: &[u8], byte_offset: usize) -> usize { + let content_len = bytes.len(); + let in_content = byte_offset.min(content_len); + Self::text(&bytes[..in_content]).code_points().count() + + byte_offset.saturating_sub(content_len) + } + + fn read_size(buffer: &BufferedIO, size: Option, newline: Option) -> usize { + let position = buffer.tell() as usize; + let bytes = buffer.cursor.get_ref().get(position..).unwrap_or_default(); + let size_end = size + .and_then(|size| crate::common::str::codepoint_range_end(Self::text(bytes), size)) + .unwrap_or(bytes.len()); + newline + .and_then(|newline| newline.find_newline(Self::text(&bytes[..size_end])).ok()) + .unwrap_or(size_end) + } } #[pyclass(flags(BASETYPE, HAS_DICT, HAS_WEAKREF), with(Constructor, Initializer))] @@ -4395,6 +4539,15 @@ mod _io { self.closed.load() } + #[pygetset] + fn newlines(&self, vm: &VirtualMachine) -> PyResult { + if self.closed.load() { + Err(io_closed_error(vm)) + } else { + Ok(self.seennl.load().to_pyobject(vm)) + } + } + #[pymethod] fn close(&self) { self.closed.store(true); @@ -4403,10 +4556,14 @@ mod _io { // write string to underlying vector #[pymethod] fn write(&self, data: PyStrRef, vm: &VirtualMachine) -> PyResult { - let bytes = data.as_bytes(); - self.buffer(vm)? - .write(bytes) - .ok_or_else(|| vm.new_type_error("Error Writing String")) + let newline = self.newline.load(); + let bytes = Self::translate_newlines(data.as_wtf8(), newline).into_bytes(); + let mut buffer = self.buffer(vm)?; + self.observe_newlines(data.as_wtf8(), newline); + buffer + .write(&bytes) + .ok_or_else(|| vm.new_type_error("Error Writing String"))?; + Ok(data.char_len() as u64) } // return the entire contents of the underlying @@ -4424,9 +4581,36 @@ mod _io { how: OptionalArg, vm: &VirtualMachine, ) -> PyResult { - self.buffer(vm)? - .seek(seekfrom(vm, offset, how)?) - .map_err(|err| os_err(vm, err)) + let offset: isize = ArgSize::try_from_object(vm, offset)?.into(); + let how = how.unwrap_or(0); + let mut buffer = self.buffer(vm)?; + let char_offset = match how { + 0 if offset >= 0 => offset as usize, + 0 => return Err(vm.new_value_error(format!("negative seek position {offset}"))), + 1 | 2 if offset != 0 => { + let kind = if how == 1 { "cur" } else { "end" }; + return Err(vm.new_os_error(format!("can't do nonzero {kind}-relative seeks"))); + } + 1 | 2 => { + let byte_offset = if how == 1 { + buffer.tell() as usize + } else { + buffer.cursor.get_ref().len() + }; + Self::byte_offset_to_char(buffer.cursor.get_ref(), byte_offset) + } + _ => { + return Err( + vm.new_value_error(format!("invalid whence ({how}, should be 0, 1 or 2)")) + ); + } + }; + + let byte_offset = Self::char_offset_to_byte(buffer.cursor.get_ref(), char_offset); + buffer + .seek(SeekFrom::Start(byte_offset as u64)) + .map_err(|err| os_err(vm, err))?; + Ok(char_offset as u64) } // Read k bytes from the object and return. @@ -4434,7 +4618,9 @@ mod _io { // This also increments the stream position by the value of k #[pymethod] fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult { - let data = self.buffer(vm)?.read(size.to_usize()).unwrap_or_default(); + let mut buffer = self.buffer(vm)?; + let size = Self::read_size(&buffer, size.to_usize(), None); + let data = buffer.read(Some(size)).unwrap_or_default(); let value = Wtf8Buf::from_bytes(data) .map_err(|_| vm.new_value_error("Error Retrieving Value"))?; @@ -4443,22 +4629,28 @@ mod _io { #[pymethod] fn tell(&self, vm: &VirtualMachine) -> PyResult { - Ok(self.buffer(vm)?.tell()) + let buffer = self.buffer(vm)?; + Ok(Self::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize) as u64) } #[pymethod] fn readline(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult { - // TODO size should correspond to the number of characters, at the moments its the number of - // bytes. - let input = self.buffer(vm)?.readline(size.to_usize(), vm)?; + let mut buffer = self.buffer(vm)?; + let size = Self::read_size(&buffer, size.to_usize(), Some(self.newline.load())); + let input = buffer.read(Some(size)).unwrap_or_default(); Wtf8Buf::from_bytes(input).map_err(|_| vm.new_value_error("Error Retrieving Value")) } #[pymethod] fn truncate(&self, pos: OptionalSize, vm: &VirtualMachine) -> PyResult { let mut buffer = self.buffer(vm)?; - let pos = pos.try_usize(vm)?; - Ok(buffer.truncate(pos)) + let pos = match pos.try_usize(vm)? { + Some(pos) => pos, + None => Self::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize), + }; + let byte_pos = Self::char_offset_to_byte(buffer.cursor.get_ref(), pos); + buffer.truncate(Some(byte_pos)); + Ok(pos) } #[pygetset] @@ -4471,7 +4663,7 @@ mod _io { let buffer = zelf.buffer(vm)?; let content = Wtf8Buf::from_bytes(buffer.getvalue()) .map_err(|_| vm.new_value_error("Error Retrieving Value"))?; - let pos = buffer.tell(); + let pos = Self::byte_offset_to_char(buffer.cursor.get_ref(), buffer.tell() as usize); drop(buffer); // Get __dict__ if it exists and is non-empty @@ -4480,11 +4672,18 @@ mod _io { _ => vm.ctx.none(), }; + let newline = match zelf.newline.load() { + Newlines::Universal => vm.ctx.none(), + Newlines::Passthrough => vm.ctx.new_str("").into(), + Newlines::Lf => vm.ctx.new_str("\n").into(), + Newlines::Cr => vm.ctx.new_str("\r").into(), + Newlines::Crlf => vm.ctx.new_str("\r\n").into(), + }; + // Return (content, newline, position, dict) - // TODO: store actual newline setting when it's implemented Ok(vm.ctx.new_tuple(vec![ vm.ctx.new_str(content).into(), - vm.ctx.new_str("\n").into(), + newline, vm.ctx.new_int(pos).into(), dict_obj, ])) @@ -4504,18 +4703,28 @@ mod _io { } let content: PyStrRef = state[0].clone().try_into_value(vm)?; - // state[1] is newline - TODO: use when newline handling is implemented - let pos: u64 = state[2].clone().try_into_value(vm)?; + let newline = Newlines::try_from_object(vm, state[1].clone())?; + let pos: isize = ArgSize::try_from_object(vm, state[2].clone())?.into(); + if pos < 0 { + return Err(vm.new_value_error("negative seek position")); + } let dict = &state[3]; // Set content and position let raw_bytes = content.as_bytes().to_vec(); let mut buffer = zelf.buffer.write(); *buffer = BufferedIO::new(Cursor::new(raw_bytes)); + let byte_pos = Self::char_offset_to_byte(buffer.cursor.get_ref(), pos as usize); buffer - .seek(SeekFrom::Start(pos)) + .seek(SeekFrom::Start(byte_pos as u64)) .map_err(|err| os_err(vm, err))?; drop(buffer); + zelf.newline.store(newline); + let mut seennl = SeenNewline::empty(); + if matches!(newline, Newlines::Universal | Newlines::Passthrough) { + seennl.observe(content.as_wtf8()); + } + zelf.seennl.store(seennl); // Set __dict__ if provided if !vm.is_none(dict) { @@ -4620,8 +4829,12 @@ mod _io { } #[pymethod] - fn write(&self, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + fn write(&self, data: ArgContiguousBytesLike, vm: &VirtualMachine) -> PyResult { let mut buffer = self.try_resizable(vm)?; + // Acquiring the buffer can run `__buffer__`, which may have closed us. + if self.closed.load() { + return Err(io_closed_error(vm)); + } data.with_ref(|b| buffer.write(b)) .ok_or_else(|| vm.new_type_error("Error Writing Bytes")) } @@ -4644,8 +4857,20 @@ mod _io { } #[pymethod] - fn readinto(&self, obj: ArgMemoryBuffer, vm: &VirtualMachine) -> PyResult { - let mut buf = self.buffer(vm)?; + fn readinto(zelf: &Py, obj: ArgMemoryBuffer, vm: &VirtualMachine) -> PyResult { + // Reading locks this object, and a destination that views it locks + // it too, so such a destination is filled after the read is done. + if obj.source_object().is(zelf.as_object()) { + let mut data = vm.new_zeroed_bytes(obj.len())?; + let ret = zelf + .buffer(vm)? + .cursor + .read(&mut data) + .map_err(|_| vm.new_value_error("Error readinto from Take"))?; + obj.borrow_buf_mut()[..ret].copy_from_slice(&data[..ret]); + return Ok(ret); + } + let mut buf = zelf.buffer(vm)?; let ret = buf .cursor .read(&mut obj.borrow_buf_mut()) @@ -5241,10 +5466,10 @@ mod _io { if vm.state.config.settings.warn_default_encoding { let mut stacklevel = stacklevel.unwrap_or(2); if stacklevel > 1 - && let Some(frame) = vm.current_frame() + && let Some(code) = crate::frame::current_code() && let Some(stdlib_dir) = vm.state.config.paths.stdlib_dir.as_deref() { - let path = frame.code.source_path().as_str(); + let path = code.source_path().as_str(); if !path.starts_with(stdlib_dir) { stacklevel = stacklevel.saturating_sub(1); } @@ -5598,7 +5823,7 @@ mod fileio { } let handle = zelf.get_fd(vm)?; let bytes = if let Some(read_byte) = read_byte.to_usize() { - let mut bytes = vec![0; read_byte]; + let mut bytes = vm.new_zeroed_bytes(read_byte)?; // Loop on EINTR (PEP 475) let n = loop { match vm.allow_threads(|| host_io::read_once(handle, &mut bytes)) { @@ -5642,6 +5867,26 @@ mod fileio { Ok(Some(bytes)) } + /// One `read()` into `buf`, retried on EINTR (PEP 475). `None` on EAGAIN. + fn read_once_into( + zelf: &Py, + handle: crt_fd::Borrowed<'_>, + buf: &mut [u8], + vm: &VirtualMachine, + ) -> PyResult> { + loop { + match vm.allow_threads(|| host_io::read_once(handle, buf)) { + Ok(n) => return Ok(Some(n)), + Err(e) if host_io::is_interrupted_error(&e) => { + vm.check_signals()?; + } + // Non-blocking mode: return None if EAGAIN + Err(e) if host_io::is_would_block_error(&e) => return Ok(None), + Err(e) => return Err(Self::io_error(zelf, e, vm)), + } + } + } + #[pymethod] fn readinto( zelf: &Py, @@ -5657,24 +5902,28 @@ mod fileio { let handle = zelf.get_fd(vm)?; - let mut buf = obj.borrow_buf_mut(); - // Loop on EINTR (PEP 475) - let ret = loop { - match vm.allow_threads(|| host_io::read_once(handle, &mut buf)) { - Ok(n) => break n, - Err(e) if host_io::is_interrupted_error(&e) => { - vm.check_signals()?; - continue; - } - // Non-blocking mode: return None if EAGAIN - Err(e) if host_io::is_would_block_error(&e) => { - return Ok(None); - } - Err(e) => return Err(Self::io_error(zelf, e, vm)), - } - }; - - Ok(Some(ret)) + if host_io::reads_without_waiting(handle) { + // The read answers from the file itself, so it returns without + // waiting on anyone; write where the caller asked directly. + // Seekability is not the question -- a pipe on Windows seeks. + let mut buf = obj.borrow_buf_mut(); + return Self::read_once_into(zelf, handle, &mut buf, vm); + } + + // A pipe, socket or terminal answers only when the other end + // writes, which may be never. Holding the export for the whole + // call is what keeps the target from being resized meanwhile, as a + // Py_buffer does; but reaching its bytes takes a lock that every + // other thread touching the same object waits on, and a thread + // waiting on a lock never reaches a safepoint, so holding that one + // across the wait stops the world from being stopped at all. Read + // aside and take the lock for the copy. + let mut scratch = vm.new_zeroed_bytes(obj.len())?; + let ret = Self::read_once_into(zelf, handle, &mut scratch, vm)?; + if let Some(n) = ret { + obj.borrow_buf_mut()[..n].copy_from_slice(&scratch[..n]); + } + Ok(ret) } #[pymethod] @@ -5692,9 +5941,14 @@ mod fileio { let handle = zelf.get_fd(vm)?; + // A pipe, socket or terminal takes the bytes only when the other + // end makes room, which may be never; see readinto above for what + // holding the source's lock across that wait costs. + let buf = obj.borrow_buf_unlocked(vm)?; + // Loop on EINTR (PEP 475) let len = loop { - match obj.with_ref(|b| vm.allow_threads(|| host_io::write_once(handle, b))) { + match vm.allow_threads(|| host_io::write_once(handle, &buf)) { Ok(n) => break n, Err(e) if host_io::is_interrupted_error(&e) => { vm.check_signals()?; diff --git a/crates/vm/src/stdlib/_operator.rs b/crates/vm/src/stdlib/_operator.rs index e4db046053b..5e72ef03eb4 100644 --- a/crates/vm/src/stdlib/_operator.rs +++ b/crates/vm/src/stdlib/_operator.rs @@ -610,7 +610,7 @@ mod _operator { } for (key, value) in kwargs { result.push_str(", "); - result.push_str(key); + result.push_wtf8(key); result.push_char('='); result.push_wtf8(value.repr(vm)?.as_wtf8()); } diff --git a/crates/vm/src/stdlib/_signal.rs b/crates/vm/src/stdlib/_signal.rs index e3d12568d26..5abfd327553 100644 --- a/crates/vm/src/stdlib/_signal.rs +++ b/crates/vm/src/stdlib/_signal.rs @@ -177,7 +177,9 @@ pub(crate) mod _signal { module: &Py, vm: &VirtualMachine, ) { - if vm.state.config.settings.install_signal_handlers { + // Process-global signal disposition is owned by the main interpreter only. + // Subinterpreters (PEP 734) must not reinstall SIGINT / probe handlers. + if vm.state.is_main_interpreter() && vm.state.config.settings.install_signal_handlers { let sig_dfl = vm.new_pyobj(SIG_DFL as u8); let sig_ign = vm.new_pyobj(SIG_IGN as u8); @@ -335,6 +337,10 @@ pub(crate) mod _signal { } #[cfg(windows)] + #[expect( + clippy::std_instead_of_core, + reason = "false positive: core::io::ErrorKind is unstable (core_io)" + )] let is_socket = if fd != INVALID_WAKEUP { host_signal::wakeup_fd_is_socket(fd).map_err(|err| { if err.kind() == std::io::ErrorKind::InvalidInput { @@ -425,6 +431,13 @@ pub(crate) mod _signal { } #[pyfunction] + #[cfg_attr( + not(any(unix, windows)), + expect( + clippy::unnecessary_wraps, + reason = "WASI does not support signals yet" + ) + )] fn valid_signals(vm: &VirtualMachine) -> PyResult { use crate::PyPayload; use crate::builtins::PySet; @@ -442,7 +455,7 @@ pub(crate) mod _signal { } #[cfg(unix)] - fn sigset_to_pyset(mask: &libc::sigset_t, vm: &VirtualMachine) -> PyResult { + fn sigset_to_pyset(mask: libc::sigset_t, vm: &VirtualMachine) -> PyResult { use crate::PyPayload; use crate::builtins::PySet; let set = PySet::default().into_ref(&vm.ctx); @@ -491,7 +504,7 @@ pub(crate) mod _signal { signal::check_signals(vm)?; // Convert old mask to Python set - sigset_to_pyset(&old_mask, vm) + sigset_to_pyset(old_mask, vm) } #[cfg(any(unix, windows))] diff --git a/crates/vm/src/stdlib/_sre.rs b/crates/vm/src/stdlib/_sre.rs index 1f62b48b137..a9f98ca7015 100644 --- a/crates/vm/src/stdlib/_sre.rs +++ b/crates/vm/src/stdlib/_sre.rs @@ -3,8 +3,8 @@ pub(crate) use _sre::module_def; #[pymodule] mod _sre { use crate::{ - Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromBorrowedObject, - TryFromObject, VirtualMachine, atomic_func, + Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, + atomic_func, builtins::{ PyCallableIterator, PyDictRef, PyGenericAlias, PyInt, PyList, PyListRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyTypeRef, @@ -13,7 +13,7 @@ mod _sre { common::{ascii, hash::PyHash}, convert::ToPyObject, function::{ArgCallable, OptionalArg, PosArgs, PyComparisonValue}, - protocol::{PyBuffer, PyCallable, PyMappingMethods}, + protocol::{BufferFlags, PyBuffer, PyCallable, PyMappingMethods}, stdlib::sys, types::{AsMapping, Comparable, Hashable, Representable}, }; @@ -22,8 +22,8 @@ mod _sre { use itertools::Itertools; use num_traits::ToPrimitive; use rustpython_sre_engine::{ - Request, SearchIter, SreFlag, State, StrDrive, - string::{lower_ascii, lower_unicode, upper_unicode}, + Request, SearchIter, SreFlag, State, StrDrive, StringCursor, + string::{lower_ascii, lower_unicode}, }; #[pyattr] @@ -41,8 +41,7 @@ mod _sre { #[pyfunction] fn unicode_iscased(ch: i32) -> bool { - let ch = ch as u32; - ch != lower_unicode(ch) || ch != upper_unicode(ch) + char::from_u32(ch as u32).is_some_and(rustpython_unicode::case::is_cased) } #[pyfunction] @@ -71,19 +70,146 @@ mod _sre { } } - impl SreStr for &Wtf8 { + /// A `str` subject with non-ASCII characters, driven through the string's + /// own character-index table. + /// + /// The `&Wtf8` drive answers `count` and `create_cursor` by decoding from + /// the start of the subject, so both are O(n) and a scan that restarts at + /// successive positions walks the subject once per position. `PyStr` + /// already caches its character length and can resolve a character index to + /// a byte offset in constant time, so this drive asks the string instead of + /// re-deriving: the table it builds on the first lookup is shared by every + /// later one, including by `Match` objects that outlive the scan and have + /// no cursor of their own to move relative to. + /// + /// Stepping is the `&Wtf8` drive's, unchanged -- the subject is the same + /// buffer, decoded the same way. Only the two operations that resolve a + /// position from scratch differ. + #[derive(Clone, Copy)] + struct Utf8Str<'a>(&'a Py); + + impl StrDrive for Utf8Str<'_> { + fn count(&self) -> usize { + self.0.char_len() + } + + fn create_cursor(&self, n: usize) -> StringCursor { + // `StringCursor`'s pointer is private to the engine, so the cursor + // is taken from the `&Wtf8` drive at the start of the suffix that + // begins at `n` -- an O(1) reslice -- rather than built here. + let suffix = &self.0.as_wtf8()[self.0.char_index_to_byte(n)..]; + let mut cursor = <&Wtf8 as StrDrive>::create_cursor(&suffix, 0); + cursor.position = n; + cursor + } + + fn adjust_cursor(&self, cursor: &mut StringCursor, n: usize) { + // Rebuilding is O(1), so it is never the slower branch and the + // `&Wtf8` drive's walk-or-restart choice does not apply. + *cursor = self.create_cursor(n); + } + + fn advance(cursor: &mut StringCursor) -> u32 { + <&Wtf8 as StrDrive>::advance(cursor) + } + + fn peek(cursor: &StringCursor) -> u32 { + <&Wtf8 as StrDrive>::peek(cursor) + } + + fn skip(cursor: &mut StringCursor, n: usize) { + <&Wtf8 as StrDrive>::skip(cursor, n) + } + + fn back_advance(cursor: &mut StringCursor) -> u32 { + <&Wtf8 as StrDrive>::back_advance(cursor) + } + + fn back_peek(cursor: &StringCursor) -> u32 { + <&Wtf8 as StrDrive>::back_peek(cursor) + } + + fn back_skip(cursor: &mut StringCursor, n: usize) { + <&Wtf8 as StrDrive>::back_skip(cursor, n) + } + } + + impl SreStr for Utf8Str<'_> { fn slice(&self, start: usize, end: usize, vm: &VirtualMachine) -> PyObjectRef { + let end = self.0.char_index_to_byte(end); + let start = self.0.char_index_to_byte(start).min(end); vm.ctx - .new_str( - self.code_points() - .take(end) - .skip(start) - .collect::(), - ) + .new_str(self.0.as_wtf8()[start..end].to_owned()) .into() } } + /// An all-ASCII `str` subject, driven over its bytes. + /// + /// For ASCII a character index *is* a byte index, so `&[u8]`'s cursor + /// arithmetic is already the right arithmetic: `count` is the byte length + /// and `create_cursor` is a pointer offset. The `&Wtf8` drive has to count + /// code points from the start of the subject to answer either, once per + /// `Request`, which makes a scan that restarts at successive positions -- + /// `finditer`, or `re` module functions called in a loop -- walk the + /// subject again on every call. + /// + /// Matching is unaffected: `StrDrive` carries no unicode semantics of its + /// own, because the engine keys every unicode decision on the compiled + /// pattern's opcode rather than on the subject type. Only `slice` differs + /// from the `&[u8]` impl, to hand back `str` instead of `bytes`. + #[derive(Clone, Copy)] + struct AsciiStr<'a>(&'a [u8]); + + impl StrDrive for AsciiStr<'_> { + fn count(&self) -> usize { + <&[u8] as StrDrive>::count(&self.0) + } + + fn create_cursor(&self, n: usize) -> StringCursor { + <&[u8] as StrDrive>::create_cursor(&self.0, n) + } + + fn adjust_cursor(&self, cursor: &mut StringCursor, n: usize) { + <&[u8] as StrDrive>::adjust_cursor(&self.0, cursor, n) + } + + fn advance(cursor: &mut StringCursor) -> u32 { + <&[u8] as StrDrive>::advance(cursor) + } + + fn peek(cursor: &StringCursor) -> u32 { + <&[u8] as StrDrive>::peek(cursor) + } + + fn skip(cursor: &mut StringCursor, n: usize) { + <&[u8] as StrDrive>::skip(cursor, n) + } + + fn back_advance(cursor: &mut StringCursor) -> u32 { + <&[u8] as StrDrive>::back_advance(cursor) + } + + fn back_peek(cursor: &StringCursor) -> u32 { + <&[u8] as StrDrive>::back_peek(cursor) + } + + fn back_skip(cursor: &mut StringCursor, n: usize) { + <&[u8] as StrDrive>::back_skip(cursor, n) + } + } + + impl SreStr for AsciiStr<'_> { + fn slice(&self, start: usize, end: usize, vm: &VirtualMachine) -> PyObjectRef { + let end = end.min(self.0.len()); + let start = start.min(end); + // The subject is ASCII, so any span of it is valid UTF-8 and the + // span is a reslice rather than a walk from the subject's start. + let s = str::from_utf8(&self.0[start..end]).expect("ascii subject"); + vm.ctx.new_str(s).into() + } + } + #[pyfunction] fn compile( pattern: PyObjectRef, @@ -147,11 +273,9 @@ mod _sre { let mut items = Vec::with_capacity(1); let v = template.borrow_vec(); let literal = v.first().ok_or_else(err)?.clone(); - let trunks = v[1..].chunks_exact(2); - - if !trunks.remainder().is_empty() { + let (trunks, []) = v[1..].as_chunks::<2>() else { return Err(err()); - } + }; for trunk in trunks { let index: usize = trunk[0] @@ -203,32 +327,66 @@ mod _sre { } macro_rules! with_sre_str { - ($pattern:expr, $string:expr, $vm:expr, $f:expr) => { + ($pattern:expr, $string:expr, $vm:expr, $f:expr) => {{ + // Bind once: the branches only borrow the subject, and callers pass + // a temporary (`&x.clone()`) that would otherwise be rebuilt per arm. + let subject = $string; if $pattern.isbytes { - Pattern::with_bytes($string, $vm, $f) + Pattern::with_bytes(subject, $vm, $f) + } else if Pattern::is_ascii_str(subject) { + Pattern::with_ascii_str(subject, $vm, $f) } else { - Pattern::with_str($string, $vm, $f) + Pattern::with_utf8_str(subject, $vm, $f) } - }; + }}; } #[pyclass(with(Hashable, Comparable, Representable), flags(HAS_WEAKREF))] impl Pattern { + fn downcast_str<'a>(string: &'a PyObject, vm: &VirtualMachine) -> PyResult<&'a Py> { + string.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!("expected string got '{}'", string.class())) + }) + } + fn with_str(string: &PyObject, vm: &VirtualMachine, f: F) -> PyResult where F: FnOnce(&Wtf8) -> PyResult, { - let string = string.downcast_ref::().ok_or_else(|| { - vm.new_type_error(format!("expected string got '{}'", string.class())) - })?; - f(string.as_wtf8()) + f(Self::downcast_str(string, vm)?.as_wtf8()) + } + + /// Whether a `str` subject can take the [`AsciiStr`] drive. + /// + /// `PyStr` already knows: `StrKind` is decided when the string is + /// built, so this is a field load rather than a scan. A non-`str` + /// argument answers `false` and is reported by [`Self::with_utf8_str`]. + fn is_ascii_str(string: &PyObject) -> bool { + string + .downcast_ref::() + .is_some_and(|s| s.kind().is_ascii()) + } + + fn with_ascii_str(string: &PyObject, vm: &VirtualMachine, f: F) -> PyResult + where + F: FnOnce(AsciiStr<'_>) -> PyResult, + { + let string = Self::downcast_str(string, vm)?; + f(AsciiStr(string.as_wtf8().as_bytes())) + } + + fn with_utf8_str(string: &PyObject, vm: &VirtualMachine, f: F) -> PyResult + where + F: FnOnce(Utf8Str<'_>) -> PyResult, + { + f(Utf8Str(Self::downcast_str(string, vm)?)) } fn with_bytes(string: &PyObject, vm: &VirtualMachine, f: F) -> PyResult where F: FnOnce(&[u8]) -> PyResult, { - PyBuffer::try_from_borrowed_object(vm, string)?.contiguous_or_collect(f) + PyBuffer::from_object(vm, string, BufferFlags::SIMPLE)?.contiguous_or_collect(f) } #[pymethod(name = "match")] @@ -472,15 +630,7 @@ mod _sre { } FilterType::Template(template) => { let m = Match::new(&mut iter.state, zelf.clone(), string.clone()); - // template.expand(m)? - // let mut list = vec![template.literal.clone()]; - sub_list.push(template.literal.clone()); - for (index, literal) in template.items.iter().cloned() { - if let Some(item) = m.get_slice(index, s, vm) { - sub_list.push(item); - } - sub_list.push(literal); - } + m.expand_template(template, s, &mut sub_list, vm); } }; @@ -509,7 +659,7 @@ mod _sre { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -608,7 +758,7 @@ mod _sre { regs: Vec<(isize, isize)>, } - #[pyclass(with(AsMapping, Representable))] + #[pyclass(with(AsMapping, Representable), flags(DISALLOW_INSTANTIATION))] impl Match { pub(crate) fn new(state: &mut State, pattern: PyRef, string: PyObjectRef) -> Self { let string_position = state.cursor.position; @@ -703,10 +853,19 @@ mod _sre { } #[pymethod] - fn expand(zelf: PyRef, template: PyStrRef, vm: &VirtualMachine) -> PyResult { - let re = vm.import("re", 0)?; - let func = re.get_attr("_expand", vm)?; - func.call((zelf.pattern.clone(), zelf, template), vm) + fn expand(zelf: PyRef, template: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let template = Template::compile(zelf.pattern.clone(), template, vm)?; + with_sre_str!(zelf.pattern, &zelf.string, vm, |s| { + let mut list: Vec = Vec::new(); + zelf.expand_template(&template, s, &mut list, vm); + + let join_type: PyObjectRef = if zelf.pattern.isbytes { + vm.ctx.new_bytes(vec![]).into() + } else { + vm.ctx.new_str(ascii!("")).into() + }; + vm.call_method(&join_type, "join", (PyList::from(list).into_pyobject(vm),)) + }) } #[pymethod] @@ -821,12 +980,32 @@ mod _sre { Some(str_drive.slice(start as usize, end as usize, vm)) } + /// Expand an already-compiled template against this match, appending the + /// resulting literal/group segments to `list`. Shared by `expand` and + /// `Pattern.sub` so the template-filling logic lives in one place; the + /// caller is responsible for compiling the template (once) beforehand. + fn expand_template( + &self, + template: &Template, + str_drive: S, + list: &mut Vec, + vm: &VirtualMachine, + ) { + list.push(template.literal.clone()); + for (index, literal) in template.items.iter().cloned() { + if let Some(item) = self.get_slice(index, str_drive, vm) { + list.push(item); + } + list.push(literal); + } + } + #[pyclassmethod] fn __class_getitem__( cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } diff --git a/crates/vm/src/stdlib/_symtable.rs b/crates/vm/src/stdlib/_symtable.rs index 4945cbeedce..eb0ecaa87d7 100644 --- a/crates/vm/src/stdlib/_symtable.rs +++ b/crates/vm/src/stdlib/_symtable.rs @@ -9,78 +9,67 @@ mod _symtable { types::Representable, }; use alloc::fmt; - use rustpython_codegen::symboltable::{ - CompilerScope, Symbol, SymbolFlags, SymbolScope, SymbolTable, - }; + use rustpython_codegen::symboltable::{CompilerScope, SymbolFlags, SymbolScope, SymbolTable}; + + /// [CPython's `SCOPE_OFFSET`](https://github.com/python/cpython/blob/v3.14.6/Include/internal/pycore_symtable.h#L176) + const SCOPE_OFFSET: i32 = 12; // Consts as defined at // https://github.com/python/cpython/blob/6cb20a219a860eaf687b2d968b41c480c7461909/Include/internal/pycore_symtable.h#L156 #[pyattr] - pub(super) const DEF_GLOBAL: i32 = 1; - - #[pyattr] - pub(super) const DEF_LOCAL: i32 = 2; + pub(super) const DEF_GLOBAL: i32 = SymbolFlags::DEF_GLOBAL.bits() as i32; #[pyattr] - pub(super) const DEF_PARAM: i32 = 2 << 1; + pub(super) const DEF_LOCAL: i32 = SymbolFlags::DEF_LOCAL.bits() as i32; #[pyattr] - pub(super) const DEF_NONLOCAL: i32 = 2 << 2; + pub(super) const DEF_PARAM: i32 = SymbolFlags::DEF_PARAM.bits() as i32; #[pyattr] - pub(super) const USE: i32 = 2 << 3; + pub(super) const DEF_NONLOCAL: i32 = SymbolFlags::DEF_NONLOCAL.bits() as i32; #[pyattr] - pub(super) const DEF_FREE: i32 = 2 << 4; + pub(super) const USE: i32 = SymbolFlags::USE.bits() as i32; #[pyattr] - pub(super) const DEF_FREE_CLASS: i32 = 2 << 5; + pub(super) const DEF_FREE_CLASS: i32 = SymbolFlags::DEF_FREE_CLASS.bits() as i32; #[pyattr] - pub(super) const DEF_IMPORT: i32 = 2 << 6; + pub(super) const DEF_IMPORT: i32 = SymbolFlags::DEF_IMPORT.bits() as i32; #[pyattr] - pub(super) const DEF_ANNOT: i32 = 2 << 7; + pub(super) const DEF_ANNOT: i32 = SymbolFlags::DEF_ANNOT.bits() as i32; #[pyattr] - pub(super) const DEF_COMP_ITER: i32 = 2 << 8; + pub(super) const DEF_COMP_ITER: i32 = SymbolFlags::DEF_COMP_ITER.bits() as i32; #[pyattr] - pub(super) const DEF_TYPE_PARAM: i32 = 2 << 9; + pub(super) const DEF_TYPE_PARAM: i32 = SymbolFlags::DEF_TYPE_PARAM.bits() as i32; #[pyattr] - pub(super) const DEF_COMP_CELL: i32 = 2 << 10; + pub(super) const DEF_COMP_CELL: i32 = SymbolFlags::DEF_COMP_CELL.bits() as i32; #[pyattr] - pub(super) const DEF_BOUND: i32 = DEF_LOCAL | DEF_PARAM | DEF_IMPORT; - - #[pyattr] - pub(super) const SCOPE_OFFSET: i32 = 12; + pub(super) const DEF_BOUND: i32 = SymbolFlags::DEF_BOUND.bits() as i32; #[pyattr] pub(super) const SCOPE_MASK: i32 = DEF_GLOBAL | DEF_LOCAL | DEF_PARAM | DEF_NONLOCAL; #[pyattr] - pub(super) const LOCAL: i32 = 1; - - #[pyattr] - pub(super) const GLOBAL_EXPLICIT: i32 = 2; + pub(super) const LOCAL: i32 = SymbolScope::Local.as_i32(); #[pyattr] - pub(super) const GLOBAL_IMPLICIT: i32 = 3; + pub(super) const GLOBAL_EXPLICIT: i32 = SymbolScope::GlobalExplicit.as_i32(); #[pyattr] - pub(super) const FREE: i32 = 4; + pub(super) const GLOBAL_IMPLICIT: i32 = SymbolScope::GlobalImplicit.as_i32(); #[pyattr] - pub(super) const CELL: i32 = 5; + pub(super) const FREE: i32 = SymbolScope::Free.as_i32(); #[pyattr] - pub(super) const GENERATOR: i32 = 1; - - #[pyattr] - pub(super) const GENERATOR_EXPRESSION: i32 = 2; + pub(super) const CELL: i32 = SymbolScope::Cell.as_i32(); #[pyattr] pub(super) const SCOPE_OFF: i32 = SCOPE_OFFSET; @@ -98,16 +87,13 @@ mod _symtable { pub(super) const TYPE_ANNOTATION: i32 = 3; #[pyattr] - pub(super) const TYPE_TYPE_VAR_BOUND: i32 = 4; - - #[pyattr] - pub(super) const TYPE_TYPE_ALIAS: i32 = 5; + pub(super) const TYPE_TYPE_ALIAS: i32 = 4; #[pyattr] - pub(super) const TYPE_TYPE_PARAMETERS: i32 = 6; + pub(super) const TYPE_TYPE_PARAMETERS: i32 = 5; #[pyattr] - pub(super) const TYPE_TYPE_VARIABLE: i32 = 7; + pub(super) const TYPE_TYPE_VARIABLE: i32 = 6; #[pyfunction] fn symtable( @@ -162,7 +148,9 @@ mod _symtable { CompilerScope::Class => TYPE_CLASS, CompilerScope::Module => TYPE_MODULE, CompilerScope::Annotation => TYPE_ANNOTATION, + CompilerScope::TypeAlias => TYPE_TYPE_ALIAS, CompilerScope::TypeParams => TYPE_TYPE_PARAMETERS, + CompilerScope::TypeVariable => TYPE_TYPE_VARIABLE, } } @@ -193,21 +181,13 @@ mod _symtable { self as *const Self as *const core::ffi::c_void as usize } - #[pygetset] - fn identifiers(&self, vm: &VirtualMachine) -> Vec { - self.symtable - .symbols - .keys() - .map(|s| vm.ctx.new_str(s.as_str()).into()) - .collect() - } - #[pygetset] fn symbols(&self, vm: &VirtualMachine) -> PyDictRef { let dict = vm.ctx.new_dict(); for (name, symbol) in &self.symtable.symbols { - dict.set_item(name, vm.new_pyobj(symbol.flags.bits()), vm) - .unwrap(); + let packed_flags = + i32::from(symbol.flags.bits()) | (symbol.scope.as_i32() << SCOPE_OFFSET); + dict.set_item(name, vm.new_pyobj(packed_flags), vm).unwrap(); } dict } @@ -230,106 +210,4 @@ mod _symtable { )) } } - - #[pyattr] - #[pyclass(name = "Symbol")] - #[derive(PyPayload)] - struct PySymbol { - symbol: Symbol, - namespaces: Vec, - is_top_scope: bool, - } - - impl fmt::Debug for PySymbol { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Symbol()") - } - } - - #[pyclass] - impl PySymbol { - #[pymethod] - fn get_name(&self) -> String { - self.symbol.name.clone() - } - - #[pymethod] - const fn is_global(&self) -> bool { - self.symbol.is_global() || (self.is_top_scope && self.symbol.is_bound()) - } - - #[pymethod] - const fn is_declared_global(&self) -> bool { - matches!(self.symbol.scope, SymbolScope::GlobalExplicit) - } - - #[pymethod] - const fn is_local(&self) -> bool { - self.symbol.is_local() || (self.is_top_scope && self.symbol.is_bound()) - } - - #[pymethod] - const fn is_imported(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::IMPORTED) - } - - #[pymethod] - const fn is_nested(&self) -> bool { - // TODO - false - } - - #[pymethod] - const fn is_nonlocal(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::NONLOCAL) - } - - #[pymethod] - const fn is_referenced(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::REFERENCED) - } - - #[pymethod] - const fn is_assigned(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::ASSIGNED) - } - - #[pymethod] - const fn is_parameter(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::PARAMETER) - } - - #[pymethod] - const fn is_free(&self) -> bool { - matches!(self.symbol.scope, SymbolScope::Free) - } - - #[pymethod] - const fn is_namespace(&self) -> bool { - !self.namespaces.is_empty() - } - - #[pymethod] - const fn is_annotated(&self) -> bool { - self.symbol.flags.contains(SymbolFlags::ANNOTATED) - } - - #[pymethod] - fn get_namespaces(&self, vm: &VirtualMachine) -> Vec { - self.namespaces - .iter() - .map(|table| to_py_symbol_table(table.clone()).into_pyobject(vm)) - .collect() - } - - #[pymethod] - fn get_namespace(&self, vm: &VirtualMachine) -> PyResult { - if self.namespaces.len() != 1 { - return Err(vm.new_value_error("namespace is bound to multiple namespaces")); - } - Ok(to_py_symbol_table(self.namespaces.first().unwrap().clone()) - .into_ref(&vm.ctx) - .into()) - } - } } diff --git a/crates/vm/src/stdlib/_sysconfigdata.rs b/crates/vm/src/stdlib/_sysconfigdata.rs index a9871ec95dc..5a00a56aece 100644 --- a/crates/vm/src/stdlib/_sysconfigdata.rs +++ b/crates/vm/src/stdlib/_sysconfigdata.rs @@ -19,7 +19,7 @@ mod _sysconfigdata { let paths = &vm.state.config.paths; build_time_vars.set_item("prefix", paths.prefix.clone().to_pyobject(vm), vm)?; build_time_vars.set_item("exec_prefix", paths.exec_prefix.clone().to_pyobject(vm), vm)?; - let bindir = format!("{}/bin", &paths.exec_prefix); + let bindir = format!("{}/bin", paths.exec_prefix); build_time_vars.set_item("BINDIR", bindir.to_pyobject(vm), vm)?; module.set_attr("build_time_vars", build_time_vars, vm)?; diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index f3e6bec898f..79dce3d21ce 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -20,10 +20,14 @@ pub(crate) mod _thread { use crate::{ AsObject, Py, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyDictRef, PyIntRef, PyStr, PyTupleRef, PyType, PyTypeRef, PyUtf8StrRef}, - common::wtf8::Wtf8Buf, - frame::FrameRef, + builtins::{ + PyBaseExceptionRef, PyDictRef, PyIntRef, PyStr, PyTupleRef, PyType, PyTypeRef, + PyUtf8StrRef, + }, + common::{lock::PyMutex, wtf8::Wtf8Buf}, + frame::FrameObjectRef, function::{ArgCallable, FuncArgs, KwArgs, OptionalArg, PySetterValue, TimeoutSeconds}, + object::{Traverse, TraverseFn}, types::{Constructor, GetAttr, Representable, SetAttr}, }; @@ -558,6 +562,19 @@ pub(crate) mod _thread { .map_err(|_err| vm.new_runtime_error("can't start new thread")) } + fn report_unraisable_thread_exception( + exc: PyBaseExceptionRef, + func: &ArgCallable, + vm: &VirtualMachine, + ) { + let msg = func + .as_ref() + .repr(vm) + .ok() + .map(|repr| format!("Exception ignored in thread started by {}", repr.as_wtf8())); + vm.run_unraisable(exc, msg, vm.ctx.none()); + } + fn run_thread(func: ArgCallable, args: FuncArgs, vm: &VirtualMachine) { // Increment thread count when thread actually starts executing vm.state.thread_count.fetch_add(1); @@ -572,11 +589,7 @@ pub(crate) mod _thread { if let Err(exc) = func.invoke(args, vm) && !exc.fast_isinstance(vm.ctx.exceptions.system_exit) { - vm.run_unraisable( - exc, - Some("Exception ignored in thread started by".to_owned()), - func.into(), - ); + report_unraisable_thread_exception(exc, &func, vm); } } for lock in SENTINELS.take() { @@ -592,14 +605,35 @@ pub(crate) mod _thread { vm.state.thread_count.fetch_sub(1); } + /// Default stack size for Python threads in **debug builds only**, where + /// Rust stack frames are substantially larger than in release. Rust's + /// `std::thread::Builder` otherwise defaults to 2 MB, which is too small + /// for the call chains the Python stdlib runs on helper threads in debug + /// (e.g. the SSL test server, see #7941). Release builds keep the prior + /// behavior — leave the stack size unset and let Rust's std default apply + /// — to avoid oversized virtual stack mappings when many threads spawn. + #[cfg(debug_assertions)] + const DEFAULT_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024; + + /// Configure a `thread::Builder` with the stack size to use for a new + /// Python thread. Uses the value set via `threading.stack_size(N)` when + /// the user has provided one (non-zero). Otherwise, debug builds fall + /// back to [`DEFAULT_THREAD_STACK_SIZE`] and release builds leave the + /// builder unmodified (Rust's std default applies). fn apply_thread_stack_size( thread_builder: thread::Builder, vm: &VirtualMachine, ) -> thread::Builder { let configured = vm.state.stacksize.load(); if configured != 0 { - thread_builder.stack_size(configured) - } else { + return thread_builder.stack_size(configured); + } + #[cfg(debug_assertions)] + { + thread_builder.stack_size(DEFAULT_THREAD_STACK_SIZE) + } + #[cfg(not(debug_assertions))] + { thread_builder } } @@ -607,11 +641,10 @@ pub(crate) mod _thread { /// Clean up thread-local data for the current thread. /// This triggers __del__ on objects stored in thread-local variables. fn cleanup_thread_local_data() { - // Take all guards - this will trigger LocalGuard::drop for each, - // which removes the thread's dict from each Local instance - LOCAL_GUARDS.with(|guards| { - guards.borrow_mut().clear(); - }); + // Move all guards out before dropping them. A local dict's __del__ may + // re-enter thread-local access and borrow LOCAL_GUARDS again. + let guards = LOCAL_GUARDS.take(); + drop(guards); } #[cfg(all(not(target_arch = "wasm32"), feature = "host_env"))] @@ -623,7 +656,7 @@ pub(crate) mod _thread { #[pyfunction] fn exit(vm: &VirtualMachine) -> PyResult { - Err(vm.new_exception_empty(vm.ctx.exceptions.system_exit.to_owned())) + Err(vm.new_system_exit(vec![].into())) } thread_local!(static SENTINELS: RefCell>> = const { RefCell::new(Vec::new()) }); @@ -763,9 +796,8 @@ pub(crate) mod _thread { } #[pyfunction] - fn _is_main_interpreter() -> bool { - // RustPython only has one interpreter - true + fn _is_main_interpreter(vm: &VirtualMachine) -> bool { + vm.state.is_main_interpreter() } /// Initialize the main thread ident. Should be called once at interpreter startup. @@ -917,15 +949,36 @@ pub(crate) mod _thread { if let Some(local_data) = self.local.upgrade() { // Remove from map while holding the lock, but drop the value // outside the lock to prevent deadlock if __del__ accesses _local - let removed = local_data.data.lock().remove(&self.thread_id); + let removed = local_data.state.lock().dicts.remove(&self.thread_id); drop(removed); } } } + struct LocalState { + init_args: FuncArgs, + dicts: std::collections::HashMap, + } + + unsafe impl Traverse for LocalState { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.init_args.traverse(tracer_fn); + #[allow(clippy::iter_over_hash_type)] + for dict in self.dicts.values() { + dict.traverse(tracer_fn); + } + } + + fn clear(&mut self, out: &mut Vec) { + out.append(&mut self.init_args.args); + out.extend(self.init_args.kwargs.drain(..).map(|(_, value)| value)); + out.extend(self.dicts.drain().map(|(_, dict)| dict.into())); + } + } + // Shared data structure for Local struct LocalData { - data: parking_lot::Mutex>, + state: PyMutex, } impl fmt::Debug for LocalData { @@ -935,37 +988,62 @@ pub(crate) mod _thread { } #[pyattr] - #[pyclass(module = "_thread", name = "_local")] + #[pyclass(module = "_thread", name = "_local", traverse = "manual")] #[derive(Debug, PyPayload)] struct Local { inner: Arc, } + unsafe impl Traverse for Local { + fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { + self.inner.state.traverse(tracer_fn); + } + + fn clear(&mut self, out: &mut Vec) { + if let Some(mut state) = self.inner.state.try_lock() { + state.clear(out); + } + } + } + #[pyclass(with(GetAttr, SetAttr), flags(BASETYPE))] impl Local { - fn l_dict(&self, vm: &VirtualMachine) -> PyDictRef { + fn custom_init(cls: &Py, vm: &VirtualMachine) -> Option { + let cls_init = cls.slots.init.load()?; + let object_init = vm + .ctx + .types + .object_type + .slots + .init + .load() + .map(|init| crate::types::fn_addr(init)); + (Some(crate::types::fn_addr(cls_init)) != object_init).then_some(cls_init) + } + + fn create_dict(&self, vm: &VirtualMachine) -> (PyDictRef, bool) { let thread_id = current_thread_id(); // Fast path: check if dict exists under lock - let value = self.inner.data.lock().get(&thread_id).cloned(); + let value = self.inner.state.lock().dicts.get(&thread_id).cloned(); if let Some(dict) = value { - return dict; + return (dict, false); } // Slow path: allocate dict outside lock to reduce lock hold time let new_dict = vm.ctx.new_dict(); // Insert with double-check to handle races - let mut data = self.inner.data.lock(); + let mut state = self.inner.state.lock(); use std::collections::hash_map::Entry; - let (dict, need_guard) = match data.entry(thread_id) { + let (dict, need_guard) = match state.dicts.entry(thread_id) { Entry::Occupied(e) => (e.get().clone(), false), Entry::Vacant(e) => { e.insert(new_dict.clone()); (new_dict, true) } }; - drop(data); // Release lock before TLS access + drop(state); // Release lock before TLS access // Register cleanup guard only if we inserted a new entry if need_guard { @@ -978,29 +1056,80 @@ pub(crate) mod _thread { }); } - dict + (dict, need_guard) + } + + fn remove_current_dict(&self) { + let thread_id = current_thread_id(); + let guard = LOCAL_GUARDS.with(|guards| { + let mut guards = guards.borrow_mut(); + guards + .iter() + .rposition(|guard| { + guard.thread_id == thread_id + && guard.local.as_ptr() == Arc::as_ptr(&self.inner) + }) + .map(|position| guards.remove(position)) + }); + + if let Some(guard) = guard { + drop(guard); + } else { + let removed = self.inner.state.lock().dicts.remove(&thread_id); + drop(removed); + } + } + + fn l_dict(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let (dict, created) = zelf.create_dict(vm); + if !created { + return Ok(dict); + } + + let Some(init) = Self::custom_init(zelf.class(), vm) else { + return Ok(dict); + }; + let init_args = zelf.inner.state.lock().init_args.clone(); + if let Err(err) = init(zelf.as_object().to_owned(), init_args, vm) { + zelf.remove_current_dict(); + return Err(err); + } + + Ok(dict) } #[pygetset(name = "__dict__")] - fn dict(zelf: PyRef, vm: &VirtualMachine) -> PyDictRef { - zelf.l_dict(vm) + fn dict(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Self::l_dict(&zelf, vm) } #[pyslot] - fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { - Self { + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.is_empty() && Self::custom_init(&cls, vm).is_none() { + return Err(vm.new_type_error("Initialization arguments are not supported")); + } + + let zelf = Self { inner: Arc::new(LocalData { - data: parking_lot::Mutex::new(std::collections::HashMap::new()), + state: PyMutex::new(LocalState { + init_args: args, + dicts: std::collections::HashMap::new(), + }), }), } - .into_ref_with_type(vm, cls) - .map(Into::into) + .into_ref_with_type(vm, cls)?; + + // type.__call__ invokes __init__ after __new__. Create this thread's + // dict first so assignments made by __init__ cannot recursively + // initialize the same local object. + zelf.create_dict(vm); + Ok(zelf.into()) } } impl GetAttr for Local { fn getattro(zelf: &Py, attr: &Py, vm: &VirtualMachine) -> PyResult { - let l_dict = zelf.l_dict(vm); + let l_dict = Self::l_dict(zelf, vm)?; if attr.as_bytes() == b"__dict__" { Ok(l_dict.into()) } else { @@ -1030,7 +1159,7 @@ pub(crate) mod _thread { zelf.class().name() ))) } else { - let dict = zelf.l_dict(vm); + let dict = Self::l_dict(zelf, vm)?; if let PySetterValue::Assign(value) = value { dict.set_item(attr, value, vm)?; } else { @@ -1052,19 +1181,90 @@ pub(crate) mod _thread { pub(crate) use crate::vm::thread::CurrentFrameSlot; /// Get all threads' current (top) frames. Used by sys._current_frames(). - pub(crate) fn get_all_current_frames(vm: &VirtualMachine) -> Vec<(u64, FrameRef)> { - let registry = vm.state.thread_frames.lock(); - registry - .iter() - .filter_map(|(id, slot)| { - let frames = slot.frames.lock(); - // SAFETY: the owning thread can't pop while we hold the Mutex, - // so the FramePtr is valid for the duration of the lock. - frames - .last() - .map(|fp| (*id, unsafe { fp.as_ref() }.to_owned())) - }) - .collect() + pub(crate) fn get_all_current_frames(vm: &VirtualMachine) -> Vec<(u64, FrameObjectRef)> { + // unix: read each thread's published top frame under stop-the-world so + // the owning thread is parked at a safepoint and cannot pop or free the + // frame while we take a strong reference. Request stop-the-world before + // the registry lock to avoid deadlocking a thread parking mid-registry. + // + // For the current thread, use TLS CURRENT_FRAME directly because + // stack-allocated frames only update TLS (not top_frame). + #[cfg(unix)] + { + use core::sync::atomic::Ordering; + let current_ident = get_ident(); + vm.state.stop_the_world.stop_the_world(&vm.state); + scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); } + let registry = vm.state.thread_frames.lock(); + registry + .iter() + .filter_map(|(id, slot)| { + if *id == current_ident { + // Current thread: materialize from TLS chain + crate::frame::current_thread_frame_materialize(vm).map(|frame| (*id, frame)) + } else { + // Other threads: try top_frame first (FrameObject), + // fall back to top_iframe (may be a stack-allocated frame). + let top = slot.top_frame.load(Ordering::Relaxed); + if let Some(p) = core::ptr::NonNull::new(top) { + // SAFETY: world stopped -> the owning thread is parked + // with this frame on its chain, so it is alive. + let py = unsafe { p.as_ref() }; + Some((*id, py.to_owned())) + } else { + // Stack-allocated frame: materialize from top_iframe. + // SAFETY: world stopped -> owning thread is parked. + let iframe_ptr = slot.top_iframe.load(Ordering::Relaxed) + as *const crate::frame::InterpreterFrame; + if !iframe_ptr.is_null() { + let iframe = unsafe { &*iframe_ptr }; + // SAFETY: world stopped -> owning thread parked. + let fo = unsafe { iframe.materialize_detached_chain(vm) }; + Some((*id, fo)) + } else { + None + } + } + } + }) + .collect() + } + #[cfg(not(unix))] + { + use core::sync::atomic::Ordering; + let current_ident = get_ident(); + vm.state.stop_the_world.stop_the_world(&vm.state); + scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); } + let registry = vm.state.thread_frames.lock(); + registry + .iter() + .filter_map(|(id, slot)| { + if *id == current_ident { + // Current thread: materialize from TLS chain + crate::frame::current_thread_frame_materialize(vm).map(|frame| (*id, frame)) + } else { + // Other threads: use top_iframe to include + // stack-allocated frames. Materialize the entire + // chain and link retained_back so f_back works. + // SAFETY: world stopped -> owning thread is parked. + let iframe_ptr = slot.top_iframe.load(Ordering::Relaxed) + as *const crate::frame::InterpreterFrame; + if !iframe_ptr.is_null() { + let iframe = unsafe { &*iframe_ptr }; + // SAFETY: world stopped -> owning thread parked. + let fo = unsafe { iframe.materialize_detached_chain(vm) }; + Some((*id, fo)) + } else { + // Fall back to frames stack for FrameObject-only path + let frames = slot.frames.lock(); + frames + .last() + .map(|fp| (*id, unsafe { fp.as_ref() }.to_owned())) + } + } + }) + .collect() + } } /// Called after fork() in child process to mark all other threads as done. @@ -1155,6 +1355,19 @@ pub(crate) mod _thread { } } + /// Take a thread handle's completion mutex, detaching first. + /// + /// A joiner holds this mutex across its `allow_threads` wait, so it can + /// still hold it when stop-the-world stops it. An attached thread that + /// blocked on it would never reach a safepoint, so the stop could never + /// complete and the holder would never be resumed to release it. + fn lock_done<'a>( + lock: &'a parking_lot::Mutex, + vm: &VirtualMachine, + ) -> parking_lot::MutexGuard<'a, bool> { + vm.allow_threads(|| lock.lock()) + } + /// Reset a parking_lot::Mutex to unlocked state after fork. #[cfg(all(unix, feature = "host_env"))] fn reinit_parking_lot_mutex(mutex: &parking_lot::Mutex) { @@ -1237,7 +1450,7 @@ pub(crate) mod _thread { // Wait for thread completion using Condvar (supports timeout) // Loop to handle spurious wakeups let (lock, cvar) = &**done_event; - let mut done = lock.lock(); + let mut done = lock_done(lock, vm); // ThreadHandle_join semantics: self-join/finalizing checks // apply only while target thread has not reported it is exiting yet. @@ -1297,7 +1510,7 @@ pub(crate) mod _thread { drop(inner_guard); // Wait on done_event let (lock, cvar) = &**done_event; - let mut done = lock.lock(); + let mut done = lock_done(lock, vm); while !*done { vm.allow_threads(|| cvar.wait(&mut done)); } @@ -1359,7 +1572,7 @@ pub(crate) mod _thread { remove_from_shutdown_handles(vm, inner, done_event); let (lock, cvar) = &**done_event; - *lock.lock() = true; + *lock_done(lock, vm) = true; cvar.notify_all(); Ok(()) } @@ -1429,7 +1642,7 @@ pub(crate) mod _thread { // before returning True. let done = { let (lock, _) = &*self.done_event; - *lock.lock() + *lock_done(lock, vm) }; if !done { return Ok(false); @@ -1625,7 +1838,7 @@ pub(crate) mod _thread { // Starting a handle always resets the completion event. { let (done_lock, _) = &*handle.done_event; - *done_lock.lock() = false; + *lock_done(done_lock, vm) = false; } // Add non-daemon threads to shutdown registry so _shutdown() will wait for them @@ -1659,17 +1872,22 @@ pub(crate) mod _thread { started_cvar.notify_all(); } // Don't execute the target function until parent marks the - // handle as running. + // handle as running. Detach while blocked so a concurrent + // stop-the-world (e.g. a GC on another thread) can park this + // thread instead of stalling waiting for it to reach a + // safepoint it will not reach until released. { let (ready_lock, ready_cvar) = &*handle_ready_event_clone; - let mut ready = ready_lock.lock().unwrap(); - while !*ready { - // Short timeout so we stay responsive to STW requests. - let (guard, _) = ready_cvar - .wait_timeout(ready, core::time::Duration::from_millis(1)) - .unwrap(); - ready = guard; - } + vm.allow_threads(|| { + let mut ready = ready_lock.lock().unwrap(); + while !*ready { + // Short timeout so we stay responsive to STW requests. + let (guard, _) = ready_cvar + .wait_timeout(ready, core::time::Duration::from_millis(1)) + .unwrap(); + ready = guard; + } + }); } // Ensure cleanup happens even if the function panics @@ -1702,7 +1920,7 @@ pub(crate) mod _thread { // This must be LAST to ensure all cleanup is complete before join() returns { let (lock, cvar) = &*done_event_for_cleanup; - *lock.lock() = true; + *lock_done(lock, vm) = true; cvar.notify_all(); } } @@ -1722,11 +1940,7 @@ pub(crate) mod _thread { if let Err(exc) = func.invoke((), vm) && !exc.fast_isinstance(vm.ctx.exceptions.system_exit) { - vm.run_unraisable( - exc, - Some("Exception ignored in thread started by".to_owned()), - func.into(), - ); + report_unraisable_thread_exception(exc, &func, vm); } } })) @@ -1741,7 +1955,7 @@ pub(crate) mod _thread { } { let (done_lock, done_cvar) = &*handle.done_event; - *done_lock.lock() = true; + *lock_done(done_lock, vm) = true; done_cvar.notify_all(); } if !daemon { @@ -1750,16 +1964,22 @@ pub(crate) mod _thread { vm.new_runtime_error("can't start new thread") })?; - // Wait until the new thread has reported its ident. + // Wait until the new thread has reported its ident. Detach while + // waiting so a concurrent stop-the-world (e.g. a GC on another thread) + // can park this thread instead of stalling on it: the child may park + // itself at startup while the world is stopped and cannot report until + // released, so the waiter must be parkable too. { let (started_lock, started_cvar) = &*started_event; - let mut started = started_lock.lock().unwrap(); - while !*started { - let (guard, _) = started_cvar - .wait_timeout(started, core::time::Duration::from_millis(1)) - .unwrap(); - started = guard; - } + vm.allow_threads(|| { + let mut started = started_lock.lock().unwrap(); + while !*started { + let (guard, _) = started_cvar + .wait_timeout(started, core::time::Duration::from_millis(1)) + .unwrap(); + started = guard; + } + }); } // Mark the handle running in the parent thread (like CPython's @@ -1779,4 +1999,55 @@ pub(crate) mod _thread { Ok(handle_clone) } + + #[cfg(test)] + mod tests { + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + use super::*; + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + use crate::Interpreter; + + /// Regression test for #7941: a Python thread started without an + /// explicit `threading.stack_size()` must not run on Rust's 2 MiB + /// std default in debug builds, where the call chains the stdlib + /// runs on helper threads (e.g. the SSL test server) overflowed it. + #[test] + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + fn default_python_thread_stack_size_debug() { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + assert_eq!(vm.state.stacksize.load(), 0); + let builder = apply_thread_stack_size(thread::Builder::new(), vm); + let stack_size = builder + .spawn(current_thread_stack_size) + .expect("failed to spawn thread") + .join() + .expect("thread panicked"); + assert!( + stack_size >= DEFAULT_THREAD_STACK_SIZE, + "Python thread stack size is {stack_size} bytes, expected at least {DEFAULT_THREAD_STACK_SIZE}" + ); + }); + } + + #[cfg(all(debug_assertions, target_os = "linux"))] + fn current_thread_stack_size() -> usize { + use libc::{ + pthread_attr_destroy, pthread_attr_getstacksize, pthread_attr_t, + pthread_getattr_np, pthread_self, + }; + let mut attr: pthread_attr_t = unsafe { core::mem::zeroed() }; + unsafe { + assert_eq!(pthread_getattr_np(pthread_self(), &mut attr), 0); + let mut size = 0; + assert_eq!(pthread_attr_getstacksize(&attr, &mut size), 0); + pthread_attr_destroy(&mut attr); + size + } + } + + #[cfg(all(debug_assertions, target_os = "macos"))] + fn current_thread_stack_size() -> usize { + unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) } + } + } } diff --git a/crates/vm/src/stdlib/_typing.rs b/crates/vm/src/stdlib/_typing.rs index 45740f7ebad..0b19d8e3c32 100644 --- a/crates/vm/src/stdlib/_typing.rs +++ b/crates/vm/src/stdlib/_typing.rs @@ -39,8 +39,17 @@ pub(crate) mod decl { }; #[pyfunction] - pub(crate) fn _idfunc(args: FuncArgs, _vm: &VirtualMachine) -> PyObjectRef { - args.args[0].clone() + pub(crate) fn _idfunc(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.kwargs.is_empty() { + return Err(vm.new_type_error("_typing._idfunc() takes no keyword arguments")); + } + if args.args.len() != 1 { + return Err(vm.new_type_error(format!( + "_typing._idfunc() takes exactly one argument ({} given)", + args.args.len() + ))); + } + Ok(args.args[0].clone()) } #[pyfunction(name = "override")] @@ -288,7 +297,7 @@ pub(crate) mod decl { PyTuple::new_ref(vec![args], &vm.ctx) }; let origin: PyObjectRef = zelf.as_object().to_owned(); - Ok(PyGenericAlias::new(origin, args_tuple, false, vm).into_pyobject(vm)) + Ok(PyGenericAlias::new(origin, args_tuple, false, vm)?.into_pyobject(vm)) } #[pymethod] @@ -353,9 +362,9 @@ pub(crate) mod decl { // typealias(name, value, *, type_params=()) // name and value are positional-or-keyword; type_params is keyword-only. - // Reject unexpected keyword arguments + // Reject unexpected keyword arguments. for key in args.kwargs.keys() { - if key != "name" && key != "value" && key != "type_params" { + if !matches!(key.as_str(), Ok("name" | "value" | "type_params")) { return Err(vm.new_type_error(format!( "typealias() got an unexpected keyword argument '{key}'" ))); @@ -417,9 +426,8 @@ pub(crate) mod decl { }; // Get caller's module name from frame globals, like typevar.rs caller() - let module = vm - .current_frame() - .and_then(|f| f.globals.get_item("__name__", vm).ok()); + let module = + crate::frame::current_globals().and_then(|g| g.get_item("__name__", vm).ok()); Ok(Self::new_eager(name, type_params, value, module)) } diff --git a/crates/vm/src/stdlib/_winapi.rs b/crates/vm/src/stdlib/_winapi.rs index f6faf6a8a95..34e7b897e12 100644 --- a/crates/vm/src/stdlib/_winapi.rs +++ b/crates/vm/src/stdlib/_winapi.rs @@ -8,7 +8,7 @@ mod _winapi { use crate::{ Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine, builtins::PyStrRef, - common::lock::PyMutex, + common::lock::{PyMutex, PyMutexGuard}, convert::ToPyException, function::{ArgMapping, ArgSequence, OptionalArg}, types::Constructor, @@ -235,12 +235,12 @@ mod _winapi { if let Some(ref name) = args.name && name.as_bytes().contains(&0) { - return Err(crate::exceptions::cstring_error(vm)); + return Err(crate::exceptions::nul_char_error(vm)); } if let Some(ref cmd) = args.command_line && cmd.as_bytes().contains(&0) { - return Err(crate::exceptions::cstring_error(vm)); + return Err(crate::exceptions::nul_char_error(vm)); } let wcstring = |s: PyStrRef| s.as_wtf8().to_wide_cstring(); @@ -357,7 +357,8 @@ mod _winapi { } else { ms as u32 }; - host_winapi::wait_for_single_object(h.0, ms).map_err(|e| e.to_pyexception(vm)) + vm.allow_threads(|| host_winapi::wait_for_single_object(h.0, ms)) + .map_err(|e| e.to_pyexception(vm)) } #[pyfunction] @@ -381,8 +382,10 @@ mod _winapi { return Err(vm.new_value_error("WaitForMultipleObjects supports at most 64 handles")); } - host_winapi::wait_for_multiple_objects(&handles, wait_all, milliseconds) - .map_err(|e| e.to_pyexception(vm)) + vm.allow_threads(|| { + host_winapi::wait_for_multiple_objects(&handles, wait_all, milliseconds) + }) + .map_err(|e| e.to_pyexception(vm)) } #[pyfunction] @@ -563,18 +566,26 @@ mod _winapi { .map_err(|e| e.to_pyexception(vm)) } + /// Take `inner`, detaching while blocked. + /// + /// `GetOverlappedResult` holds this mutex across its `allow_threads` + /// wait, so a stopped thread can still be holding it. Blocking on it + /// while attached would leave no safepoint for that stop to complete at. + fn lock_inner(&self, vm: &VirtualMachine) -> PyMutexGuard<'_, host_overlapped::Operation> { + vm.allow_threads(|| self.inner.lock()) + } + #[pymethod] fn GetOverlappedResult(&self, wait: bool, vm: &VirtualMachine) -> PyResult<(u32, u32)> { - let mut inner = self.inner.lock(); - inner - .get_result(wait) + let mut inner = self.lock_inner(vm); + vm.allow_threads(|| inner.get_result(wait)) .map(|result| (result.transferred, result.error)) .map_err(|e| e.to_pyexception(vm)) } #[pymethod] fn getbuffer(&self, vm: &VirtualMachine) -> PyResult> { - let inner = self.inner.lock(); + let inner = self.lock_inner(vm); if !inner.is_completed() { return Err(vm.new_value_error( "can't get read buffer before GetOverlappedResult() signals the operation completed", @@ -587,13 +598,13 @@ mod _winapi { #[pymethod] fn cancel(&self, vm: &VirtualMachine) -> PyResult<()> { - let mut inner = self.inner.lock(); + let mut inner = self.lock_inner(vm); inner.cancel().map_err(|e| e.to_pyexception(vm)) } #[pygetset] - fn event(&self) -> isize { - let inner = self.inner.lock(); + fn event(&self, vm: &VirtualMachine) -> isize { + let inner = self.lock_inner(vm); inner.event() as isize } } @@ -634,7 +645,8 @@ mod _winapi { } Ok(ov.into_pyobject(vm)) } else { - host_winapi::connect_named_pipe(handle.0).map_err(|e| e.to_pyexception(vm))?; + vm.allow_threads(|| host_winapi::connect_named_pipe(handle.0)) + .map_err(|e| e.to_pyexception(vm))?; Ok(vm.ctx.none()) } } @@ -802,7 +814,9 @@ mod _winapi { return Ok(result.into()); } - let result = host_winapi::read_file(handle.0, size).map_err(|e| e.to_pyexception(vm))?; + let result = vm + .allow_threads(|| host_winapi::read_file(handle.0, size)) + .map_err(|e| e.to_pyexception(vm))?; Ok(vm .ctx .new_tuple(vec![ @@ -926,12 +940,15 @@ mod _winapi { #[cfg(not(feature = "threading"))] let sigint_event: Option = None; - match host_winapi::batched_wait_for_multiple_objects( - &handles, - wait_all, - milliseconds, - sigint_event, - ) { + let batched_result = vm.allow_threads(|| { + host_winapi::batched_wait_for_multiple_objects( + &handles, + wait_all, + milliseconds, + sigint_event, + ) + }); + match batched_result { Ok(host_winapi::BatchedWaitResult::All) => Ok(vm.ctx.none()), Ok(host_winapi::BatchedWaitResult::Indices(indices)) => Ok(vm .ctx diff --git a/crates/vm/src/stdlib/atexit.rs b/crates/vm/src/stdlib/atexit.rs index 891f8e5437b..0260b0f115d 100644 --- a/crates/vm/src/stdlib/atexit.rs +++ b/crates/vm/src/stdlib/atexit.rs @@ -3,7 +3,9 @@ pub(crate) use atexit::module_def; #[pymodule] mod atexit { - use crate::{AsObject, PyObjectRef, PyResult, VirtualMachine, function::FuncArgs}; + use crate::{ + AsObject, PyObjectRef, PyResult, VirtualMachine, common::rc::PyRc, function::FuncArgs, + }; #[pyfunction] fn register(func: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyObjectRef { @@ -11,7 +13,7 @@ mod atexit { vm.state .atexit_funcs .lock() - .insert(0, Box::new((func.clone(), args))); + .insert(0, PyRc::new((func.clone(), args))); func } @@ -29,24 +31,26 @@ mod atexit { funcs.len() as isize - 1 }; while i >= 0 { - let (cb, entry_ptr) = { + let entry = { let funcs = vm.state.atexit_funcs.lock(); if i as usize >= funcs.len() { i = funcs.len() as isize; i -= 1; continue; } - let entry = &funcs[i as usize]; - (entry.0.clone(), &**entry as *const (PyObjectRef, FuncArgs)) + // Keep the entry alive for as long as it is being compared, so + // it cannot be dropped and have its address handed to a + // callback registered from within __eq__. + funcs[i as usize].clone() }; // Lock released: __eq__ can safely call atexit functions - let eq = vm.bool_eq(&func, &cb)?; + let eq = vm.bool_eq(&func, &entry.0)?; if eq { // The entry may have moved during __eq__. Search backward by identity. let mut funcs = vm.state.atexit_funcs.lock(); let mut j = (funcs.len() as isize - 1).min(i); while j >= 0 { - if core::ptr::eq(&**funcs.get(j as usize).unwrap(), entry_ptr) { + if PyRc::ptr_eq(funcs.get(j as usize).unwrap(), &entry) { funcs.remove(j as usize); i = j; break; @@ -70,7 +74,7 @@ mod atexit { let funcs: Vec<_> = core::mem::take(&mut *vm.state.atexit_funcs.lock()); // Callbacks stored in LIFO order, iterate forward for entry in funcs { - let (func, args) = *entry; + let (func, args) = PyRc::try_unwrap(entry).unwrap_or_else(|e| (*e).clone()); if let Err(e) = func.call(args, vm) { let exit = e.fast_isinstance(vm.ctx.exceptions.system_exit); let msg = func diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index d0ed32b22d6..19e33110a5b 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -13,25 +13,30 @@ mod builtins { PyByteArray, PyBytes, PyDictRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyUtf8StrRef, enumerate::PyReverseSequenceIterator, - function::{PyCellRef, PyFunction}, + function::{PyCell, PyCellRef, PyFunction}, int::PyIntRef, iter::PyCallableIterator, list::{PyList, SortOptions}, }, + bytecode, common::hash::PyHash, function::{ - ArgBytesLike, ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping, - ArgPrimitiveIndex, ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg, - OptionalOption, PosArgs, + ArgCallable, ArgIndex, ArgIntoBool, ArgIterable, ArgMapping, ArgPrimitiveIndex, + ArgStrOrBytesLike, Either, FsPath, FuncArgs, KwArgs, OptionalArg, OptionalOption, + PosArgs, }, protocol::{PyIter, PyIterReturn}, py_io, readline::{Readline, ReadlineResult}, stdlib::sys, types::PyComparisonOp, + vm::compile_mode::{ + CompilerFlags, PY_EVAL_INPUT, PY_FILE_INPUT, PY_FUNC_TYPE_INPUT, PY_SINGLE_INPUT, + compile_future_feature_mask, compile_future_features_from_flags, + }, }; use itertools::Itertools; - use num_traits::{Signed, ToPrimitive, Zero}; + use num_traits::{Signed, ToPrimitive}; use rustpython_common::wtf8::CodePoint; #[cfg(not(feature = "rustpython-compiler"))] @@ -103,8 +108,8 @@ mod builtins { filename: PyObjectRef, mode: PyUtf8StrRef, // CPython parity: flags / optimize accept any object with __index__, - // not just exact int. Matches the behavior of `int(x)` arg conversion - // used by Python/Python-ast.c::compile. + // not just exact int. Matches the argument conversion used by + // builtin_compile_impl. #[pyarg(any, optional)] flags: OptionalArg>, // CPython parity: dont_inherit goes through PyObject_IsTrue, so @@ -114,174 +119,67 @@ mod builtins { dont_inherit: OptionalArg, #[pyarg(any, optional)] optimize: OptionalArg>, - #[pyarg(any, optional)] + #[pyarg(named, optional)] _feature_version: OptionalArg, } - /// Detect PEP 263 encoding cookie from source bytes. - /// Checks first two lines for `# coding[:=] ` pattern. - /// Returns the encoding name if found, or None for default (UTF-8). - #[cfg(feature = "parser")] - fn detect_source_encoding(source: &[u8]) -> Option { - fn find_encoding_in_line(line: &[u8]) -> Option { - // PEP 263: '#' must be preceded only by whitespace/formfeed - let hash_pos = line.iter().position(|&b| b == b'#')?; - if !line[..hash_pos] - .iter() - .all(|&b| matches!(b, b' ' | b'\t' | b'\x0c' | b'\r')) - { - return None; - } - let after_hash = &line[hash_pos..]; - - // Find "coding" after the # - let coding_pos = after_hash.windows(6).position(|w| w == b"coding")?; - let after_coding = &after_hash[coding_pos + 6..]; - - // Next char must be ':' or '=' - let rest = if matches!(after_coding.first(), Some(b':' | b'=')) { - &after_coding[1..] - } else { - return None; - }; - - // Skip whitespace - let rest = rest - .iter() - .copied() - .skip_while(|&b| matches!(b, b' ' | b'\t')) - .collect::>(); - - // 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::(); - - if name.is_empty() { - None - } else { - Some(normalize_source_encoding(&name)) - } + fn merge_compile_future_features( + flags: i32, + dont_inherit: bool, + _vm: &VirtualMachine, + ) -> bytecode::CodeFlags { + let mut future_features = compile_future_features_from_flags(flags); + if !dont_inherit && let Some(code) = crate::frame::current_code() { + future_features |= bytecode::CodeFlags::from_bits_truncate( + code.flags.bits() & compile_future_feature_mask().bits(), + ); } - - // Split into lines (first two only) - let mut lines = source.splitn(3, |&b| b == b'\n'); - - if let Some(first) = lines.next() { - // Strip BOM if present - let first = first.strip_prefix(b"\xef\xbb\xbf").unwrap_or(first); - if let Some(enc) = find_encoding_in_line(first) { - return Some(enc); - } - // Only check second line if first line is blank or a comment - let trimmed = first - .iter() - .find(|&&b| !matches!(b, b' ' | b'\t' | b'\x0c' | b'\r')) - .copied(); - - if trimmed.is_some_and(|b| b != b'#') { - return None; - } - } - - lines.next().and_then(find_encoding_in_line) + future_features } - /// Match CPython's Parser/tokenizer/helpers.c:get_normal_name(). - #[cfg(feature = "parser")] - fn normalize_source_encoding(name: &str) -> String { - let mut normalized = String::with_capacity(name.len().min(12)); - for ch in name.chars().take(12) { - if ch == '_' { - normalized.push('-'); - } else { - normalized.push(ch.to_ascii_lowercase()); - } - } + fn audit_compile_source(vm: &VirtualMachine, source: &[u8], filename: &str) -> PyResult<()> { + vm.sys_module.get_attr("audit", vm)?.call( + ( + vm.ctx.new_str("compile"), + vm.ctx.new_bytes(source.to_vec()), + vm.ctx.new_str(filename), + ), + vm, + )?; + Ok(()) + } - if normalized == "utf-8" || normalized.starts_with("utf-8-") { - "utf-8".to_owned() - } else if normalized == "latin-1" - || normalized == "iso-8859-1" - || normalized == "iso-latin-1" - || normalized.starts_with("latin-1-") - || normalized.starts_with("iso-8859-1-") - || normalized.starts_with("iso-latin-1-") + fn trim_eval_source_bytes(mut source: &[u8]) -> &[u8] { + while let Some((&first, rest)) = source.split_first() + && matches!(first, b' ' | b'\t') { - "iso-8859-1".to_owned() - } else { - name.to_owned() + source = rest; } + source } - /// Decode source bytes to a string, handling PEP 263 encoding declarations - /// and BOM. Raises SyntaxError for invalid UTF-8 without an encoding - /// declaration. - #[cfg(feature = "parser")] - fn is_utf8_encoding(name: &str) -> bool { - name == "utf-8" - } - - #[cfg(feature = "parser")] - fn decode_source_bytes(source: &[u8], filename: &str, vm: &VirtualMachine) -> PyResult { - let has_bom = source.starts_with(b"\xef\xbb\xbf"); - let encoding = detect_source_encoding(source); - - let is_utf8 = encoding.as_deref().is_none_or(is_utf8_encoding); - - // Validate BOM + encoding combination - if has_bom && !is_utf8 { - let enc = encoding.as_deref().unwrap_or("utf-8"); - return Err(vm.new_exception_msg( - vm.ctx.exceptions.syntax_error.to_owned(), - format!("encoding problem: {enc} with BOM").into(), - )); + fn decode_eval_exec_source_bytes( + vm: &VirtualMachine, + source: &[u8], + filename: &str, + ) -> PyResult { + #[cfg(feature = "parser")] + { + vm.decode_source_bytes(source, filename, false) } - - if is_utf8 { - let src = if has_bom { &source[3..] } else { source }; - match core::str::from_utf8(src) { - Ok(s) => Ok(s.to_owned()), - Err(e) => { - let bad_byte = src[e.valid_up_to()]; - let line = src[..e.valid_up_to()] - .iter() - .filter(|&&b| b == b'\n') - .count() - + 1; - Err(vm.new_exception_msg( - vm.ctx.exceptions.syntax_error.to_owned(), - format!( - "Non-UTF-8 code starting with '\\x{bad_byte:02x}' \ - on line {line}, but no encoding declared; \ - see https://peps.python.org/pep-0263/ for details \ - ({filename}, line {line})" - ) - .into(), - )) - } - } - } else { - // Use codec registry for non-UTF-8 encodings - let enc = encoding.as_deref().unwrap(); - let bytes_obj = vm.ctx.new_bytes(source.to_vec()); - let decoded = vm - .state - .codec_registry - .decode_text(bytes_obj.into(), enc, None, vm) - .map_err(|exc| { - if exc.fast_isinstance(vm.ctx.exceptions.lookup_error) { - vm.new_exception_msg( - vm.ctx.exceptions.syntax_error.to_owned(), - format!("unknown encoding for '{filename}': {enc}").into(), - ) - } else { - exc - } - })?; - Ok(decoded.to_string_lossy().into_owned()) + #[cfg(not(feature = "parser"))] + { + _ = filename; + core::str::from_utf8(source) + .map(str::to_owned) + .map_err(|err| { + let msg = format!( + "(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte", + source[err.valid_up_to()], + err.valid_up_to() + ); + vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into()) + }) } } @@ -303,30 +201,60 @@ mod builtins { use crate::{class::PyClassImpl, stdlib::_ast}; - let feature_version = feature_version_from_arg(args._feature_version, vm)?; + let feature_version = args._feature_version.into_option().unwrap_or(-1); let mode_str = args.mode.as_str(); + let flags: i32 = args.flags.map_or(0, |v| v.value); + let cf = CompilerFlags::from_bits_retain(flags); + + if (flags & !CompilerFlags::ALLOWED_FLAGS.bits()) != 0 { + return Err(vm.new_value_error("compile(): unrecognised flags")); + } let optimize: i32 = args.optimize.map_or(-1, |v| v.value); let optimize: u8 = match optimize { - -1 => vm.state.config.settings.optimize, + -1 => vm.state.config.settings.optimize.min(2), 0..=2 => optimize as u8, _ => return Err(vm.new_value_error("compile(): invalid optimize value")), }; - - if args - .source - .fast_isinstance(&_ast::NodeAst::make_static_type()) - { - let flags: i32 = args.flags.map_or(0, |v| v.value); - let is_ast_only = !(flags & _ast::PY_CF_ONLY_AST).is_zero(); - - // func_type mode requires PyCF_ONLY_AST - if mode_str == "func_type" && !is_ast_only { + let 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); + + let start = if mode_str == "exec" { + PY_FILE_INPUT + } else if mode_str == "eval" { + PY_EVAL_INPUT + } else if mode_str == "single" { + PY_SINGLE_INPUT + } else if mode_str == "func_type" { + if !is_ast_only { return Err(vm.new_value_error( "compile() mode 'func_type' requires flag PyCF_ONLY_AST", )); } + PY_FUNC_TYPE_INPUT + } else { + let msg = if is_ast_only { + "compile() mode must be 'exec', 'eval', 'single' or 'func_type'" + } else { + "compile() mode must be 'exec', 'eval' or 'single'" + }; + return Err(vm.new_value_error(msg)); + }; + + let ast_type = _ast::NodeAst::make_static_type().as_object().to_owned(); + if args.source.is_instance(&ast_type, vm)? { + let explicit_future_annotations = + future_features.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS); + vm.sys_module.get_attr("audit", vm)?.call( + ( + vm.ctx.new_str("compile"), + args.source.clone(), + vm.ctx.none(), + ), + vm, + )?; // compile(ast_node, ..., PyCF_ONLY_AST) returns the AST after validation if is_ast_only { @@ -336,15 +264,29 @@ mod builtins { "compile() mode must be 'exec', 'eval', 'single' or 'func_type'", ) })?; - if !args.source.fast_isinstance(&expected_type) { + if !args.source.is_instance(expected_type.as_object(), vm)? { return Err(vm.new_type_error(format!( "expected {} node, got {}", expected_name, args.source.class().name() ))); } - _ast::validate_ast_object(vm, args.source.clone())?; - return Ok(args.source); + #[cfg(not(feature = "rustpython-codegen"))] + { + _ast::validate_ast_object(vm, args.source.clone())?; + return Ok(args.source); + } + #[cfg(feature = "rustpython-codegen")] + { + return _ast::preprocess_ast_object( + vm, + args.source, + &filename.to_string_lossy(), + optimize, + cf.contains(CompilerFlags::OPTIMIZED_AST), + explicit_future_annotations, + ); + } } #[cfg(not(feature = "rustpython-codegen"))] @@ -353,133 +295,105 @@ mod builtins { } #[cfg(feature = "rustpython-codegen")] { + let (expected_type, expected_name) = _ast::mode_type_and_name(mode_str) + .ok_or_else(|| { + vm.new_value_error("compile() mode must be 'exec', 'eval' or 'single'") + })?; + if !args.source.is_instance(expected_type.as_object(), vm)? { + return Err(vm.new_type_error(format!( + "expected {} node, got {}", + expected_name, + args.source.class().name() + ))); + } let mode = mode_str .parse::() .map_err(|err| vm.new_value_error(err.to_string()))?; - return _ast::compile( - vm, - args.source, - &filename.to_string_lossy(), - mode, - Some(optimize), - ); + let mut opts = vm.compile_opts(); + opts.optimize = optimize; + opts.allow_top_level_await = cf.contains(CompilerFlags::ALLOW_TOP_LEVEL_AWAIT); + opts.future_features = future_features; + return _ast::compile(vm, args.source, &filename.to_string_lossy(), mode, opts); } } #[cfg(not(feature = "parser"))] - return Err(vm.new_type_error( - "can't compile() source code when the `parser` feature of rustpython is disabled", - )); - + { + Err(vm.new_type_error( + "can't compile() source code when the `parser` feature of rustpython is disabled", + )) + } #[cfg(feature = "parser")] { - use crate::convert::ToPyException; - - use ruff_python_parser as parser; - let source = ArgStrOrBytesLike::try_from_object(vm, args.source)?; - let source = source.borrow_bytes(); - - let source = decode_source_bytes(&source, &filename.to_string_lossy(), vm)?; - let source = source.as_str(); - let flags: i32 = args.flags.map_or(0, |v| v.value); - - if !(flags & !_ast::PY_COMPILE_FLAGS_MASK).is_zero() { - return Err(vm.new_value_error("compile(): unrecognised flags")); - } - - let allow_incomplete = !(flags & _ast::PY_CF_ALLOW_INCOMPLETE_INPUT).is_zero(); - let type_comments = !(flags & _ast::PY_CF_TYPE_COMMENTS).is_zero(); - - let optimize_level = optimize; - - if (flags & _ast::PY_CF_ONLY_AST).is_zero() { - #[cfg(not(feature = "compiler"))] - { - Err(vm.new_value_error(CODEGEN_NOT_SUPPORTED)) - } - #[cfg(feature = "compiler")] - { - if let Some(feature_version) = feature_version { - let mode = mode_str - .parse::() - .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::() - .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::() - .map_err(|err| vm.new_value_error(err.to_string()))?; - let parsed = _ast::parse( - vm, + let mut compile_flags = flags | future_features.bits() as i32; + #[cfg(feature = "rustpython-compiler")] + let compile_source = |source: &[u8], compile_flags: i32| { + vm.compile_string_object_with_flags( source, - mode, - optimize_level, + &filename.to_string_lossy(), + start, + compile_flags, feature_version, - type_comments, + optimize as i32, ) - .map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm))?; - - if mode_str == "single" { - return _ast::wrap_interactive(vm, parsed); + }; + match &source { + ArgStrOrBytesLike::Str(source) => { + let source = source.try_as_utf8(vm)?.as_str(); + 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, - vm: &VirtualMachine, - ) -> PyResult> { - let Some(minor) = feature_version.into_option() else { - return Ok(None); - }; - - if minor < 0 { - return Ok(None); - } - - u8::try_from(minor) - .map(|v| Some(ruff_python_ast::PythonVersion { major: 3, minor: v })) - .map_err(|_| vm.new_value_error("compile() _feature_version out of range")) - } - #[pyfunction] fn delattr(obj: PyObjectRef, attr: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let attr = attr.try_to_ref::(vm).map_err(|_| { + let attr = attr.try_to_ref::(vm).map_err(|_e| { vm.new_type_error(format!( "attribute name must be string, not '{}'", attr.class().name() @@ -507,42 +421,39 @@ mod builtins { } impl ScopeArgs { - fn validate_globals_dict( - globals: &PyObject, - vm: &VirtualMachine, - func_name: &'static str, - ) -> PyResult<()> { - if globals.fast_isinstance(vm.ctx.types.dict_type) { - return Ok(()); - } - - let msg = match func_name { - "eval" => { - let is_mapping = globals.mapping_unchecked().check(); - if is_mapping { - "globals must be a real dict; try eval(expr, {}, mapping)".into() - } else { - "globals must be a dict".into() - } - } - "exec" => format!( - "exec() globals must be a dict, not {}", - globals.class().name() - ), - _ => "globals must be a dict".into(), - }; - - Err(vm.new_type_error(msg)) - } - fn make_scope( self, vm: &VirtualMachine, func_name: &'static str, ) -> PyResult { + fn validate_globals_dict( + globals: &PyObject, + vm: &VirtualMachine, + func_name: &'static str, + ) -> PyResult<()> { + if !globals.fast_isinstance(vm.ctx.types.dict_type) { + return Err(match func_name { + "eval" => { + let is_mapping = globals.mapping_unchecked().check(); + vm.new_type_error(if is_mapping { + "globals must be a real dict; try eval(expr, {}, mapping)" + } else { + "globals must be a dict" + }) + } + "exec" => vm.new_type_error(format!( + "exec() globals must be a dict, not {}", + globals.class().name() + )), + _ => vm.new_type_error("globals must be a dict"), + }); + } + Ok(()) + } + let (globals, locals) = match self.globals { Some(globals) => { - Self::validate_globals_dict(&globals, vm, func_name)?; + validate_globals_dict(&globals, vm, func_name)?; let globals = PyDictRef::try_from_object(vm, globals)?; if !globals.contains_key(identifier!(vm, __builtins__), vm) { @@ -570,6 +481,61 @@ mod builtins { } } + #[derive(FromArgs)] + struct ExecArgs { + #[pyarg(positional)] + source: Either>, + #[pyarg(any, default)] + globals: Option, + #[pyarg(any, default)] + locals: Option, + #[pyarg(named, optional)] + closure: OptionalOption, + } + + fn exec_closure( + code_obj: &PyRef, + closure: Option, + vm: &VirtualMachine, + ) -> PyResult>>> { + 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::(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::(vm) + .map(Some) + .map_err(|_| { + vm.new_type_error(format!( + "code object requires a closure of exactly length {num_free}" + )) + }) + } + #[pyfunction] fn eval( source: Either>, @@ -581,38 +547,95 @@ mod builtins { // source as string let code = match source { Either::A(either) => { - let source: &[u8] = &either.borrow_bytes(); - if source.contains(&0) { - return Err(vm.new_exception_msg( - vm.ctx.exceptions.syntax_error.to_owned(), - "source code string cannot contain null bytes".into(), - )); - } - - let source = core::str::from_utf8(source).map_err(|err| { - let msg = format!( - "(unicode error) 'utf-8' codec can't decode byte 0x{:x?} in position {}: invalid start byte", - source[err.valid_up_to()], - err.valid_up_to() - ); - - vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), msg.into()) - })?; - Ok(Either::A(vm.ctx.new_utf8_str(source.trim_start()))) + let source = match &either { + ArgStrOrBytesLike::Str(source) => { + let source = source.try_as_utf8(vm)?.as_str(); + if source.as_bytes().contains(&0) { + return Err(vm.new_exception_msg( + vm.ctx.exceptions.syntax_error.to_owned(), + "source code string cannot contain null bytes".into(), + )); + } + let source = source.trim_start_matches([' ', '\t']); + audit_compile_source(vm, source.as_bytes(), "")?; + source.to_owned() + } + ArgStrOrBytesLike::Buf(source) => { + let source: &[u8] = &source.borrow_buf(); + if source.contains(&0) { + return Err(vm.new_exception_msg( + vm.ctx.exceptions.syntax_error.to_owned(), + "source code string cannot contain null bytes".into(), + )); + } + let source = trim_eval_source_bytes(source); + audit_compile_source(vm, source, "")?; + decode_eval_exec_source_bytes(vm, source, "eval")? + } + }; + Ok(Either::A(vm.ctx.new_utf8_str(source))) } Either::B(code) => Ok(Either::B(code)), }?; - run_code(vm, code, scope, crate::compiler::Mode::Eval, "eval") + run_code(vm, code, scope, crate::compiler::Mode::Eval, "eval", None) } #[pyfunction] - fn exec( - source: Either>, - scope: ScopeArgs, - vm: &VirtualMachine, - ) -> PyResult { - let scope = scope.make_scope(vm, "exec")?; - run_code(vm, source, scope, crate::compiler::Mode::Exec, "exec") + fn exec(args: ExecArgs, vm: &VirtualMachine) -> PyResult { + let ExecArgs { + source, + globals, + locals, + closure, + } = args; + let scope = ScopeArgs { globals, locals }.make_scope(vm, "exec")?; + let closure = closure.flatten(); + let (source, closure) = match source { + Either::A(either) => { + if closure.is_some() { + return Err( + vm.new_type_error("closure can only be used when source is a code object") + ); + } + let source = match &either { + ArgStrOrBytesLike::Str(source) => { + let source = source.try_as_utf8(vm)?.as_str(); + 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(), "")?; + source.to_owned() + } + ArgStrOrBytesLike::Buf(source) => { + let source: &[u8] = &source.borrow_buf(); + if source.contains(&0) { + return Err(vm.new_exception_msg( + vm.ctx.exceptions.syntax_error.to_owned(), + "source code string cannot contain null bytes".into(), + )); + } + audit_compile_source(vm, source, "")?; + decode_eval_exec_source_bytes(vm, source, "exec")? + } + }; + (Either::A(vm.ctx.new_utf8_str(source)), None) + } + Either::B(code) => { + let closure = exec_closure(&code, closure, vm)?; + (Either::B(code), closure) + } + }; + run_code( + vm, + source, + scope, + crate::compiler::Mode::Exec, + "exec", + closure, + ) } fn run_code( @@ -621,28 +644,39 @@ mod builtins { scope: crate::scope::Scope, #[allow(unused_variables)] mode: crate::compiler::Mode, func: &str, + closure: Option>>, ) -> PyResult { // Determine code object: let code_obj = match source { #[cfg(feature = "rustpython-compiler")] Either::A(string) => { let source = string.as_str(); - vm.compile(source, mode, "") - .map_err(|err| vm.new_syntax_error(&err, Some(source)))? + let mut opts = vm.compile_opts(); + if let Some(code) = crate::frame::current_code() { + opts.future_features = bytecode::CodeFlags::from_bits_truncate( + code.flags.bits() & compile_future_feature_mask().bits(), + ); + } + vm.compile_with_opts(source, mode, "", opts) + .map_err(|err| err.into_pyexception(vm, Some(source)))? } #[cfg(not(feature = "rustpython-compiler"))] Either::A(_) => return Err(vm.new_type_error(CODEGEN_NOT_SUPPORTED)), Either::B(code_obj) => code_obj, }; - if !code_obj.freevars.is_empty() { + vm.sys_module + .get_attr("audit", vm)? + .call((vm.ctx.new_str("exec"), code_obj.clone()), vm)?; + + if closure.is_none() && !code_obj.freevars.is_empty() { return Err(vm.new_type_error(format!( "code object passed to {func}() may not contain free variables" ))); } // Run the code: - vm.run_code_obj(code_obj, scope) + vm.run_code_obj_with_closure(code_obj, scope, closure) } #[pyfunction] @@ -963,18 +997,10 @@ mod builtins { } #[pyfunction] - fn ord(string: Either, vm: &VirtualMachine) -> PyResult { - match string { - Either::A(bytes) => bytes.with_ref(|bytes| { - let bytes_len = bytes.len(); - if bytes_len != 1 { - return Err(vm.new_type_error(format!( - "ord() expected a character, but string of length {bytes_len} found" - ))); - } - Ok(u32::from(bytes[0])) - }), - Either::B(string) => match string.as_wtf8().code_points().exactly_one() { + // builtin_ord + fn ord(c: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let bytes = if let Some(string) = c.downcast_ref::() { + return match string.as_wtf8().code_points().exactly_one() { Ok(character) => Ok(character.to_u32()), Err(_) => { let string_len = string.char_len(); @@ -982,8 +1008,24 @@ mod builtins { "ord() expected a character, but string of length {string_len} found" ))) } - }, + }; + } else if let Some(bytes) = c.downcast_ref::() { + bytes.as_bytes().to_vec() + } else if let Some(bytearray) = c.downcast_ref::() { + bytearray.borrow_buf().to_vec() + } else { + return Err(vm.new_type_error(format!( + "ord() expected string of length 1, but {} found", + c.class().name() + ))); + }; + let bytes_len = bytes.len(); + if bytes_len != 1 { + return Err(vm.new_type_error(format!( + "ord() expected a character, but string of length {bytes_len} found" + ))); } + Ok(u32::from(bytes[0])) } #[derive(FromArgs)] @@ -1002,15 +1044,15 @@ mod builtins { modulus, } = args; let modulus = modulus - .as_ref() - .map_or_else(|| vm.ctx.none.as_object(), |m| m); + .as_deref() + .unwrap_or_else(|| vm.ctx.none.as_object()); vm._pow(&x, &y, modulus) } #[pyfunction] pub(super) fn exit(exit_code_arg: OptionalArg, vm: &VirtualMachine) -> PyResult { let code = exit_code_arg.unwrap_or_else(|| vm.ctx.new_int(0).into()); - Err(vm.new_exception(vm.ctx.exceptions.system_exit.to_owned(), vec![code])) + Err(vm.new_system_exit(vec![code].into())) } #[derive(Debug, Default, FromArgs)] diff --git a/crates/vm/src/stdlib/gc.rs b/crates/vm/src/stdlib/gc.rs index 00eea1b39d5..af861862edb 100644 --- a/crates/vm/src/stdlib/gc.rs +++ b/crates/vm/src/stdlib/gc.rs @@ -23,20 +23,20 @@ mod gc { /// Enable automatic garbage collection. #[pyfunction] - fn enable() { - gc_state::gc_state().enable(); + fn enable(vm: &VirtualMachine) { + vm.state.gc.enable(); } /// Disable automatic garbage collection. #[pyfunction] - fn disable() { - gc_state::gc_state().disable(); + fn disable(vm: &VirtualMachine) { + vm.state.gc.disable(); } /// Return true if automatic gc is enabled. #[pyfunction] - fn isenabled() -> bool { - gc_state::gc_state().is_enabled() + fn isenabled(vm: &VirtualMachine) -> bool { + vm.state.gc.is_enabled() } /// Run a garbage collection. Returns the number of unreachable objects found. @@ -58,15 +58,14 @@ mod gc { invoke_callbacks(vm, "start", generation_num as usize, &Default::default()); // Manual gc.collect() should run even if GC is disabled - let gc = gc_state::gc_state(); + let gc = &vm.state.gc; let result = gc.collect_force(generation_num as usize); - // Move objects from gc_state.garbage to vm.ctx.gc_garbage (for DEBUG_SAVEALL) + // Publish what the collection saved as gc.garbage (for DEBUG_SAVEALL) { let mut state_garbage = gc.garbage.lock(); if !state_garbage.is_empty() { - let py_garbage = &vm.ctx.gc_garbage; - let mut garbage_vec = py_garbage.borrow_vec_mut(); + let mut garbage_vec = gc.py_garbage.borrow_vec_mut(); for obj in state_garbage.drain(..) { garbage_vec.push(obj); } @@ -82,7 +81,7 @@ mod gc { /// Return the current collection thresholds as a tuple. #[pyfunction] fn get_threshold(vm: &VirtualMachine) -> PyObjectRef { - let (t0, t1, t2) = gc_state::gc_state().get_threshold(); + let (t0, t1, t2) = vm.state.gc.get_threshold(); vm.ctx .new_tuple(vec![ vm.ctx.new_int(t0).into(), @@ -94,8 +93,13 @@ mod gc { /// Set the collection thresholds. #[pyfunction] - fn set_threshold(threshold0: u32, threshold1: OptionalArg, threshold2: OptionalArg) { - gc_state::gc_state().set_threshold( + fn set_threshold( + threshold0: u32, + threshold1: OptionalArg, + threshold2: OptionalArg, + vm: &VirtualMachine, + ) { + vm.state.gc.set_threshold( threshold0, threshold1.into_option(), threshold2.into_option(), @@ -117,20 +121,22 @@ mod gc { /// Return the current debugging flags. #[pyfunction] - fn get_debug() -> u32 { - gc_state::gc_state().get_debug().bits() + fn get_debug(vm: &VirtualMachine) -> u32 { + vm.state.gc.get_debug().bits() } /// Set the debugging flags. #[pyfunction] - fn set_debug(flags: u32) { - gc_state::gc_state().set_debug(gc_state::GcDebugFlags::from_bits_truncate(flags)); + fn set_debug(flags: u32, vm: &VirtualMachine) { + vm.state + .gc + .set_debug(gc_state::GcDebugFlags::from_bits_truncate(flags)); } /// Return a list of per-generation gc stats. #[pyfunction] fn get_stats(vm: &VirtualMachine) -> PyResult { - let stats = gc_state::gc_state().get_stats(); + let stats = vm.state.gc.get_stats(); let mut result = Vec::with_capacity(3); for stat in &stats { @@ -165,7 +171,7 @@ mod gc { { return Err(vm.new_value_error(format!("generation must be in range(0, 3), not {g}"))); } - let objects = gc_state::gc_state().get_objects(generation_opt); + let objects = vm.state.gc.get_objects(generation_opt); Ok(vm.ctx.new_list(objects)) } @@ -199,20 +205,16 @@ mod gc { // PyObjects, so they never appear in get_referrers results. Since // RustPython materializes every frame as a PyObject, we must exclude // them manually to match the expected behavior. - let stack_frames: HashSet = vm - .frames - .borrow() - .iter() - .map(|fp| { - let frame: &crate::PyObject = unsafe { fp.as_ref() }.as_ref(); - frame as *const crate::PyObject as usize - }) - .collect(); + let mut stack_frames: HashSet = HashSet::new(); + crate::frame::for_each_current_frame(|frame| { + let obj: &crate::PyObject = frame.as_ref(); + stack_frames.insert(obj as *const crate::PyObject as usize); + }); let mut result = Vec::new(); // Scan all tracked objects across all generations - let all_objects = gc_state::gc_state().get_objects(None); + let all_objects = vm.state.gc.get_objects(None); for obj in all_objects { let obj_ptr = obj.as_ref() as *const crate::PyObject as usize; if stack_frames.contains(&obj_ptr) { @@ -245,14 +247,14 @@ mod gc { /// Freeze all objects tracked by gc. #[pyfunction] - fn freeze() { - gc_state::gc_state().freeze(); + fn freeze(vm: &VirtualMachine) { + vm.state.gc.freeze(); } /// Unfreeze all objects in the permanent generation. #[pyfunction] - fn unfreeze() { - gc_state::gc_state().unfreeze(); + fn unfreeze(vm: &VirtualMachine) { + vm.state.gc.unfreeze(); } /// Return the number of objects in the permanent generation. @@ -264,13 +266,13 @@ mod gc { /// gc.garbage - list of uncollectable objects #[pyattr] fn garbage(vm: &VirtualMachine) -> PyListRef { - vm.ctx.gc_garbage.clone() + vm.state.gc.py_garbage.clone() } /// gc.callbacks - list of callbacks to be invoked #[pyattr] fn callbacks(vm: &VirtualMachine) -> PyListRef { - vm.ctx.gc_callbacks.clone() + vm.state.gc.py_callbacks.clone() } /// Helper function to invoke GC callbacks @@ -280,7 +282,7 @@ mod gc { generation: usize, result: &gc_state::CollectResult, ) { - let callbacks_list = &vm.ctx.gc_callbacks; + let callbacks_list = &vm.state.gc.py_callbacks; let callbacks: Vec = callbacks_list.borrow_vec().to_vec(); if callbacks.is_empty() { return; diff --git a/crates/vm/src/stdlib/itertools.rs b/crates/vm/src/stdlib/itertools.rs index 8a78c698ed3..6eb268d94c1 100644 --- a/crates/vm/src/stdlib/itertools.rs +++ b/crates/vm/src/stdlib/itertools.rs @@ -4,13 +4,12 @@ pub(crate) use decl::module_def; mod decl { use crate::{ AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, PyWeakRef, VirtualMachine, - builtins::{PyGenericAlias, PyInt, PyIntRef, PyList, PyTuple, PyType, PyTypeRef, int}, - common::{ - lock::{PyMutex, PyRwLock, PyRwLockWriteGuard}, - rc::PyRc, + builtins::{ + PyGenericAlias, PyInt, PyIntRef, PyList, PyTuple, PyTupleRef, PyType, PyTypeRef, int, }, + common::lock::{PyMutex, PyRwLock, PyRwLockWriteGuard}, convert::ToPyObject, - function::{ArgCallable, FuncArgs, OptionalArg, OptionalOption, PosArgs}, + function::{FuncArgs, OptionalArg, OptionalOption, PosArgs}, protocol::{PyIter, PyIterReturn, PyNumber}, raise_if_stop, stdlib::sys, @@ -26,7 +25,7 @@ mod decl { use num_traits::{Signed, ToPrimitive}; #[pyattr] - #[pyclass(name = "chain")] + #[pyclass(name = "chain", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsChain { source: PyRwLock>, @@ -64,7 +63,7 @@ mod decl { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } } @@ -119,7 +118,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "compress")] + #[pyclass(name = "compress", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsCompress { data: PyIter, @@ -166,7 +165,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "count")] + #[pyclass(name = "count", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsCount { cur: PyRwLock, @@ -225,7 +224,9 @@ mod decl { let step = &zelf.step; let mut result = Wtf8Buf::from("count("); result.push_wtf8(cur_repr.as_wtf8()); - if !vm.bool_eq(step, vm.ctx.new_int(1).as_object())? { + let step_is_int_one = step.fast_isinstance(vm.ctx.types.int_type) + && vm.bool_eq(step, vm.ctx.new_int(1).as_object())?; + if !step_is_int_one { result.push_str(", "); result.push_wtf8(step.repr(vm)?.as_wtf8()); } @@ -235,11 +236,12 @@ mod decl { } #[pyattr] - #[pyclass(name = "cycle")] + #[pyclass(name = "cycle", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsCycle { iter: PyIter, saved: PyRwLock>, + #[pytraverse(skip)] index: AtomicCell, } @@ -271,11 +273,15 @@ mod decl { return Ok(PyIterReturn::StopIteration(None)); } - let last_index = zelf.index.fetch_add(1); - - if last_index >= saved.len() - 1 { - zelf.index.store(0); - } + // Advance and wrap in a single atomic step. A separate + // fetch_add followed by a reset lets a second thread observe + // an index past the end of `saved`. + let last_index = match zelf.index.fetch_update(|index| { + let next = index + 1; + Some(if next < saved.len() { next } else { 0 }) + }) { + Ok(index) | Err(index) => index, + }; saved[last_index].clone() }; @@ -285,10 +291,11 @@ mod decl { } #[pyattr] - #[pyclass(name = "repeat")] + #[pyclass(name = "repeat", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsRepeat { object: PyObjectRef, + #[pytraverse(skip)] times: Option>, } @@ -363,7 +370,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "starmap")] + #[pyclass(name = "starmap", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsStarmap { function: PyObjectRef, @@ -410,11 +417,12 @@ mod decl { } #[pyattr] - #[pyclass(name = "takewhile")] + #[pyclass(name = "takewhile", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsTakewhile { predicate: PyObjectRef, iterable: PyIter, + #[pytraverse(skip)] stop_flag: AtomicCell, } @@ -472,18 +480,19 @@ mod decl { } #[pyattr] - #[pyclass(name = "dropwhile")] + #[pyclass(name = "dropwhile", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsDropwhile { - predicate: ArgCallable, + predicate: PyObjectRef, iterable: PyIter, + #[pytraverse(skip)] start_flag: AtomicCell, } #[derive(FromArgs)] struct DropwhileNewArgs { #[pyarg(positional)] - predicate: ArgCallable, + predicate: PyObjectRef, #[pyarg(positional)] iterable: PyIter, } @@ -520,8 +529,7 @@ mod decl { if !zelf.start_flag.load() { loop { let obj = raise_if_stop!(iterable.next(vm)?); - let pred = predicate.clone(); - let pred_value = pred.invoke((obj.clone(),), vm)?; + let pred_value = predicate.call((obj.clone(),), vm)?; if !pred_value.try_to_bool(vm)? { zelf.start_flag.store(true); return Ok(PyIterReturn::Return(obj)); @@ -532,11 +540,13 @@ mod decl { } } - #[derive(Default)] + #[derive(Default, Traverse)] struct GroupByState { current_value: Option, current_key: Option, + #[pytraverse(skip)] next_group: bool, + #[pytraverse(skip)] grouper: Option>, } @@ -560,7 +570,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "groupby")] + #[pyclass(name = "groupby", traverse)] #[derive(PyPayload)] struct PyItertoolsGroupBy { iterable: PyIter, @@ -660,7 +670,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "_grouper")] + #[pyclass(name = "_grouper", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsGrouper { groupby: PyRef, @@ -702,13 +712,17 @@ mod decl { } #[pyattr] - #[pyclass(name = "islice")] + #[pyclass(name = "islice", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsIslice { iterable: PyIter, + #[pytraverse(skip)] cur: AtomicCell, + #[pytraverse(skip)] next: AtomicCell, + #[pytraverse(skip)] stop: Option, + #[pytraverse(skip)] step: usize, } @@ -827,7 +841,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "filterfalse")] + #[pyclass(name = "filterfalse", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsFilterFalse { predicate: PyObjectRef, @@ -886,7 +900,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "accumulate")] + #[pyclass(name = "accumulate", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsAccumulate { iterable: PyIter, @@ -947,20 +961,25 @@ mod decl { } } - #[derive(Debug)] + #[pyattr] + #[pyclass(name = "_tee_dataobject", traverse)] + #[derive(Debug, PyPayload)] struct PyItertoolsTeeData { iterable: PyIter, values: PyMutex>, + #[pytraverse(skip)] running: AtomicBool, } + #[pyclass(flags(DISALLOW_INSTANTIATION))] impl PyItertoolsTeeData { - fn new(iterable: PyIter, _vm: &VirtualMachine) -> PyRc { - PyRc::new(Self { + fn new(iterable: PyIter, vm: &VirtualMachine) -> PyRef { + Self { iterable, values: PyMutex::new(vec![]), running: AtomicBool::new(false), - }) + } + .into_ref(&vm.ctx) } fn get_item(&self, vm: &VirtualMachine, index: usize) -> PyResult { @@ -973,13 +992,15 @@ mod decl { return Ok(PyIterReturn::Return(values[index].clone())); } } - // Prevent concurrent/reentrant calls to iterable.next() + // Prevent concurrent/reentrant calls to iterable.next(). The claim + // covers caching the value as well: released any earlier, a second + // tee at the same index fetches a value of its own and one of the + // two is dropped without ever reaching a caller. if self.running.swap(true, Ordering::Acquire) { return Err(vm.new_runtime_error("cannot re-enter the tee iterator")); } - let result = self.iterable.next(vm); - self.running.store(false, Ordering::Release); - let obj = raise_if_stop!(result?); + scopeguard::defer! { self.running.store(false, Ordering::Release) } + let obj = raise_if_stop!(self.iterable.next(vm)?); let Some(mut values) = self.values.try_lock() else { return Err(vm.new_runtime_error("cannot re-enter the tee iterator")); }; @@ -991,59 +1012,44 @@ mod decl { } #[pyattr] - #[pyclass(name = "tee")] + #[pyclass(name = "_tee", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsTee { - tee_data: PyRc, + tee_data: PyRef, + #[pytraverse(skip)] index: AtomicCell, - } - - #[derive(FromArgs)] - struct TeeNewArgs { - #[pyarg(positional)] - iterable: PyIter, - #[pyarg(positional, optional)] - n: OptionalArg, + #[pytraverse(skip)] + advancing: AtomicBool, } impl Constructor for PyItertoolsTee { - type Args = TeeNewArgs; - - // TODO: make tee() a function, rename this class to itertools._tee and make - // teedata a python class - fn slot_new(_cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let TeeNewArgs { iterable, n } = args.bind(vm)?; - let n = n.unwrap_or(2); - - let copyable = if iterable.class().has_attr(identifier!(vm, __copy__)) { - vm.call_special_method(iterable.as_object(), identifier!(vm, __copy__), ())? - } else { - Self::from_iter(iterable, vm)? - }; + type Args = PyIter; - let mut tee_vec: Vec = Vec::with_capacity(n); - for _ in 0..n { - tee_vec.push(vm.call_special_method(©able, identifier!(vm, __copy__), ())?); + fn py_new(_cls: &Py, iterator: Self::Args, vm: &VirtualMachine) -> PyResult { + // An iterator that is already a tee shares its buffer rather than + // getting one of its own. + if let Some(tee) = iterator.as_object().downcast_ref::() { + return Ok(tee.__copy__()); } - - Ok(PyTuple::new_ref(tee_vec, &vm.ctx).into()) - } - - fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { - unimplemented!("use slot_new") + Ok(Self { + tee_data: PyItertoolsTeeData::new(iterator, vm), + index: AtomicCell::new(0), + advancing: AtomicBool::new(false), + }) } } - #[pyclass(with(IterNext, Iterable, Constructor))] + #[pyclass(with(IterNext, Iterable, Constructor), flags(HAS_WEAKREF))] impl PyItertoolsTee { fn from_iter(iterator: PyIter, vm: &VirtualMachine) -> PyResult { let class = Self::class(&vm.ctx); - if iterator.class().is(Self::class(&vm.ctx)) { + if iterator.class().is(class) { return vm.call_special_method(&iterator, identifier!(vm, __copy__), ()); } Ok(Self { tee_data: PyItertoolsTeeData::new(iterator, vm), index: AtomicCell::new(0), + advancing: AtomicBool::new(false), } .into_ref_with_type(vm, class.to_owned())? .into()) @@ -1052,27 +1058,65 @@ mod decl { #[pymethod] fn __copy__(&self) -> Self { Self { - tee_data: PyRc::clone(&self.tee_data), + tee_data: self.tee_data.clone(), index: AtomicCell::new(self.index.load()), + advancing: AtomicBool::new(false), } } } + + #[pyfunction] + fn tee(iterable: PyIter, n: OptionalArg, vm: &VirtualMachine) -> PyResult { + let n = n.unwrap_or(2); + if n < 0 { + return Err(vm.new_value_error("n must be >= 0")); + } + let n = n as usize; + + // Only an iterator that cannot copy itself needs a tee to buffer it. + let copyable = if iterable.class().has_attr(identifier!(vm, __copy__)) { + iterable.into() + } else { + PyItertoolsTee::from_iter(iterable, vm)? + }; + + let mut tee_vec: Vec = Vec::new(); + tee_vec + .try_reserve_exact(n) + .map_err(|_| vm.new_memory_error(""))?; + for _ in 0..n { + tee_vec.push(vm.call_special_method(©able, identifier!(vm, __copy__), ())?); + } + + Ok(PyTuple::new_ref(tee_vec, &vm.ctx)) + } impl SelfIter for PyItertoolsTee {} impl IterNext for PyItertoolsTee { fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let value = raise_if_stop!(zelf.tee_data.get_item(vm, zelf.index.load())?); - zelf.index.fetch_add(1); + // Reading the index and moving it on is one step: two callers that + // read the same index hand out the same value twice and leave the + // buffer to be filled out of order. + if zelf.advancing.swap(true, Ordering::Acquire) { + return Err(vm.new_runtime_error("cannot re-enter the tee iterator")); + } + scopeguard::defer! { zelf.advancing.store(false, Ordering::Release) } + let index = zelf.index.load(); + let value = raise_if_stop!(zelf.tee_data.get_item(vm, index)?); + zelf.index.store(index + 1); Ok(PyIterReturn::Return(value)) } } #[pyattr] - #[pyclass(name = "product")] + #[pyclass(name = "product", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsProduct { pools: Vec>, + #[pytraverse(skip)] idxs: PyRwLock>, + #[pytraverse(skip)] cur: AtomicCell, + #[pytraverse(skip)] stop: AtomicCell, } @@ -1168,13 +1212,16 @@ mod decl { } #[pyattr] - #[pyclass(name = "combinations")] + #[pyclass(name = "combinations", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsCombinations { pool: Vec, + #[pytraverse(skip)] indices: PyRwLock>, result: PyRwLock>>, + #[pytraverse(skip)] r: AtomicCell, + #[pytraverse(skip)] exhausted: AtomicCell, } @@ -1200,13 +1247,21 @@ mod decl { if r.is_negative() { return Err(vm.new_value_error("r must be non-negative")); } - let r = r.to_usize().unwrap(); + let r = r.to_isize().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C ssize_t") + })? as usize; let n = pool.len(); + let mut indices = Vec::new(); + indices + .try_reserve_exact(r) + .map_err(|_| vm.new_memory_error(""))?; + indices.extend(0..r); + Ok(Self { pool, - indices: PyRwLock::new((0..r).collect()), + indices: PyRwLock::new(indices), result: PyRwLock::new(None), r: AtomicCell::new(r), exhausted: AtomicCell::new(r > n), @@ -1279,12 +1334,15 @@ mod decl { } #[pyattr] - #[pyclass(name = "combinations_with_replacement")] + #[pyclass(name = "combinations_with_replacement", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsCombinationsWithReplacement { pool: Vec, + #[pytraverse(skip)] indices: PyRwLock>, + #[pytraverse(skip)] r: AtomicCell, + #[pytraverse(skip)] exhausted: AtomicCell, } @@ -1301,13 +1359,21 @@ mod decl { if r.is_negative() { return Err(vm.new_value_error("r must be non-negative")); } - let r = r.to_usize().unwrap(); + let r = r.to_isize().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C ssize_t") + })? as usize; let n = pool.len(); + let mut indices = Vec::new(); + indices + .try_reserve_exact(r) + .map_err(|_| vm.new_memory_error(""))?; + indices.resize(r, 0); + Ok(Self { pool, - indices: PyRwLock::new(vec![0; r]), + indices: PyRwLock::new(indices), r: AtomicCell::new(r), exhausted: AtomicCell::new(n == 0 && r > 0), }) @@ -1365,15 +1431,20 @@ mod decl { } #[pyattr] - #[pyclass(name = "permutations")] + #[pyclass(name = "permutations", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsPermutations { - pool: Vec, // Collected input iterable - indices: PyRwLock>, // One index per element in pool - cycles: PyRwLock>, // One rollover counter per element in the result + pool: Vec, // Collected input iterable + #[pytraverse(skip)] + indices: PyRwLock>, // One index per element in pool + #[pytraverse(skip)] + cycles: PyRwLock>, // One rollover counter per element in the result + #[pytraverse(skip)] result: PyRwLock>>, // Indexes of the most recently returned result - r: AtomicCell, // Size of result tuple - exhausted: AtomicCell, // Set when the iterator is exhausted + #[pytraverse(skip)] + r: AtomicCell, // Size of result tuple + #[pytraverse(skip)] + exhausted: AtomicCell, // Set when the iterator is exhausted } #[derive(FromArgs)] @@ -1407,7 +1478,9 @@ mod decl { if val.is_negative() { return Err(vm.new_value_error("r must be non-negative")); } - val.to_usize().unwrap() + val.to_isize().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C ssize_t") + })? as usize } None => n, }; @@ -1523,7 +1596,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "zip_longest")] + #[pyclass(name = "zip_longest", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsZipLongest { iterators: Vec, @@ -1561,7 +1634,7 @@ mod decl { } #[pyattr] - #[pyclass(name = "pairwise")] + #[pyclass(name = "pairwise", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsPairwise { iterator: PyIter, @@ -1610,12 +1683,15 @@ mod decl { } #[pyattr] - #[pyclass(name = "batched")] + #[pyclass(name = "batched", traverse)] #[derive(Debug, PyPayload)] struct PyItertoolsBatched { + #[pytraverse(skip)] exhausted: AtomicCell, iterable: PyIter, + #[pytraverse(skip)] n: AtomicCell, + #[pytraverse(skip)] strict: AtomicCell, } diff --git a/crates/vm/src/stdlib/marshal.rs b/crates/vm/src/stdlib/marshal.rs index ace3aff58f2..08a8f589a77 100644 --- a/crates/vm/src/stdlib/marshal.rs +++ b/crates/vm/src/stdlib/marshal.rs @@ -9,14 +9,15 @@ mod decl { use crate::{ PyObjectRef, PyResult, TryFromObject, VirtualMachine, builtins::{ - PyBool, PyByteArray, PyBytes, PyCode, PyComplex, PyDict, PyEllipsis, PyFloat, - PyFrozenSet, PyInt, PyList, PyNone, PySet, PyStopIteration, PyStr, PyTuple, + PyBaseExceptionRef, PyBool, PyByteArray, PyBytes, PyCode, PyComplex, PyDict, + PyEllipsis, PyFloat, PyFrozenSet, PyInt, PyList, PyNone, PySet, PyStopIteration, PyStr, + PyTuple, }, convert::ToPyObject, function::{ArgBytesLike, OptionalArg}, object::{AsObject, PyPayload}, - protocol::PyBuffer, }; + use core::cell::RefCell; use malachite_bigint::BigInt; use num_traits::Zero; use rustpython_compiler_core::marshal::{self, DumpableValue}; @@ -115,9 +116,6 @@ mod decl { )?; } - if !allow_code { - check_no_code(&value, vm)?; - } check_exact_type(&value, vm)?; let mut buf = Vec::new(); let mut refs = if version >= 3 { @@ -125,12 +123,19 @@ mod decl { } else { None }; - write_object(&mut buf, &value, &mut refs, version, vm)?; + write_object(&mut buf, &value, &mut refs, version, allow_code, vm)?; Ok(PyBytes::from(buf)) } + struct WriterRefEntry { + idx: u32, + /// Set between `reserve` and `complete` for the object kinds whose + /// immutable representation cannot be rebuilt from a back-reference. + incomplete: bool, + } + struct WriterRefTable { - map: std::collections::HashMap, + map: std::collections::HashMap, next_idx: u32, } @@ -141,23 +146,35 @@ mod decl { next_idx: 0, } } - fn try_ref(&mut self, buf: &mut Vec, obj: &PyObjectRef) -> bool { + /// `w_ref`: write a back-reference to an object already in the table. + /// Reaching an entry that is still being written is a recursion the + /// reader could not rebuild, so it is an error rather than a `TYPE_REF`. + fn try_ref(&mut self, buf: &mut Vec, obj: &PyObjectRef) -> Result { use marshal::Write; - let id = obj.get_id(); - if let Some(&idx) = self.map.get(&id) { - buf.write_u8(b'r'); - buf.write_u32(idx); - true - } else { - false + let Some(entry) = self.map.get(&obj.get_id()) else { + return Ok(false); + }; + if entry.incomplete { + return Err(()); } + buf.write_u8(b'r'); + buf.write_u32(entry.idx); + Ok(true) } - fn reserve(&mut self, obj: &PyObjectRef) -> u32 { + fn reserve(&mut self, obj: &PyObjectRef, incomplete: bool) -> u32 { let idx = self.next_idx; - self.map.insert(obj.get_id(), idx); + self.map + .insert(obj.get_id(), WriterRefEntry { idx, incomplete }); self.next_idx += 1; idx } + /// `w_complete`: the object's contents are on the stream, so a later + /// occurrence may reference it. + fn complete(&mut self, obj: &PyObjectRef) { + if let Some(entry) = self.map.get_mut(&obj.get_id()) { + entry.incomplete = false; + } + } } fn write_object( @@ -165,6 +182,7 @@ mod decl { obj: &PyObjectRef, refs: &mut Option, version: i32, + allow_code: bool, vm: &VirtualMachine, ) -> PyResult<()> { write_object_depth( @@ -172,6 +190,7 @@ mod decl { obj, refs, version, + allow_code, vm, marshal::MAX_MARSHAL_STACK_DEPTH, ) @@ -182,6 +201,7 @@ mod decl { obj: &PyObjectRef, refs: &mut Option, version: i32, + allow_code: bool, vm: &VirtualMachine, depth: usize, ) -> PyResult<()> { @@ -197,16 +217,28 @@ mod decl { || obj.downcast_ref::().is_some(); // FLAG_REF: check if already written, otherwise reserve slot - if !is_singleton - && let Some(rt) = refs.as_mut() - && rt.try_ref(buf, obj) - { - return Ok(()); + if !is_singleton && let Some(rt) = refs.as_mut() { + match rt.try_ref(buf, obj) { + Ok(true) => return Ok(()), + Ok(false) => {} + Err(()) => { + return Err(vm.new_value_error(format!( + "cannot marshal recursion {} objects", + obj.class().name() + ))); + } + } } let type_pos = buf.len(); let use_ref = refs.is_some() && !is_singleton; + // A code or slice entry stays incomplete until its contents are + // written: the reader rebuilds both from their fields, so a + // back-reference issued while those fields are still being emitted + // would name an object that does not exist yet. + let requires_completion = obj.downcast_ref::().is_some() + || obj.downcast_ref::().is_some(); if use_ref { - refs.as_mut().unwrap().reserve(obj); + refs.as_mut().unwrap().reserve(obj, requires_completion); } if vm.is_none(obj) { @@ -290,20 +322,20 @@ mod decl { buf.write_u8(b'('); buf.write_u32(t.len() as u32); for elem in t.as_slice() { - write_object_depth(buf, elem, refs, version, vm, depth - 1)?; + write_object_depth(buf, elem, refs, version, allow_code, vm, depth - 1)?; } } else if let Some(l) = obj.downcast_ref::() { buf.write_u8(b'['); let items = l.borrow_vec(); buf.write_u32(items.len() as u32); for elem in items.iter() { - write_object_depth(buf, elem, refs, version, vm, depth - 1)?; + write_object_depth(buf, elem, refs, version, allow_code, vm, depth - 1)?; } } else if let Some(d) = obj.downcast_ref::() { buf.write_u8(b'{'); for (k, v) in d { - write_object_depth(buf, &k, refs, version, vm, depth - 1)?; - write_object_depth(buf, &v, refs, version, vm, depth - 1)?; + write_object_depth(buf, &k, refs, version, allow_code, vm, depth - 1)?; + write_object_depth(buf, &v, refs, version, allow_code, vm, depth - 1)?; } buf.write_u8(b'0'); // TYPE_NULL terminator } else if let Some(s) = obj.downcast_ref::() { @@ -311,18 +343,28 @@ mod decl { let elems = s.elements(); buf.write_u32(elems.len() as u32); for elem in &elems { - write_object_depth(buf, elem, refs, version, vm, depth - 1)?; + write_object_depth(buf, elem, refs, version, allow_code, vm, depth - 1)?; } } else if let Some(s) = obj.downcast_ref::() { buf.write_u8(b'>'); let elems = s.elements(); buf.write_u32(elems.len() as u32); for elem in &elems { - write_object_depth(buf, elem, refs, version, vm, depth - 1)?; + write_object_depth(buf, elem, refs, version, allow_code, vm, depth - 1)?; } } else if let Some(co) = obj.downcast_ref::() { + if !allow_code { + return Err(vm.new_value_error("marshalling code objects is disallowed")); + } buf.write_u8(b'c'); - marshal::serialize_code(buf, &co.code); + // `Literal` holds the exact object a constant was built from, so + // route `co_consts` back through the object writer: it reaches the + // values `BorrowedConstant` cannot describe and shares the one + // reference table the reader indexes against. + marshal::serialize_code_with(buf, &co.code, |buf, constant| { + let constant = PyObjectRef::from(constant.clone()); + write_object_depth(buf, &constant, refs, version, allow_code, vm, depth - 1) + })?; } else if let Some(sl) = obj.downcast_ref::() { if version < 5 { return Err(vm.new_value_error("unmarshallable object")); @@ -334,15 +376,17 @@ mod decl { sl.start.as_ref().unwrap_or(&none), refs, version, + allow_code, vm, depth - 1, )?; - write_object_depth(buf, &sl.stop, refs, version, vm, depth - 1)?; + write_object_depth(buf, &sl.stop, refs, version, allow_code, vm, depth - 1)?; write_object_depth( buf, sl.step.as_ref().unwrap_or(&none), refs, version, + allow_code, vm, depth - 1, )?; @@ -357,6 +401,9 @@ mod decl { if use_ref { buf[type_pos] |= marshal::FLAG_REF; + if requires_completion { + refs.as_mut().unwrap().complete(obj); + } } Ok(()) } @@ -386,79 +433,201 @@ mod decl { } #[derive(Copy, Clone)] - struct PyMarshalBag<'a>(&'a VirtualMachine); + struct PyMarshalBag<'a> { + vm: &'a VirtualMachine, + pending_error: &'a RefCell>, + allow_code: bool, + } + + impl<'a> PyMarshalBag<'a> { + fn new( + vm: &'a VirtualMachine, + pending_error: &'a RefCell>, + allow_code: bool, + ) -> Self { + Self { + vm, + pending_error, + allow_code, + } + } + + /// Room for a container the decoder publishes before it reads what + /// goes in it. The length is the input's to choose, so the room is + /// asked for rather than assumed: a length no allocator can serve is + /// a MemoryError, not an aborted process. + fn placeholder_elements( + &self, + len: usize, + ) -> Result, marshal::MarshalError> { + let mut elements = Vec::new(); + elements + .try_reserve_exact(len) + .map_err(|_| self.remember_python_error(self.vm.new_memory_error("")))?; + elements.resize(len, self.vm.ctx.none()); + Ok(elements) + } + + fn remember_python_error(&self, error: PyBaseExceptionRef) -> marshal::MarshalError { + let mut pending = self.pending_error.borrow_mut(); + if pending.is_none() { + *pending = Some(error); + } + marshal::MarshalError::BadType + } + } impl<'a> marshal::MarshalBag for PyMarshalBag<'a> { type Value = PyObjectRef; type ConstantBag = PyVmBag<'a>; fn make_bool(&self, value: bool) -> Self::Value { - self.0.ctx.new_bool(value).into() + self.vm.ctx.new_bool(value).into() } fn make_none(&self) -> Self::Value { - self.0.ctx.none() + self.vm.ctx.none() } fn make_ellipsis(&self) -> Self::Value { - self.0.ctx.ellipsis.clone().into() + self.vm.ctx.ellipsis.clone().into() } fn make_float(&self, value: f64) -> Self::Value { - self.0.ctx.new_float(value).into() + self.vm.ctx.new_float(value).into() } fn make_complex(&self, value: num_complex::Complex64) -> Self::Value { - self.0.ctx.new_complex(value).into() + self.vm.ctx.new_complex(value).into() } fn make_str(&self, value: &Wtf8) -> Self::Value { - self.0.ctx.new_str(value).into() + self.vm.ctx.new_str(value).into() + } + fn make_interned_str(&self, value: &Wtf8) -> Self::Value { + self.vm.ctx.intern_str(value).to_owned().into() } fn make_bytes(&self, value: &[u8]) -> Self::Value { - self.0.ctx.new_bytes(value.to_vec()).into() + self.vm.ctx.new_bytes(value.to_vec()).into() } fn make_int(&self, value: BigInt) -> Self::Value { - self.0.ctx.new_int(value).into() + self.vm.ctx.new_int(value).into() } fn make_tuple(&self, elements: impl Iterator) -> Self::Value { - self.0.ctx.new_tuple(elements.collect()).into() + self.vm.ctx.new_tuple(elements.collect()).into() } - fn make_code(&self, code: CodeObject) -> Self::Value { - crate::builtins::PyCode::new_ref_with_bag(self.0, code).into() + fn make_tuple_placeholder( + &self, + len: usize, + ) -> Result, marshal::MarshalError> { + let elements = self.placeholder_elements(len)?; + Ok(Some(PyTuple::new_ref(elements, &self.vm.ctx).into())) + } + fn set_tuple_item( + &self, + tuple: &Self::Value, + index: usize, + value: Self::Value, + ) -> Result<(), marshal::MarshalError> { + let tuple = tuple + .downcast_ref::() + .ok_or(marshal::MarshalError::BadType)?; + // SAFETY: compiler-core calls this only on a fresh placeholder, + // once per index, before returning it to Python code. + unsafe { tuple.set_marshal_item(index, value) }; + Ok(()) + } + fn make_code(&self, code: CodeObject) -> Result { + if !self.allow_code { + return Err(self.remember_python_error( + self.vm + .new_value_error("unmarshalling code objects is disallowed"), + )); + } + Ok(crate::builtins::PyCode::new_ref_with_bag(self.vm, code).into()) } fn make_stop_iter(&self) -> Result { - Ok(self.0.ctx.exceptions.stop_iteration.to_owned().into()) + Ok(self.vm.ctx.exceptions.stop_iteration.to_owned().into()) } fn make_list( &self, it: impl Iterator, ) -> Result { - Ok(self.0.ctx.new_list(it.collect()).into()) + Ok(self.vm.ctx.new_list(it.collect()).into()) + } + fn make_list_placeholder( + &self, + len: usize, + ) -> Result, marshal::MarshalError> { + let elements = self.placeholder_elements(len)?; + Ok(Some(self.vm.ctx.new_list(elements).into())) + } + fn set_list_item( + &self, + list: &Self::Value, + index: usize, + value: Self::Value, + ) -> Result<(), marshal::MarshalError> { + let list = list + .downcast_ref::() + .ok_or(marshal::MarshalError::BadType)?; + list.borrow_vec_mut()[index] = value; + Ok(()) } fn make_set( &self, it: impl Iterator, ) -> Result { - let set = PySet::default().into_ref(&self.0.ctx); + let set = PySet::default().into_ref(&self.vm.ctx); for elem in it { - set.add(elem, self.0).unwrap() + set.add(elem, self.vm) + .map_err(|error| self.remember_python_error(error))?; } Ok(set.into()) } + fn make_set_placeholder(&self) -> Option { + Some(PySet::default().into_ref(&self.vm.ctx).into()) + } + fn insert_set_item( + &self, + set: &Self::Value, + value: Self::Value, + ) -> Result<(), marshal::MarshalError> { + let set = set + .downcast_ref::() + .ok_or(marshal::MarshalError::BadType)?; + set.add(value, self.vm) + .map_err(|error| self.remember_python_error(error)) + } fn make_frozenset( &self, it: impl Iterator, ) -> Result { - Ok(PyFrozenSet::from_iter(self.0, it) - .unwrap() - .to_pyobject(self.0)) + PyFrozenSet::from_iter(self.vm, it) + .map(|set| set.to_pyobject(self.vm)) + .map_err(|error| self.remember_python_error(error)) } fn make_dict( &self, it: impl Iterator, ) -> Result { - let dict = self.0.ctx.new_dict(); + let dict = self.vm.ctx.new_dict(); for (k, v) in it { - dict.set_item(&*k, v, self.0).unwrap() + dict.set_item(&*k, v, self.vm) + .map_err(|error| self.remember_python_error(error))?; } Ok(dict.into()) } + fn make_dict_placeholder(&self) -> Option { + Some(self.vm.ctx.new_dict().into()) + } + fn insert_dict_item( + &self, + dict: &Self::Value, + key: Self::Value, + value: Self::Value, + ) -> Result<(), marshal::MarshalError> { + let dict = dict + .downcast_ref::() + .ok_or(marshal::MarshalError::BadType)?; + dict.set_item(&*key, value, self.vm) + .map_err(|error| self.remember_python_error(error)) + } fn make_slice( &self, start: Self::Value, @@ -466,7 +635,7 @@ mod decl { step: Self::Value, ) -> Result { use crate::builtins::PySlice; - let vm = self.0; + let vm = self.vm; Ok(PySlice { start: if vm.is_none(&start) { None @@ -480,37 +649,67 @@ mod decl { .into()) } fn constant_bag(self) -> Self::ConstantBag { - PyVmBag(self.0) + PyVmBag(self.vm) + } + /// `Literal` wraps any object, so a decoded `co_consts` entry is + /// already its own compiler-side constant — no placeholder is needed + /// and `make_code_with_constants` keeps the default. + fn constant_ref_from_value(&self, value: &Self::Value) -> Option { + Some(Literal::from(value.clone())) + } + fn bytes_from_value(&self, value: &Self::Value) -> Option> { + value + .downcast_ref::() + .map(|bytes| bytes.as_bytes().to_vec()) + } + fn str_from_value(&self, value: &Self::Value) -> Option { + value + .downcast_ref::() + .map(|str| str.to_string_lossy().into_owned()) + } + fn tuple_elements_from_value(&self, value: &Self::Value) -> Option> { + value + .downcast_ref::() + .map(|tuple| tuple.as_slice().to_vec()) + } + } + + fn deserialize_value( + rdr: &mut impl marshal::Read, + allow_code: bool, + vm: &VirtualMachine, + ) -> PyResult { + let pending_error = RefCell::new(None); + match marshal::deserialize_value(rdr, PyMarshalBag::new(vm, &pending_error, allow_code)) { + Ok(value) => Ok(value), + Err(error) => Err(pending_error.into_inner().unwrap_or_else(|| match error { + marshal::MarshalError::Eof => vm.new_eof_error("marshal data too short"), + error @ marshal::MarshalError::NullObject => vm.new_type_error(error.to_string()), + error @ (marshal::MarshalError::BadSize(_) + | marshal::MarshalError::UnknownType + | marshal::MarshalError::InvalidRef) => { + vm.new_value_error(format!("bad marshal data ({error})")) + } + _ => vm.new_value_error("bad marshal data"), + })), } } #[derive(FromArgs)] struct LoadsArgs { #[pyarg(any)] - data: PyBuffer, + // marshal_loads_impl takes `bytes: Py_buffer`, a y* argument. + data: ArgBytesLike, #[pyarg(named, default = true)] allow_code: bool, } #[pyfunction] fn loads(args: LoadsArgs, vm: &VirtualMachine) -> PyResult { - let LoadsArgs { - data: pybuffer, - allow_code, - } = args; - let buf = pybuffer.as_contiguous().ok_or_else(|| { - vm.new_buffer_error("Buffer provided to marshal.loads() is not contiguous") - })?; + let LoadsArgs { data, allow_code } = args; + let buf = data.borrow_buf(); - let result = - marshal::deserialize_value(&mut &buf[..], PyMarshalBag(vm)).map_err(|e| match e { - marshal::MarshalError::Eof => vm.new_eof_error("marshal data too short"), - _ => vm.new_value_error("bad marshal data"), - })?; - if !allow_code { - check_no_code(&result, vm)?; - } - Ok(result) + deserialize_value(&mut &buf[..], allow_code, vm) } #[derive(FromArgs)] @@ -530,61 +729,25 @@ mod decl { .try_into_value::(vm)?; let read_res = vm.call_method(&args.f, "read", ())?; let bytes = ArgBytesLike::try_from_object(vm, read_res)?; - let buf = bytes.borrow_buf(); - - let mut rdr: &[u8] = &buf; - let len_before = rdr.len(); - let result = - marshal::deserialize_value(&mut rdr, PyMarshalBag(vm)).map_err(|e| match e { - marshal::MarshalError::Eof => vm.new_exception_msg( - vm.ctx.exceptions.eof_error.to_owned(), - "marshal data too short".into(), - ), - _ => vm.new_value_error("bad marshal data"), - })?; - let consumed = len_before - rdr.len(); + + // The borrow ends here: seek() below is the caller's, and reaching the + // same buffer from it would deadlock on a borrow still held. + let (result, consumed) = { + let buf = bytes.borrow_buf(); + let mut rdr: &[u8] = &buf; + let len_before = rdr.len(); + let result = deserialize_value(&mut rdr, args.allow_code, vm)?; + (result, len_before - rdr.len()) + }; // Seek file to just after the consumed bytes let new_pos = tell_before + consumed as i64; vm.call_method(&args.f, "seek", (new_pos,))?; - if !args.allow_code { - check_no_code(&result, vm)?; - } Ok(result) } /// Reject subclasses of marshallable types (int, float, complex, tuple, etc.). - /// Recursively check that no code objects are present. - fn check_no_code(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - if obj.downcast_ref::().is_some() { - return Err(vm.new_value_error("unmarshalling code objects is disallowed")); - } - if let Some(tup) = obj.downcast_ref::() { - for elem in tup.as_slice() { - check_no_code(elem, vm)?; - } - } else if let Some(list) = obj.downcast_ref::() { - for elem in list.borrow_vec().iter() { - check_no_code(elem, vm)?; - } - } else if let Some(set) = obj.downcast_ref::() { - for elem in set.elements() { - check_no_code(&elem, vm)?; - } - } else if let Some(fset) = obj.downcast_ref::() { - for elem in fset.elements() { - check_no_code(&elem, vm)?; - } - } else if let Some(dict) = obj.downcast_ref::() { - for (k, v) in dict { - check_no_code(&k, vm)?; - check_no_code(&v, vm)?; - } - } - Ok(()) - } - fn check_exact_type(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let cls = obj.class(); // bool is a subclass of int but is marshallable diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index 0d423541b87..26412e352ce 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -9,16 +9,17 @@ pub(crate) mod module { Py, PyResult, TryFromObject, VirtualMachine, builtins::{PyBytes, PyDictRef, PyListRef, PyStr, PyStrRef, PyTupleRef}, convert::ToPyException, - exceptions::OSErrorBuilder, + exceptions::{self, OSErrorBuilder}, function::{ArgMapping, Either, OptionalArg}, host_env::{crt_fd, windows::ToWideString}, ospath::{OsPath, OsPathOrFd}, stdlib::os::{_os, DirFd, SupportFunc, TargetIsDirectory}, }; + use core::hint::cold_path; use libc::intptr_t; use rustpython_common::wtf8::Wtf8Buf; use rustpython_host_env::nt as host_nt; - use std::os::windows::ffi::OsStringExt; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::os::windows::io::AsRawHandle; #[pyattr] @@ -48,6 +49,26 @@ pub(crate) mod module { #[pyattr] const TMP_MAX: i32 = i32::MAX; + fn utf8_from_bytes<'a>(bytes: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a str> { + core::str::from_utf8(bytes).map_err(|err| { + let reason = match err.error_len() { + None => "unexpected end of data", + Some(_) => match bytes[err.valid_up_to()] { + 0xc2..=0xf4 => "invalid continuation byte", + _ => "invalid start byte", + }, + }; + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(bytes.to_vec()), + err.valid_up_to(), + err.error_len() + .map_or(bytes.len(), |len| err.valid_up_to() + len), + vm.ctx.new_str(reason), + ) + }) + } + #[pyattr] use host_nt::{ LOAD_LIBRARY_SEARCH_APPLICATION_DIR as _LOAD_LIBRARY_SEARCH_APPLICATION_DIR, @@ -213,11 +234,8 @@ pub(crate) mod module { fn _findfirstfile(path: OsPath, vm: &VirtualMachine) -> PyResult { let filename = host_nt::find_first_file_name(path.as_ref()) .map_err(|err| OSErrorBuilder::with_filename(&err, path.clone(), vm))?; - let filename_str = filename - .to_str() - .ok_or_else(|| vm.new_unicode_decode_error("filename contains invalid UTF-8"))?; - - Ok(vm.ctx.new_str(filename_str)) + let filename_wide: Vec<_> = filename.encode_wide().collect(); + Ok(vm.ctx.new_str(Wtf8Buf::from_wide(&filename_wide))) } #[derive(FromArgs)] @@ -551,8 +569,9 @@ pub(crate) mod module { let value_str = value.expect_str(); // Validate: no null characters in key or value - if key_str.contains('\0') || value_str.contains('\0') { - return Err(vm.new_value_error("embedded null character")); + if key.contains_nuls() || value.contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } // Validate: empty key or '=' in key after position 0 // (search from index 1 because on Windows starting '=' is allowed @@ -687,17 +706,7 @@ pub(crate) mod module { (wide, false) } else if let Some(b) = path.downcast_ref::() { // On Windows, bytes must be valid UTF-8 - this raises UnicodeDecodeError if not - let s = core::str::from_utf8(b.as_bytes()).map_err(|e| { - vm.new_exception_msg( - vm.ctx.exceptions.unicode_decode_error.to_owned(), - format!( - "'utf-8' codec can't decode byte {:#x} in position {}: invalid start byte", - b.as_bytes().get(e.valid_up_to()).copied().unwrap_or(0), - e.valid_up_to() - ) - .into(), - ) - })?; + let s = utf8_from_bytes(b.as_bytes(), vm)?; let wide: Vec = s.encode_utf16().collect(); (wide, true) } else { @@ -718,16 +727,13 @@ pub(crate) mod module { // Return as bytes if input was bytes, preserving the original content if is_bytes { // Convert UTF-16 back to UTF-8 for bytes output - let drv = String::from_utf16(&wide[..drv_size]) - .map_err(|e| vm.new_unicode_decode_error(e.to_string()))?; - let root = String::from_utf16(&wide[drv_size..drv_size + root_size]) - .map_err(|e| vm.new_unicode_decode_error(e.to_string()))?; - let tail = String::from_utf16(&wide[drv_size + root_size..]) - .map_err(|e| vm.new_unicode_decode_error(e.to_string()))?; + let drv = Wtf8Buf::from_wide(&wide[..drv_size]).into_bytes(); + let root = Wtf8Buf::from_wide(&wide[drv_size..drv_size + root_size]).into_bytes(); + let tail = Wtf8Buf::from_wide(&wide[drv_size + root_size..]).into_bytes(); Ok(vm.ctx.new_tuple(vec![ - vm.ctx.new_bytes(drv.into_bytes()).into(), - vm.ctx.new_bytes(root.into_bytes()).into(), - vm.ctx.new_bytes(tail.into_bytes()).into(), + vm.ctx.new_bytes(drv).into(), + vm.ctx.new_bytes(root).into(), + vm.ctx.new_bytes(tail).into(), ])) } else { // For str output, use WTF-8 to handle surrogates @@ -911,17 +917,7 @@ pub(crate) mod module { let wide: Vec = s.as_wtf8().encode_wide().collect(); (wide, false) } else if let Some(b) = path.downcast_ref::() { - let s = core::str::from_utf8(b.as_bytes()).map_err(|e| { - vm.new_exception_msg( - vm.ctx.exceptions.unicode_decode_error.to_owned(), - format!( - "'utf-8' codec can't decode byte {:#x} in position {}: invalid start byte", - b.as_bytes().get(e.valid_up_to()).copied().unwrap_or(0), - e.valid_up_to() - ) - .into(), - ) - })?; + let s = utf8_from_bytes(b.as_bytes(), vm)?; let wide: Vec = s.encode_utf16().collect(); (wide, true) } else { @@ -934,9 +930,8 @@ pub(crate) mod module { let normalized = normpath_wide(&wide); if is_bytes { - let s = String::from_utf16(&normalized) - .map_err(|e| vm.new_unicode_decode_error(e.to_string()))?; - Ok(vm.ctx.new_bytes(s.into_bytes()).into()) + let bytes = Wtf8Buf::from_wide(&normalized).into_bytes(); + Ok(vm.ctx.new_bytes(bytes).into()) } else { let s = Wtf8Buf::from_wide(&normalized); Ok(vm.ctx.new_str(s).into()) diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 4a1cbe2aecd..dccc0ae7e47 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -6,8 +6,9 @@ use crate::{ builtins::{PyModule, PySet}, convert::{IntoPyException, ToPyException, ToPyObject}, function::{ArgumentError, FromArgs, FuncArgs}, - host_env::crt_fd, + host_env::{crt_fd, posix::RawMode}, }; +use core::marker::PhantomData; use std::{io, path::Path}; pub(crate) fn fs_metadata>( @@ -29,7 +30,7 @@ pub struct TargetIsDirectory { } cfg_select! { - all(any(unix, target_os = "wasi"), not(target_os = "redox")) => { + any(unix, target_os = "wasi") => { use libc::AT_FDCWD; } _ => { @@ -39,19 +40,44 @@ cfg_select! { const DEFAULT_DIR_FD: crt_fd::Borrowed<'static> = unsafe { crt_fd::Borrowed::borrow_raw(AT_FDCWD) }; +pub trait DirFdKeyword: Clone + Copy + Eq + PartialEq { + const NAME: &'static str; +} + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct DefaultDirFd; +impl DirFdKeyword for DefaultDirFd { + const NAME: &'static str = "dir_fd"; +} + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct SrcDirFd; +impl DirFdKeyword for SrcDirFd { + const NAME: &'static str = "src_dir_fd"; +} + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct DstDirFd; +impl DirFdKeyword for DstDirFd { + const NAME: &'static str = "dst_dir_fd"; +} + // XXX: AVAILABLE should be a bool, but we can't yet have it as a bool and just cast it to usize -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct DirFd<'fd, const AVAILABLE: usize>(pub(crate) [crt_fd::Borrowed<'fd>; AVAILABLE]); +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct DirFd<'fd, const AVAILABLE: usize, KW: DirFdKeyword = DefaultDirFd>( + pub(crate) [crt_fd::Borrowed<'fd>; AVAILABLE], + PhantomData, +); -impl Default for DirFd<'_, AVAILABLE> { +impl Default for DirFd<'_, AVAILABLE, KW> { fn default() -> Self { - Self([DEFAULT_DIR_FD; AVAILABLE]) + Self([DEFAULT_DIR_FD; AVAILABLE], PhantomData) } } // not used on all platforms #[allow(unused)] -impl<'fd> DirFd<'fd, 1> { +impl<'fd, KW: DirFdKeyword> DirFd<'fd, 1, KW> { #[inline(always)] pub(crate) fn get_opt(self) -> Option> { let [fd] = self.0; @@ -70,9 +96,9 @@ impl<'fd> DirFd<'fd, 1> { } } -impl FromArgs for DirFd<'_, AVAILABLE> { +impl FromArgs for DirFd<'_, AVAILABLE, KW> { fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result { - let fd = match args.take_keyword("dir_fd") { + let fd = match args.take_keyword(KW::NAME) { Some(o) if vm.is_none(&o) => Ok(DEFAULT_DIR_FD), None => Ok(DEFAULT_DIR_FD), Some(o) => { @@ -93,7 +119,7 @@ impl FromArgs for DirFd<'_, AVAILABLE> { .into()); } let fd = fd.map_err(|e| e.to_pyexception(vm))?; - Ok(Self([fd; AVAILABLE])) + Ok(Self([fd; AVAILABLE], PhantomData)) } } @@ -104,8 +130,15 @@ pub(super) struct FollowSymlinks( #[cfg(not(windows))] fn bytes_as_os_str<'a>(b: &'a [u8], vm: &VirtualMachine) -> PyResult<&'a std::ffi::OsStr> { - rustpython_host_env::os::bytes_as_os_str(b) - .map_err(|_| vm.new_unicode_decode_error("can't decode path for utf-8")) + rustpython_host_env::os::bytes_as_os_str(b).map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(b.to_vec()), + e.valid_up_to(), + e.error_len().map_or(b.len(), |n| e.valid_up_to() + n), + vm.ctx.new_str("can't decode path for utf-8"), + ) + }) } pub(crate) fn warn_if_bool_fd(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { @@ -154,7 +187,9 @@ impl ToPyObject for crt_fd::Borrowed<'_> { #[pymodule(sub)] pub(super) mod _os { - use super::{DirFd, FollowSymlinks, SupportFunc}; + use super::{DirFd, DstDirFd, FollowSymlinks, RawMode, SrcDirFd, SupportFunc}; + #[cfg(not(windows))] + use crate::exceptions; use crate::host_env::fileutils::StatStruct; #[cfg(any(unix, windows))] use crate::utils::ToCString; @@ -171,10 +206,15 @@ pub(super) mod _os { ospath::{OsPath, OsPathOrFd, OutputMode, PathConverter}, protocol::PyIterReturn, recursion::ReprGuard, - types::{Destructor, IterNext, Iterable, PyStructSequence, Representable, SelfIter}, + types::{ + Destructor, IterNext, Iterable, PyStructSequence, PyStructSequenceData, Representable, + SelfIter, + }, vm::VirtualMachine, }; - use core::time::Duration; + #[cfg(not(windows))] + use core::marker::PhantomData; + use core::{hint::cold_path, time::Duration}; use crossbeam_utils::atomic::AtomicCell; use rustpython_common::wtf8::Wtf8Buf; #[cfg(windows)] @@ -184,12 +224,12 @@ pub(super) mod _os { use std::{fs, io, path::PathBuf, time::SystemTime}; const OPEN_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); - pub(crate) const MKDIR_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); + pub(crate) const MKDIR_DIR_FD: bool = cfg!(any(unix, target_os = "wasi")); const STAT_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); const UTIME_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); pub(crate) const SYMLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); pub(crate) const UNLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); - const RENAME_DIR_FD: bool = cfg!(unix); + const RENAME_DIR_FD: bool = cfg!(any(unix, target_os = "wasi")); const RMDIR_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox"))); const SCANDIR_FD: bool = cfg!(all(unix, not(target_os = "redox"))); @@ -338,32 +378,24 @@ pub(super) mod _os { } } - #[cfg(not(windows))] #[pyfunction] fn mkdir( path: OsPath, - mode: OptionalArg, - dir_fd: DirFd<'_, { MKDIR_DIR_FD as usize }>, + mode: OptionalArg, + #[cfg_attr(not(any(unix, target_os = "wasi")), expect(unused_variables))] dir_fd: DirFd< + '_, + { MKDIR_DIR_FD as usize }, + >, vm: &VirtualMachine, ) -> PyResult<()> { let mode = mode.unwrap_or(0o777); - let c_path = path.clone().into_cstring(vm)?; - #[cfg(not(target_os = "redox"))] - if let Some(fd) = dir_fd.raw_opt() { - return if let Err(err) = - crate::host_env::posix::make_dir_at(fd, c_path.as_c_str(), mode as u32) - { - Err(OSErrorBuilder::with_filename(&err, path, vm)) - } else { - Ok(()) - }; - } - #[cfg(target_os = "redox")] - let [] = dir_fd.0; - if let Err(err) = crate::host_env::posix::make_dir(c_path.as_c_str(), mode as u32) { - return Err(OSErrorBuilder::with_filename(&err, path, vm)); - } - Ok(()) + #[cfg(any(unix, target_os = "wasi"))] + let dir_fd = dir_fd.get_opt(); + #[cfg(not(any(unix, target_os = "wasi")))] + let dir_fd = None; + + crate::host_env::posix::make_dir(dir_fd, &path.path, mode) + .map_err(|err| OSErrorBuilder::with_filename(&err, path, vm)) } #[pyfunction] @@ -458,10 +490,16 @@ pub(super) mod _os { } #[cfg(not(windows))] - fn env_bytes_as_bytes(obj: &crate::function::Either) -> &[u8] { + fn env_bytes_as_bytes_checked( + obj: &crate::function::Either, + ) -> Option<&[u8]> { match obj { - crate::function::Either::A(s) => s.as_bytes(), - crate::function::Either::B(b) => b.as_bytes(), + crate::function::Either::A(s) if !s.contains_nuls() => Some(s.as_bytes()), + crate::function::Either::B(b) if !b.contains_nuls() => Some(b.as_bytes()), + _ => { + cold_path(); + None + } } } @@ -486,9 +524,10 @@ pub(super) mod _os { // defining hidden environment variables. if key_str.is_empty() || key_str.get(1..).is_some_and(|s| s.contains('=')) - || key_str.contains('\0') - || value_str.contains('\0') + || key.contains_nuls() + || value.contains_nuls() { + cold_path(); return Err(vm.new_value_error("illegal environment variable name")); } let env_str = format!("{key_str}={value_str}"); @@ -508,11 +547,13 @@ pub(super) mod _os { value: crate::function::Either, vm: &VirtualMachine, ) -> PyResult<()> { - let key = env_bytes_as_bytes(&key); - let value = env_bytes_as_bytes(&value); - if key.contains(&b'\0') || value.contains(&b'\0') { - return Err(vm.new_value_error("embedded null byte")); - } + let (Some(key), Some(value)) = ( + env_bytes_as_bytes_checked(&key), + env_bytes_as_bytes_checked(&value), + ) else { + cold_path(); + return Err(exceptions::nul_byte_error(vm)); + }; if key.is_empty() || key.contains(&b'=') { return Err(vm.new_value_error("illegal environment variable name")); } @@ -531,8 +572,9 @@ pub(super) mod _os { // defining hidden environment variables. if key_str.is_empty() || key_str.get(1..).is_some_and(|s| s.contains('=')) - || key_str.contains('\0') + || key.contains_nuls() { + cold_path(); return Err(vm.new_value_error("illegal environment variable name")); } // "key=" to unset (empty value removes the variable) @@ -552,10 +594,10 @@ pub(super) mod _os { key: crate::function::Either, vm: &VirtualMachine, ) -> PyResult<()> { - let key = env_bytes_as_bytes(&key); - if key.contains(&b'\0') { - return Err(vm.new_value_error("embedded null byte")); - } + let Some(key) = env_bytes_as_bytes_checked(&key) else { + cold_path(); + return Err(exceptions::nul_byte_error(vm)); + }; if key.is_empty() || key.contains(&b'=') { let x = vm.new_errno_error( 22, @@ -627,7 +669,7 @@ pub(super) mod _os { // Safety: the fd came from os.open() and is borrowed for // the lifetime of this DirEntry reference. let borrowed = unsafe { crt_fd::Borrowed::borrow_raw(raw_fd) }; - return DirFd([borrowed; STAT_DIR_FD as usize]); + return DirFd([borrowed; STAT_DIR_FD as usize], PhantomData); } DirFd::default() } @@ -797,7 +839,7 @@ pub(super) mod _os { FollowSymlinks(false), ) .map_err(|e| e.into_pyexception(vm))? - .ok_or_else(|| crate::exceptions::cstring_error(vm))?; + .ok_or_else(|| crate::exceptions::nul_char_error(vm))?; // On Windows, combine st_ino and st_ino_high into 128-bit value let ino: u128 = cfg_select! { windows => stat.st_ino as u128 | ((stat.st_ino_high as u128) << 64), @@ -844,7 +886,7 @@ pub(super) mod _os { cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine, - ) -> PyGenericAlias { + ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } @@ -1144,18 +1186,15 @@ pub(super) mod _os { pub st_gid: PyIntRef, pub st_size: PyIntRef, // Indices 7-9: integer seconds - #[cfg_attr(target_env = "musl", allow(deprecated))] #[pyarg(positional, default)] #[pystruct_sequence(unnamed)] - pub st_atime_int: libc::time_t, - #[cfg_attr(target_env = "musl", allow(deprecated))] + pub st_atime_int: i64, #[pyarg(positional, default)] #[pystruct_sequence(unnamed)] - pub st_mtime_int: libc::time_t, - #[cfg_attr(target_env = "musl", allow(deprecated))] + pub st_mtime_int: i64, #[pyarg(positional, default)] #[pystruct_sequence(unnamed)] - pub st_ctime_int: libc::time_t, + pub st_ctime_int: i64, // Float time attributes #[pyarg(any, default)] #[pystruct_sequence(skip)] @@ -1180,11 +1219,11 @@ pub(super) mod _os { #[cfg(not(windows))] #[pyarg(any, default)] #[pystruct_sequence(skip)] - pub st_blksize: i64, + pub st_blksize: u64, #[cfg(not(windows))] #[pyarg(any, default)] #[pystruct_sequence(skip)] - pub st_blocks: i64, + pub st_blocks: u64, #[cfg(windows)] #[pyarg(any, default)] #[pystruct_sequence(skip)] @@ -1198,19 +1237,12 @@ pub(super) mod _os { impl StatResultData { fn from_stat(stat: &StatStruct, vm: &VirtualMachine) -> Self { let (atime, mtime, ctime); - #[cfg(any(unix, windows))] - #[cfg(not(any(target_os = "netbsd", target_os = "wasi")))] + #[cfg(all(any(unix, windows), not(target_os = "wasi")))] { atime = (stat.st_atime, stat.st_atime_nsec); mtime = (stat.st_mtime, stat.st_mtime_nsec); ctime = (stat.st_ctime, stat.st_ctime_nsec); } - #[cfg(target_os = "netbsd")] - { - atime = (stat.st_atime, stat.st_atimensec); - mtime = (stat.st_mtime, stat.st_mtimensec); - ctime = (stat.st_ctime, stat.st_ctimensec); - } #[cfg(target_os = "wasi")] { atime = (stat.st_atim.tv_sec, stat.st_atim.tv_nsec); @@ -1235,12 +1267,18 @@ pub(super) mod _os { let st_ino = stat.st_ino; #[cfg(not(windows))] - #[allow(clippy::useless_conversion, reason = "needed for 32-bit platforms")] - let st_blksize = i64::from(stat.st_blksize); + #[allow( + clippy::useless_conversion, + reason = "signedness differs between platforms" + )] + let st_blksize = stat.st_blksize.try_into().unwrap_or(4096); #[cfg(not(windows))] - #[allow(clippy::useless_conversion, reason = "needed for 32-bit platforms")] - let st_blocks = i64::from(stat.st_blocks); + #[allow( + clippy::useless_conversion, + reason = "signedness differs between platforms" + )] + let st_blocks = stat.st_blocks.try_into().unwrap_or_default(); Self { st_mode: vm.ctx.new_pyref(stat.st_mode), @@ -1279,8 +1317,12 @@ pub(super) mod _os { impl PyStatResult { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let seq: PyObjectRef = args.bind(vm)?; - let result = crate::types::struct_sequence_new(cls.clone(), seq, vm)?; + let result = crate::types::struct_sequence_new( + cls.clone(), + args.bind(vm)?, + StatResultData::OPTIONAL_FIELD_NAMES, + vm, + )?; let tuple = result.downcast_ref::().unwrap(); let mut items: Vec = tuple.to_vec(); @@ -1321,11 +1363,9 @@ pub(super) mod _os { follow_symlinks: FollowSymlinks, ) -> io::Result> { match file { - OsPathOrFd::Path(path) => host_posix::stat_path( - path.as_ref().as_os_str(), - dir_fd.raw_opt(), - follow_symlinks.0, - ), + OsPathOrFd::Path(path) => { + host_posix::stat_path(path, dir_fd.get_opt(), follow_symlinks.0) + } OsPathOrFd::Fd(fd) => host_posix::stat_fd(fd).map(Some), } } @@ -1340,7 +1380,7 @@ pub(super) mod _os { ) -> PyResult { let stat = stat_inner(file.clone(), dir_fd, follow_symlinks) .map_err(|err| OSErrorBuilder::with_filename(&err, file, vm))? - .ok_or_else(|| crate::exceptions::cstring_error(vm))?; + .ok_or_else(|| crate::exceptions::nul_char_error(vm))?; Ok(StatResultData::from_stat(&stat, vm).to_pyobject(vm)) } @@ -1384,14 +1424,15 @@ pub(super) mod _os { src: PyObjectRef, #[pyarg(positional)] dst: PyObjectRef, - #[pyarg(any, default)] - src_dir_fd: OptionalArg>, - #[pyarg(any, default)] - dst_dir_fd: OptionalArg>, + #[pyarg(flatten)] + #[cfg_attr(not(any(unix, target_os = "wasi")), expect(dead_code))] + src_dir_fd: DirFd<'fd, { RENAME_DIR_FD as usize }, SrcDirFd>, + #[pyarg(flatten)] + #[cfg_attr(not(any(unix, target_os = "wasi")), expect(dead_code))] + dst_dir_fd: DirFd<'fd, { RENAME_DIR_FD as usize }, DstDirFd>, } #[pyfunction] - #[pyfunction(name = "replace")] fn rename(args: RenameArgs<'_>, vm: &VirtualMachine) -> PyResult<()> { let src = PathConverter::new() .function("rename") @@ -1402,13 +1443,46 @@ pub(super) mod _os { .argument("dst") .try_path(args.dst, vm)?; - crate::host_env::os::rename( - &src, - args.src_dir_fd.into_option(), - &dst, - args.dst_dir_fd.into_option(), - ) - .map_err(|err| { + #[cfg(any(unix, target_os = "wasi"))] + let src_dir_fd = args.src_dir_fd.get_opt(); + #[cfg(not(any(unix, target_os = "wasi")))] + let src_dir_fd = None; + + #[cfg(any(unix, target_os = "wasi"))] + let dst_dir_fd = args.dst_dir_fd.get_opt(); + #[cfg(not(any(unix, target_os = "wasi")))] + let dst_dir_fd = None; + + crate::host_env::posix::rename(&src, src_dir_fd, &dst, dst_dir_fd).map_err(|err| { + let builder = err.to_os_error_builder(vm); + let builder = builder.filename(src.filename(vm)); + let builder = builder.filename2(dst.filename(vm)); + builder.build(vm).upcast() + }) + } + + #[pyfunction] + fn replace(args: RenameArgs<'_>, vm: &VirtualMachine) -> PyResult<()> { + let src = PathConverter::new() + .function("replace") + .argument("src") + .try_path(args.src, vm)?; + let dst = PathConverter::new() + .function("replace") + .argument("dst") + .try_path(args.dst, vm)?; + + #[cfg(any(unix, target_os = "wasi"))] + let src_dir_fd = args.src_dir_fd.get_opt(); + #[cfg(not(any(unix, target_os = "wasi")))] + let src_dir_fd = None; + + #[cfg(any(unix, target_os = "wasi"))] + let dst_dir_fd = args.dst_dir_fd.get_opt(); + #[cfg(not(any(unix, target_os = "wasi")))] + let dst_dir_fd = None; + + crate::host_env::posix::replace(&src, src_dir_fd, &dst, dst_dir_fd).map_err(|err| { let builder = err.to_os_error_builder(vm); let builder = builder.filename(src.filename(vm)); let builder = builder.filename2(dst.filename(vm)); @@ -1489,10 +1563,12 @@ pub(super) mod _os { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; + + use crate::convert::ToPyException; let src_cstr = alloc::ffi::CString::new(src.path.as_os_str().as_bytes()) - .map_err(|_| vm.new_value_error("embedded null byte"))?; + .map_err(|e| e.to_pyexception(vm))?; let dst_cstr = alloc::ffi::CString::new(dst.path.as_os_str().as_bytes()) - .map_err(|_| vm.new_value_error("embedded null byte"))?; + .map_err(|e| e.to_pyexception(vm))?; let follow = follow_symlinks.into_option().unwrap_or(true); if let Err(err) = @@ -1795,9 +1871,9 @@ pub(super) mod _os { #[cfg(all(unix, not(any(target_os = "redox", target_os = "android"))))] #[pyfunction] fn getloadavg(vm: &VirtualMachine) -> PyResult<(f64, f64, f64)> { - let loadavg = crate::host_env::time::getloadavg() - .map_err(|_| vm.new_os_error("Load averages are unobtainable"))?; - Ok((loadavg[0], loadavg[1], loadavg[2])) + crate::host_env::time::getloadavg() + .map(Into::into) + .map_err(|_| vm.new_os_error("Load averages are unobtainable")) } #[cfg(unix)] @@ -1816,7 +1892,8 @@ pub(super) mod _os { #[cfg(windows)] #[pyfunction] - fn waitstatus_to_exitcode(status: u64, vm: &VirtualMachine) -> PyResult { + fn waitstatus_to_exitcode(status: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let status = status.try_index(vm)?.try_to_primitive_raw::(vm)?; let exitcode = status >> 8; // ExitProcess() accepts an UINT type: // reject exit code which doesn't fit in an UINT @@ -1894,8 +1971,12 @@ pub(super) mod _os { impl PyStatvfsResult { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let seq: PyObjectRef = args.bind(vm)?; - crate::types::struct_sequence_new(cls, seq, vm) + crate::types::struct_sequence_new( + cls, + args.bind(vm)?, + StatvfsResultData::OPTIONAL_FIELD_NAMES, + vm, + ) } } @@ -1918,7 +1999,6 @@ pub(super) mod _os { } } - /// Perform a statvfs system call on the given path. #[cfg(all(unix, not(target_os = "redox")))] #[pyfunction] #[pyfunction(name = "fstatvfs")] diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 21576e8da2d..9233af9fbe0 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -623,13 +623,20 @@ pub mod module { run_at_forkers(before_forkers, true, vm); #[cfg(feature = "threading")] - crate::stdlib::_imp::acquire_imp_lock_for_fork(); + crate::stdlib::_imp::acquire_imp_lock_for_fork(vm); #[cfg(feature = "threading")] - vm.state.stop_the_world.stop_the_world(vm); + vm.state.stop_the_world.stop_the_world(&vm.state); } fn py_os_after_fork_child(vm: &VirtualMachine) { + // The interpreter registry is reachable from every thread, so repair it + // before anything enumerates interpreters. + #[cfg(all(unix, feature = "threading"))] + unsafe { + crate::vm::runtime::reinit_after_fork() + }; + #[cfg(feature = "threading")] vm.state.stop_the_world.reset_after_fork(); @@ -639,6 +646,12 @@ pub mod module { #[cfg(feature = "threading")] reinit_locks_after_fork(vm); + // The collector stops every interpreter, so interpreters other than the + // forking one must be repaired too; otherwise the child's first + // collection waits for threads that did not survive the fork. + #[cfg(all(unix, feature = "threading"))] + reinit_other_interpreters_after_fork(vm); + // Reinit per-object IO buffer locks on std streams. // BufferedReader/Writer/TextIOWrapper use PyThreadMutex which can be // held by dead parent threads, causing deadlocks on any IO in the child. @@ -655,6 +668,17 @@ pub mod module { #[cfg(feature = "threading")] crate::object::reset_weakref_locks_after_fork(); + // Repair any type-cache entries left mid-update at fork time. + unsafe { crate::builtins::type_::type_cache_after_fork() }; + + // Reset QSBR: dead parent threads' slots would stall reclamation + // forever, and retired memory can be freed immediately in the + // single-threaded child. + #[cfg(feature = "threading")] + unsafe { + crate::object::qsbr::QSBR.reset_after_fork() + }; + // Phase 3: Clean up thread state. Locks are now reinit'd so we can // acquire them normally instead of using try_lock(). #[cfg(feature = "threading")] @@ -694,6 +718,7 @@ pub mod module { reinit_mutex_after_fork(&vm.state.atexit_funcs); reinit_mutex_after_fork(&vm.state.global_trace_func); reinit_mutex_after_fork(&vm.state.global_profile_func); + reinit_mutex_after_fork(&vm.state.type_mutex); reinit_mutex_after_fork(&vm.state.monitoring); // PyGlobalState parking_lot::Mutex locks @@ -707,17 +732,67 @@ pub mod module { // Codec registry RwLock vm.state.codec_registry.reinit_after_fork(); - // GC state (multiple Mutex + RwLock) + // GC state (multiple Mutex + RwLock), shared lists and this + // interpreter's own policy state. crate::gc_state::gc_state().reinit_after_fork(); + vm.state.gc.reinit_after_fork(); // Import lock (RawReentrantMutex) crate::stdlib::_imp::reinit_imp_lock_after_fork(); } } + /// Repair every live interpreter other than the forking one after `fork()`. + /// + /// Only the forking thread survives, so each other interpreter is left with + /// slots for threads that no longer exist (still ATTACHED if they were + /// running bytecode) and possibly locks or stop-the-world flags held by + /// them. Since a collection stops all interpreters, that state would hang + /// the child's first collection. + /// + /// # Safety + /// Must only be called after `fork()` in the child, when no other threads exist. + #[cfg(all(unix, feature = "threading"))] + fn reinit_other_interpreters_after_fork(vm: &VirtualMachine) { + use rustpython_common::lock::reinit_mutex_after_fork; + + for state in crate::vm::runtime::live_interpreter_states() { + if state.interpreter_id == vm.state.interpreter_id { + continue; + } + + unsafe { + reinit_mutex_after_fork(&state.before_forkers); + reinit_mutex_after_fork(&state.after_forkers_child); + reinit_mutex_after_fork(&state.after_forkers_parent); + reinit_mutex_after_fork(&state.atexit_funcs); + reinit_mutex_after_fork(&state.global_trace_func); + reinit_mutex_after_fork(&state.global_profile_func); + reinit_mutex_after_fork(&state.type_mutex); + reinit_mutex_after_fork(&state.monitoring); + reinit_mutex_after_fork(&state.thread_frames); + reinit_mutex_after_fork(&state.thread_handles); + reinit_mutex_after_fork(&state.shutdown_handles); + + state.codec_registry.reinit_after_fork(); + state.gc.reinit_after_fork(); + } + + state.stop_the_world.reset_after_fork(); + + // Every thread registered here belongs to the parent, including any + // slot the forking thread itself registered before the fork. + state.thread_frames.lock().clear(); + state.thread_handles.lock().clear(); + state.shutdown_handles.lock().clear(); + } + + crate::vm::thread::purge_other_interpreter_slots_after_fork(vm.state.interpreter_id); + } + fn py_os_after_fork_parent(vm: &VirtualMachine) { #[cfg(feature = "threading")] - vm.state.stop_the_world.start_the_world(vm); + vm.state.stop_the_world.start_the_world(&vm.state); #[cfg(feature = "threading")] crate::stdlib::_imp::release_imp_lock_after_fork_parent(); @@ -1265,8 +1340,20 @@ pub mod module { #[pyfunction] fn uname(vm: &VirtualMachine) -> PyResult<_os::UnameResultData> { - let info = rustpython_host_env::posix::uname_info() - .map_err(|err| vm.new_unicode_decode_error(err.to_string()))?; + let info = rustpython_host_env::posix::uname_info().map_err(|err| { + let start = err.error.valid_up_to(); + let end = err + .error + .error_len() + .map_or(err.bytes.len(), |len| start + len); + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(err.bytes), + start, + end, + vm.ctx.new_str(err.error.to_string()), + ) + })?; Ok(_os::UnameResultData { sysname: info.sysname, nodename: info.nodename, @@ -1321,14 +1408,13 @@ pub mod module { // cfg from nix #[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "redox")))] #[pyfunction] - fn setgroups( - group_ids: crate::function::ArgIterable, - vm: &VirtualMachine, - ) -> PyResult<()> { - let gids = group_ids - .iter(vm)? - .map(|gid| gid.map(|gid| gid.0)) - .collect::, _>>()?; + fn setgroups(group_ids: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + group_ids + .try_sequence(vm) + .map_err(|_| vm.new_type_error("setgroups argument must be a sequence"))?; + let gids = vm.extract_elements_with(&group_ids, |gid| { + RawGid::try_from_object(vm, gid).map(|gid| gid.0) + })?; rustpython_host_env::posix::setgroups_raw(&gids).map_err(|err| err.into_pyexception(vm)) } @@ -1388,7 +1474,7 @@ pub mod module { #[pyarg(positional)] path: OsPath, #[pyarg(positional)] - args: crate::function::ArgIterable, + args: PyObjectRef, #[pyarg(positional)] env: Option, #[pyarg(named, default)] @@ -1427,6 +1513,19 @@ pub mod module { .into_cstring(vm) .map_err(|_| vm.new_value_error("path should not have nul bytes"))?; + let function_name = if spawnp { + "posix_spawnp" + } else { + "posix_spawn" + }; + if !self.args.fast_isinstance(vm.ctx.types.list_type) + && !self.args.fast_isinstance(vm.ctx.types.tuple_type) + { + return Err( + vm.new_type_error(format!("{function_name}: argv must be a tuple or list")) + ); + } + let mut file_actions = Vec::new(); if let Some(it) = self.file_actions { for action in it.iter(vm)? { @@ -1466,20 +1565,21 @@ pub mod module { } } - let setsigdef = self - .setsigdef - .map(|sigs| { - let sigs = sigs.iter(vm)?.collect::>>()?; - for &sig in &sigs { - if !rustpython_host_env::posix::validate_posix_spawn_signal(sig) { - return Err( - vm.new_value_error(format!("signal number {sig} out of range")) - ); - } + let collect_signals = |sigs: crate::function::ArgIterable| { + let mut collected = Vec::new(); + for sig in sigs.iter(vm)? { + let sig = sig?; + if !rustpython_host_env::posix::validate_posix_spawn_signal(sig) { + return Err(vm.new_value_error(format!("signal number {sig} out of range"))); } - Ok(sigs) - }) - .transpose()?; + if !collected.contains(&sig) { + collected.push(sig); + } + } + Ok(collected) + }; + + let setsigdef = self.setsigdef.map(&collect_signals).transpose()?; if let Some(_scheduler) = self.scheduler { // TODO: Implement scheduler parameter handling @@ -1495,29 +1595,12 @@ pub mod module { )); } - let setsigmask = self - .setsigmask - .map(|sigs| { - let sigs = sigs.iter(vm)?.collect::>>()?; - for &sig in &sigs { - if !rustpython_host_env::posix::validate_posix_spawn_signal(sig) { - return Err( - vm.new_value_error(format!("signal number {sig} out of range")) - ); - } - } - Ok(sigs) - }) - .transpose()?; + let setsigmask = self.setsigmask.map(collect_signals).transpose()?; - let args: Vec = self - .args - .iter(vm)? - .map(|res| { - CString::new(res?.into_bytes()) - .map_err(|_| vm.new_value_error("path should not have nul bytes")) - }) - .collect::>()?; + let args = vm.extract_elements_with(&self.args, |arg| { + CString::new(OsPath::try_from_object(vm, arg)?.into_bytes()) + .map_err(|_| vm.new_value_error("path should not have nul bytes")) + })?; let env = if let Some(env_dict) = self.env { envp_from_dict(env_dict, vm)? } else { @@ -1719,10 +1802,16 @@ pub mod module { let Some(login) = rustpython_host_env::posix::getlogin() else { return Err(vm.new_os_error("unable to determine login name")); }; - login - .to_str() - .map(|s| s.to_owned()) - .map_err(|e| vm.new_unicode_decode_error(format!("unable to decode login name: {e}"))) + login.to_str().map(|s| s.to_owned()).map_err(|e| { + vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(login.as_bytes().to_vec()), + e.valid_up_to(), + e.error_len() + .map_or(login.as_bytes().len(), |n| e.valid_up_to() + n), + vm.ctx.new_str("unable to decode login name"), + ) + }) } // cfg from nix diff --git a/crates/vm/src/stdlib/posix_compat.rs b/crates/vm/src/stdlib/posix_compat.rs index c50134a33a4..9afea821c9b 100644 --- a/crates/vm/src/stdlib/posix_compat.rs +++ b/crates/vm/src/stdlib/posix_compat.rs @@ -60,7 +60,7 @@ pub(crate) mod module { #[allow(dead_code)] fn os_unimpl(func: &str, vm: &VirtualMachine) -> PyResult { - Err(vm.new_os_error(format!("{} is not supported on this platform", func))) + Err(vm.new_os_error(format!("{func} is not supported on this platform"))) } pub(crate) fn support_funcs() -> Vec { diff --git a/crates/vm/src/stdlib/pwd.rs b/crates/vm/src/stdlib/pwd.rs index e2f987ce019..cfd571e4c17 100644 --- a/crates/vm/src/stdlib/pwd.rs +++ b/crates/vm/src/stdlib/pwd.rs @@ -11,6 +11,7 @@ mod pwd { exceptions, types::PyStructSequence, }; + use core::hint::cold_path; use rustpython_host_env::pwd as host_pwd; #[cfg(not(target_os = "android"))] @@ -50,15 +51,16 @@ mod pwd { #[pyfunction] fn getpwnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - let pw_name = name.as_str(); - if pw_name.contains('\0') { - return Err(exceptions::cstring_error(vm)); + if name.as_pystr().contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } - let user = host_pwd::getpwnam(name.as_str()); + let name = name.as_str(); + let user = host_pwd::getpwnam(name); let user = user.ok_or_else(|| { vm.new_key_error( vm.ctx - .new_str(format!("getpwnam(): name not found: {pw_name}")) + .new_str(format!("getpwnam(): name not found: {name}")) .into(), ) })?; diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 58324a3c071..5ee36b450d4 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -4,6 +4,7 @@ use crate::{Py, PyPayload, PyResult, VirtualMachine, builtins::PyModule, convert #[cfg(all(not(feature = "host_env"), feature = "stdio"))] pub(crate) use sys::SandboxStdio; +pub use sys::{COPYRIGHT, PLATFORM}; pub(crate) use sys::{DOC, MAXSIZE, RUST_MULTIARCH, UnraisableHookArgsData, module_def, multiarch}; #[pymodule(name = "_jit")] @@ -43,13 +44,14 @@ pub mod sys { hash::{PyHash, PyUHash}, }, convert::ToPyObject, - frame::{Frame, FrameRef}, + frame::FrameObjectRef, function::{FuncArgs, KwArgs, OptionalArg, PosArgs}, stdlib::{_warnings::warn, builtins}, types::PyStructSequence, version, vm::{Settings, VirtualMachine}, }; + use core::ffi::CStr; use core::sync::atomic::Ordering; use num_traits::ToPrimitive; use std::{ @@ -222,7 +224,7 @@ pub mod sys { #[pyattr(name = "api_version")] const API_VERSION: u32 = 0x0; // what C api? #[pyattr(name = "copyright")] - const COPYRIGHT: &str = "Copyright (c) 2019 RustPython Team"; + pub const COPYRIGHT: &CStr = c"Copyright (c) 2019 RustPython Team"; #[pyattr(name = "float_repr_style")] const FLOAT_REPR_STYLE: &str = "short"; #[pyattr(name = "_framework")] @@ -235,14 +237,14 @@ pub mod sys { const MAXUNICODE: u32 = core::char::MAX as u32; #[pyattr(name = "platform")] - pub const PLATFORM: &str = cfg_select! { - target_os = "linux" => "linux", - target_os = "android" => "android", - target_os = "macos" => "darwin", - target_os = "ios" => "ios", - windows => "win32", - target_os = "wasi" => "wasi", - _ => "unknown" + pub const PLATFORM: &CStr = cfg_select! { + target_os = "linux" => c"linux", + target_os = "android" => c"android", + target_os = "macos" => c"darwin", + target_os = "ios" => c"ios", + windows => c"win32", + target_os = "wasi" => c"wasi", + _ => c"unknown" }; #[pyattr(name = "ps1")] @@ -667,7 +669,8 @@ pub mod sys { "_multiarch" => ctx.new_str(multiarch()), "version" => PyVersionInfo::from_data(VersionInfoData::IMPLEMENTATION, vm), "hexversion" => ctx.new_int(version::VERSION_HEX_IMPL), - "supports_isolated_interpreters" => ctx.new_bool(false), + "supports_isolated_interpreters" => + ctx.new_bool(crate::vm::runtime::SUPPORTS_ISOLATED_INTERPRETERS), }) } @@ -751,7 +754,7 @@ pub mod sys { .read_to_string(&mut source) .map_err(|e| vm.new_os_error(format!("Error reading from stdin: {e}")))?; vm.compile(&source, crate::compiler::Mode::Single, "") - .map_err(|e| vm.new_os_error(format!("Error running stdin: {e}")))?; + .map_err(|e| e.into_pyexception(vm, Some(&source)))?; Ok(()) } @@ -774,8 +777,7 @@ pub mod sys { } else { vec![status] }; - let exc = vm.invoke_exception(vm.ctx.exceptions.system_exit.to_owned(), args)?; - Err(exc) + Err(vm.new_system_exit(args.into())) } #[pyfunction] @@ -816,8 +818,27 @@ pub mod sys { vm: &VirtualMachine, ) -> PyResult<()> { let stderr = super::get_stderr(vm)?; + // Keep runtime SyntaxErrors on the normal traceback path. + let has_traceback = !vm.is_none(&exc_tb); match vm.normalize_exception(exc_type, exc_val.clone(), exc_tb) { Ok(exc) => { + let native_syntax_error_display = !has_traceback + && exc.fast_isinstance(vm.ctx.exceptions.syntax_error) + && exc + .as_object() + .get_attr("msg", vm) + .ok() + .and_then(|msg| msg.downcast::().ok()) + .is_some_and(|msg| msg.to_string_lossy() == "unexpected EOF while parsing") + && exc + .as_object() + .get_attr("text", vm) + .ok() + .and_then(|text| text.downcast::().ok()) + .is_some_and(|text| text.to_string_lossy().trim_end() == "\\"); + if native_syntax_error_display { + return vm.write_exception(&mut crate::py_io::PyWriter(stderr, vm), &exc); + } // PyErr_Display: try traceback._print_exception_bltin first if let Ok(tb_mod) = vm.import("traceback", 0) && let Ok(print_exc_builtin) = tb_mod.get_attr("_print_exception_bltin", vm) @@ -868,8 +889,7 @@ pub mod sys { format!("Ignoring unimportable $PYTHONBREAKPOINT: \"{env_var}\"",), 0, vm, - ) - .unwrap(); + )?; Ok(vm.ctx.none()) }; @@ -966,19 +986,11 @@ pub mod sys { } #[pyfunction] - fn _getframe(offset: OptionalArg, vm: &VirtualMachine) -> PyResult { + fn _getframe(offset: OptionalArg, vm: &VirtualMachine) -> PyResult { let offset = offset.into_option().unwrap_or(0); - let frame_ref = { - let frames = vm.frames.borrow(); - if offset >= frames.len() { - return Err(vm.new_value_error("call stack is not deep enough")); - } - - let idx = frames.len() - offset - 1; - // SAFETY: the FrameRef is alive on the call stack while it's in the Vec - let py: &crate::Py = unsafe { frames[idx].as_ref() }; - py.to_owned() - }; + let frame_ref = crate::frame::frame_at_offset(offset, vm) + .ok_or_else(|| vm.new_value_error("call stack is not deep enough"))?; + frame_ref.mark_escaped(); if let Ok(audit) = vm.sys_module.get_attr("audit", vm) { audit.call((vm.ctx.new_str("sys._getframe"), frame_ref.to_owned()), vm)?; @@ -998,15 +1010,9 @@ pub mod sys { } // Get the frame at the specified depth - let func_obj = { - let frames = vm.frames.borrow(); - if depth >= frames.len() { - return Ok(vm.ctx.none()); - } - let idx = frames.len() - depth - 1; - // SAFETY: the FrameRef is alive on the call stack while it's in the Vec - let frame: &crate::Py = unsafe { frames[idx].as_ref() }; - frame.func_obj.clone() + let func_obj = match crate::frame::frame_at_offset(depth, vm) { + Some(frame) => frame.iframe().func_obj().map(|o| o.to_owned()), + None => return Ok(vm.ctx.none()), }; // If the frame has a function object, return its __module__ attribute @@ -1228,7 +1234,7 @@ pub mod sys { vm.state.int_max_str_digits.store(maxdigits); Ok(()) } else { - let error = format!("maxdigits must be 0 or larger than {threshold:?}"); + let error = format!("maxdigits must be 0 or larger than {threshold}"); Err(vm.new_value_error(error)) } } @@ -1744,12 +1750,54 @@ pub mod sys { } for hook in hooks { - hook.call((event.clone(), args.clone()), vm)?; + call_audit_hook(&hook, event.clone().into(), args, vm)?; } Ok(()) } + fn audit_hook_can_trace(hook: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + match hook.get_attr("__cantrace__", vm) { + Ok(can_trace) => can_trace.try_to_bool(vm), + Err(exc) + if exc + .class() + .fast_issubclass(vm.ctx.exceptions.attribute_error) => + { + Ok(false) + } + Err(exc) => Err(exc), + } + } + + fn call_audit_hook( + hook: &PyObjectRef, + event: PyObjectRef, + args: &PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + // Tracing is suppressed while dispatching Python audit hooks, + // except for hooks that explicitly opt in with __cantrace__. + vm.enter_tracing(); + let can_trace = audit_hook_can_trace(hook, vm); + let result = match can_trace { + Ok(can_trace) => { + if can_trace { + vm.leave_tracing(); + } + let result = hook.call((event, args.clone()), vm).map(|_| ()); + if can_trace { + vm.enter_tracing(); + } + result + } + Err(exc) => Err(exc), + }; + + vm.leave_tracing(); + result + } + #[pyfunction] fn audit(event: PyStrRef, args: PosArgs, vm: &VirtualMachine) -> PyResult<()> { if vm.audit_hooks.borrow().is_empty() { @@ -1773,10 +1821,13 @@ pub mod sys { let event: PyObjectRef = vm.ctx.new_str("sys.addaudithook").into(); for existing_hook in hooks { - let Err(exc) = existing_hook.call((event.clone(), args.clone()), vm) else { + let Err(exc) = call_audit_hook(&existing_hook, event.clone(), &args, vm) else { continue; }; - if exc.class().fast_issubclass(vm.ctx.exceptions.runtime_error) { + if exc + .class() + .fast_issubclass(vm.ctx.exceptions.exception_type) + { return Ok(()); } return Err(exc); @@ -1866,7 +1917,7 @@ pub(crate) fn sysconfigdata_name() -> String { format!( "_sysconfigdata_{}_{}_{}", sys::ABIFLAGS, - sys::PLATFORM, + sys::PLATFORM.to_string_lossy(), sys::multiarch() ) } diff --git a/crates/vm/src/stdlib/sys/monitoring.rs b/crates/vm/src/stdlib/sys/monitoring.rs index 6e61692507d..a4be3ba5a5a 100644 --- a/crates/vm/src/stdlib/sys/monitoring.rs +++ b/crates/vm/src/stdlib/sys/monitoring.rs @@ -1,5 +1,5 @@ use crate::{ - AsObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyCode, PyDictRef, PyNamespace, PyUtf8StrRef, code::CoMonitoringData}, function::FuncArgs, }; @@ -344,7 +344,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) { continue; } // Excluded: RESUME, END_FOR, CACHE (and their instrumented variants) - let base = op.to_base().map_or(op, |b| b); + let base = op.to_base().unwrap_or(op); if matches!( base, Instruction::Resume { .. } | Instruction::EndFor | Instruction::Cache @@ -387,7 +387,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) { .skip(first_traceable) { let op = unit.op; - let base = op.to_base().map_or(op, |b| b); + let base = op.to_base().unwrap_or(op); if matches!(base, Instruction::ExtendedArg) { continue; } @@ -425,7 +425,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) { let mut instr_idx = first_traceable; for unit in code.code.instructions[first_traceable..len].iter().copied() { let (op, arg) = arg_state.get(unit); - let base = op.to_base().map_or(op, |b| b); + let base = op.to_base().unwrap_or(op); if matches!(base, Instruction::ExtendedArg) || matches!(base, Instruction::Cache) { instr_idx += 1; @@ -460,7 +460,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) { && !no_loc_mask.get(target_idx).copied().unwrap_or(false) { let target_op = code.code.instructions[target_idx].op; - let target_base = target_op.to_base().map_or(target_op, |b| b); + let target_base = target_op.to_base().unwrap_or(target_op); // Skip synthetic cleanup targets. if matches!(target_base, Instruction::PopIter) { instr_idx += 1; @@ -483,7 +483,7 @@ pub(crate) fn instrument_code(code: &PyCode, events: u32) { && !no_loc_mask.get(target_idx).copied().unwrap_or(false) { let target_op = code.code.instructions[target_idx].op; - let target_base = target_op.to_base().map_or(target_op, |b| b); + let target_base = target_op.to_base().unwrap_or(target_op); if !matches!(target_base, Instruction::PopIter) && let Some((loc, _)) = line_locations.get(target_idx) && loc.line.get() > 0 @@ -528,16 +528,21 @@ fn update_events_mask(vm: &VirtualMachine, state: &MonitoringState) { // Each code object gets only the events that apply to it (global + its // own local events), preventing e.g. INSTRUCTION from being applied to // unrelated code objects. - for fp in vm.frames.borrow().iter() { - // SAFETY: frames in the Vec are alive while their FrameRef is on the call stack. - let frame = unsafe { fp.as_ref() }; - let code = &frame.code; - let code_ver = code.instrumentation_version.load(Ordering::Acquire); - if code_ver != new_ver { - let code_events = state.events_for_code(code.get_id()); - instrument_code(code, code_events); - code.instrumentation_version - .store(new_ver, Ordering::Release); + // Re-instrument all frames on the current thread's stack, including + // data stack frames that have no FrameObject. + { + let mut cur = crate::vm::thread::get_current_frame(); + while !cur.is_null() { + let iframe_ref = unsafe { &*cur }; + let code = iframe_ref.code(); + let code_ver = code.instrumentation_version.load(Ordering::Acquire); + if code_ver != new_ver { + let code_events = state.events_for_code(code.get_id()); + instrument_code(code, code_events); + code.instrumentation_version + .store(new_ver, Ordering::Release); + } + cur = iframe_ref.previous(); } } } @@ -742,12 +747,12 @@ thread_local! { fn fire( vm: &VirtualMachine, event: u32, - code: &PyRef, + code: &Py, offset: u32, cb_extra: &[PyObjectRef], ) -> PyResult<()> { // Prevent recursive event firing - if FIRING.with(|f| f.get()) { + if vm.tracing_is_suppressed() || FIRING.with(|f| f.get()) { return Ok(()); } @@ -790,11 +795,12 @@ fn fire( } let mut args_vec = Vec::with_capacity(1 + cb_extra.len()); - args_vec.push(code.clone().into()); + args_vec.push(code.to_owned().into()); args_vec.extend_from_slice(cb_extra); let args = FuncArgs::from(args_vec); FIRING.with(|f| f.set(true)); + vm.enter_tracing(); let result = (|| { for (tool, cb) in callbacks { let result = cb.call(args.clone(), vm)?; @@ -817,17 +823,14 @@ fn fire( } Ok(()) })(); + vm.leave_tracing(); FIRING.with(|f| f.set(false)); result } // Public dispatch functions (called from frame.rs) -pub(crate) fn fire_py_start( - vm: &VirtualMachine, - code: &PyRef, - offset: u32, -) -> PyResult<()> { +pub(crate) fn fire_py_start(vm: &VirtualMachine, code: &Py, offset: u32) -> PyResult<()> { fire( vm, EVENT_PY_START, @@ -837,11 +840,7 @@ pub(crate) fn fire_py_start( ) } -pub(crate) fn fire_py_resume( - vm: &VirtualMachine, - code: &PyRef, - offset: u32, -) -> PyResult<()> { +pub(crate) fn fire_py_resume(vm: &VirtualMachine, code: &Py, offset: u32) -> PyResult<()> { fire( vm, EVENT_PY_RESUME, @@ -853,7 +852,7 @@ pub(crate) fn fire_py_resume( pub(crate) fn fire_py_return( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, retval: &PyObjectRef, ) -> PyResult<()> { @@ -868,7 +867,7 @@ pub(crate) fn fire_py_return( pub(crate) fn fire_py_yield( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, retval: &PyObjectRef, ) -> PyResult<()> { @@ -883,7 +882,7 @@ pub(crate) fn fire_py_yield( pub(crate) fn fire_call( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, callable: &PyObjectRef, arg0: PyObjectRef, @@ -899,7 +898,7 @@ pub(crate) fn fire_call( pub(crate) fn fire_c_return( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, callable: &PyObjectRef, arg0: PyObjectRef, @@ -915,7 +914,7 @@ pub(crate) fn fire_c_return( pub(crate) fn fire_c_raise( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, callable: &PyObjectRef, arg0: PyObjectRef, @@ -931,7 +930,7 @@ pub(crate) fn fire_c_raise( pub(crate) fn fire_line( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, line: u32, ) -> PyResult<()> { @@ -940,7 +939,7 @@ pub(crate) fn fire_line( pub(crate) fn fire_instruction( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, ) -> PyResult<()> { fire( @@ -954,7 +953,7 @@ pub(crate) fn fire_instruction( pub(crate) fn fire_raise( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, exception: &PyObjectRef, ) -> PyResult<()> { @@ -971,7 +970,7 @@ pub(crate) fn fire_raise( /// preventing duplicate events from chained cleanup handlers. pub(crate) fn fire_reraise( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, exception: &PyObjectRef, ) -> PyResult<()> { @@ -994,7 +993,7 @@ pub(crate) fn fire_reraise( pub(crate) fn fire_exception_handled( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, exception: &PyObjectRef, ) -> PyResult<()> { @@ -1010,7 +1009,7 @@ pub(crate) fn fire_exception_handled( pub(crate) fn fire_py_unwind( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, exception: &PyObjectRef, ) -> PyResult<()> { @@ -1026,7 +1025,7 @@ pub(crate) fn fire_py_unwind( pub(crate) fn fire_py_throw( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, exception: &PyObjectRef, ) -> PyResult<()> { @@ -1039,24 +1038,35 @@ pub(crate) fn fire_py_throw( ) } +/// If `value` is already a `StopIteration`, pass it directly; otherwise wrap +/// it in a new `StopIteration(value)` — matching `PyMonitoring_FireStopIterationEvent`. pub(crate) fn fire_stop_iteration( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, - exception: &PyObjectRef, + value: &PyObjectRef, ) -> PyResult<()> { + let exc: PyObjectRef = if value.fast_isinstance(vm.ctx.exceptions.stop_iteration) { + value.clone() + } else { + vm.ctx + .exceptions + .stop_iteration + .as_object() + .call(vec![value.clone()], vm)? + }; fire( vm, EVENT_STOP_ITERATION, code, offset, - &[vm.ctx.new_int(offset).into(), exception.clone()], + &[vm.ctx.new_int(offset).into(), exc], ) } pub(crate) fn fire_jump( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, destination: u32, ) -> PyResult<()> { @@ -1074,7 +1084,7 @@ pub(crate) fn fire_jump( pub(crate) fn fire_branch_left( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, destination: u32, ) -> PyResult<()> { @@ -1092,7 +1102,7 @@ pub(crate) fn fire_branch_left( pub(crate) fn fire_branch_right( vm: &VirtualMachine, - code: &PyRef, + code: &Py, offset: u32, destination: u32, ) -> PyResult<()> { diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 5c77afb4f5c..a5daa9cd2ff 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -18,19 +18,18 @@ mod decl { AsObject, Py, PyObjectRef, PyResult, VirtualMachine, builtins::{PyStrRef, PyTypeRef}, function::{Either, FuncArgs, OptionalArg}, - types::{PyStructSequence, struct_sequence_new}, + types::{PyStructSequence, PyStructSequenceData, struct_sequence_new}, }; #[cfg(any(unix, windows))] use crate::{ common::wtf8::Wtf8Buf, convert::{ToPyException, ToPyObject}, }; - #[cfg(not(any(unix, windows)))] - use chrono::{ - DateTime, Datelike, TimeZone, Timelike, - naive::{NaiveDate, NaiveDateTime, NaiveTime}, - }; use core::time::Duration; + #[cfg(not(any(unix, windows)))] + use jiff::{Timestamp, Zoned, civil::DateTime, tz::TimeZone}; + #[cfg(target_os = "wasi")] + use rustpython_host_env::time::ClockId; #[cfg(any(unix, windows))] use rustpython_host_env::time::asctime_from_tm; use rustpython_host_env::time::{self as host_time}; @@ -60,14 +59,27 @@ mod decl { #[pyattr] pub const _STRUCT_TM_ITEMS: usize = 11; - // TODO: implement proper monotonic time for wasm/wasi. - #[cfg(not(any(unix, windows)))] + #[cfg(target_os = "wasi")] + fn get_clock_time(id: ClockId, vm: &VirtualMachine) -> PyResult { + host_time::clock_gettime(id).map_err(|err| vm.new_os_error(err.to_string())) + } + + #[cfg(target_os = "wasi")] + fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { + get_clock_time(ClockId::CLOCK_MONOTONIC, vm) + } + + #[cfg(target_os = "wasi")] + fn get_perf_time(vm: &VirtualMachine) -> PyResult { + get_clock_time(ClockId::CLOCK_MONOTONIC, vm) + } + + #[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { duration_since_system_now(vm) } - // TODO: implement proper perf time for wasm/wasi. - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_perf_time(vm: &VirtualMachine) -> PyResult { duration_since_system_now(vm) } @@ -195,7 +207,7 @@ mod decl { Ok(get_perf_time(vm)?.as_nanos()) } - #[cfg(target_env = "msvc")] + #[cfg(windows)] #[cfg(not(target_arch = "wasm32"))] pub(super) fn get_tz_info() -> host_time::WindowsTimeZoneInfo { host_time::get_tz_info() @@ -210,8 +222,7 @@ mod decl { #[cfg(not(target_arch = "wasm32"))] #[pyattr] fn altzone(_vm: &VirtualMachine) -> core::ffi::c_long { - // TODO: RUSTPYTHON; Add support for using the C altzone - crate::host_env::time::tz::timezone() - 3600 + crate::host_env::time::tz::altzone() } #[cfg(target_env = "msvc")] @@ -275,10 +286,7 @@ mod decl { } #[cfg(not(any(unix, windows)))] - fn pyobj_to_date_time( - value: Either, - vm: &VirtualMachine, - ) -> PyResult> { + fn pyobj_to_timestamp(value: Either, vm: &VirtualMachine) -> PyResult { let secs = match value { Either::A(float) => { if !float.is_finite() { @@ -288,19 +296,19 @@ mod decl { } Either::B(int) => int, }; - DateTime::::from_timestamp(secs, 0) - .ok_or_else(|| vm.new_overflow_error("timestamp out of range for platform time_t")) + Timestamp::from_second(secs) + .map_err(|_| vm.new_overflow_error("timestamp out of range for platform time_t")) } #[cfg(not(any(unix, windows)))] impl OptionalArg>> { /// Construct a localtime from the optional seconds, or get the current local time. - fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { + fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { - Self::Present(Some(secs)) => pyobj_to_date_time(secs, vm)? - .with_timezone(&chrono::Local) - .naive_local(), - Self::Present(None) | Self::Missing => chrono::offset::Local::now().naive_local(), + Self::Present(Some(secs)) => { + pyobj_to_timestamp(secs, vm)?.to_zoned(TimeZone::system()) + } + Self::Present(None) | Self::Missing => Zoned::now(), }) } } @@ -418,10 +426,10 @@ mod decl { #[cfg(not(any(unix, windows)))] impl OptionalArg { - fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { + fn naive_or_local(self, vm: &VirtualMachine) -> PyResult { Ok(match self { Self::Present(t) => t.to_date_time(vm)?, - Self::Missing => chrono::offset::Local::now().naive_local(), + Self::Missing => Zoned::now().datetime(), }) } } @@ -442,9 +450,9 @@ mod decl { } _ => { let instant = match secs { - OptionalArg::Present(Some(secs)) => pyobj_to_date_time(secs, vm)?.naive_utc(), + OptionalArg::Present(Some(secs)) => pyobj_to_timestamp(secs, vm)?.to_zoned(TimeZone::UTC), OptionalArg::Present(None) | OptionalArg::Missing => { - chrono::offset::Utc::now().naive_utc() + Zoned::now().with_time_zone(TimeZone::UTC) } }; Ok(StructTimeData::new_utc(vm, instant)) @@ -467,7 +475,7 @@ mod decl { } _ => { let instant = secs.naive_or_local(vm)?; - Ok(StructTimeData::new_local(vm, instant, 0)) + StructTimeData::new_local(vm, instant.into(), 0) } } } @@ -488,11 +496,10 @@ mod decl { { let datetime = t.to_date_time(vm)?; // mktime interprets struct_time as local time - let local_dt = chrono::Local - .from_local_datetime(&datetime) - .single() - .ok_or_else(|| vm.new_overflow_error("mktime argument out of range"))?; - let seconds_since_epoch = local_dt.timestamp() as f64; + let local_dt = datetime + .to_zoned(TimeZone::system()) + .map_err(|_| vm.new_overflow_error("mktime argument out of range"))?; + let seconds_since_epoch = local_dt.timestamp().as_second() as f64; Ok(seconds_since_epoch) } } @@ -520,7 +527,7 @@ mod decl { #[cfg(not(any(unix, windows)))] { let instant = t.naive_or_local(vm)?; - let formatted_time = instant.format(CFMT).to_string(); + let formatted_time = instant.strftime(CFMT).to_string(); Ok(vm.ctx.new_str(formatted_time).into()) } } @@ -541,7 +548,7 @@ mod decl { #[cfg(not(any(unix, windows)))] { let instant = secs.naive_or_local(vm)?; - Ok(instant.format(CFMT).to_string()) + Ok(instant.strftime(CFMT).to_string()) } } @@ -571,8 +578,8 @@ mod decl { for codepoint in format.as_wtf8().code_points() { if codepoint.to_u32() == 0 { if !ascii.is_empty() { - let part = host_time::strftime_ascii(&ascii, &tm) - .map_err(|_| vm.new_value_error("embedded null character"))?; + let part = + host_time::strftime_ascii(&ascii, &tm).map_err(|e| e.to_pyexception(vm))?; out.extend(part.chars()); ascii.clear(); } @@ -587,22 +594,22 @@ mod decl { } if !ascii.is_empty() { - let part = host_time::strftime_ascii(&ascii, &tm) - .map_err(|_| vm.new_value_error("embedded null character"))?; + let part = + host_time::strftime_ascii(&ascii, &tm).map_err(|e| e.to_pyexception(vm))?; out.extend(part.chars()); ascii.clear(); } out.push(codepoint); } if !ascii.is_empty() { - let part = host_time::strftime_ascii(&ascii, &tm) - .map_err(|_| vm.new_value_error("embedded null character"))?; + let part = host_time::strftime_ascii(&ascii, &tm).map_err(|e| e.to_pyexception(vm))?; out.extend(part.chars()); } Ok(out.to_pyobject(vm)) } #[pyfunction] + #[cfg_attr(not(any(unix, windows)), expect(clippy::unnecessary_wraps,))] fn strftime(format: PyStrRef, t: OptionalArg, vm: &VirtualMachine) -> PyResult { #[cfg(any(unix, windows))] { @@ -632,7 +639,7 @@ mod decl { }; let mut formatted_time = String::new(); - write!(&mut formatted_time, "{}", instant.format(&fmt_lossy)) + write!(&mut formatted_time, "{}", instant.strftime(&*fmt_lossy)) .unwrap_or_else(|_| formatted_time = format.to_string()); Ok(vm.ctx.new_str(formatted_time).into()) } @@ -744,13 +751,7 @@ mod decl { impl StructTimeData { #[cfg(not(any(unix, windows)))] - fn new_inner( - vm: &VirtualMachine, - tm: NaiveDateTime, - isdst: i32, - gmtoff: i32, - zone: &str, - ) -> Self { + fn new_inner(vm: &VirtualMachine, tm: Zoned, isdst: i32) -> Self { Self { tm_year: vm.ctx.new_int(tm.year()).into(), tm_mon: vm.ctx.new_int(tm.month()).into(), @@ -758,46 +759,47 @@ mod decl { tm_hour: vm.ctx.new_int(tm.hour()).into(), tm_min: vm.ctx.new_int(tm.minute()).into(), tm_sec: vm.ctx.new_int(tm.second()).into(), - tm_wday: vm.ctx.new_int(tm.weekday().num_days_from_monday()).into(), - tm_yday: vm.ctx.new_int(tm.ordinal()).into(), + tm_wday: vm.ctx.new_int(tm.weekday().to_sunday_zero_offset()).into(), + tm_yday: vm.ctx.new_int(tm.day_of_year()).into(), tm_isdst: vm.ctx.new_int(isdst).into(), - tm_zone: vm.ctx.new_str(zone).into(), - tm_gmtoff: vm.ctx.new_int(gmtoff).into(), + tm_zone: vm.ctx.new_str(tm.strftime("%Z").to_string()).into(), + tm_gmtoff: vm.ctx.new_int(tm.offset().seconds()).into(), } } /// Create struct_time for UTC (gmtime) #[cfg(not(any(unix, windows)))] - fn new_utc(vm: &VirtualMachine, tm: NaiveDateTime) -> Self { - Self::new_inner(vm, tm, 0, 0, "UTC") + fn new_utc(vm: &VirtualMachine, tm: Zoned) -> Self { + Self::new_inner(vm, tm, 0) } /// Create struct_time for local timezone (localtime) #[cfg(not(any(unix, windows)))] - fn new_local(vm: &VirtualMachine, tm: NaiveDateTime, isdst: i32) -> Self { - let local_time = chrono::Local.from_local_datetime(&tm).unwrap(); - let offset_seconds = local_time.offset().local_minus_utc(); - let tz_abbr = local_time.format("%Z").to_string(); - Self::new_inner(vm, tm, isdst, offset_seconds, &tz_abbr) + fn new_local(vm: &VirtualMachine, tm: DateTime, isdst: i32) -> PyResult { + tm.to_zoned(TimeZone::system()) + .map(|tm| Self::new_inner(vm, tm, isdst)) + .map_err(|_| { + vm.new_overflow_error("timestamp is ambiguous for the system timezone") + }) } #[cfg(not(any(unix, windows)))] - fn to_date_time(&self, vm: &VirtualMachine) -> PyResult { - let invalid_overflow = || vm.new_overflow_error("mktime argument out of range"); - let invalid_value = || vm.new_value_error("invalid struct_time parameter"); - + fn to_date_time(&self, vm: &VirtualMachine) -> PyResult { macro_rules! field { ($field:ident) => { self.$field.clone().try_into_value(vm)? }; } - let dt = NaiveDateTime::new( - NaiveDate::from_ymd_opt(field!(tm_year), field!(tm_mon), field!(tm_mday)) - .ok_or_else(invalid_value)?, - NaiveTime::from_hms_opt(field!(tm_hour), field!(tm_min), field!(tm_sec)) - .ok_or_else(invalid_overflow)?, - ); - Ok(dt) + DateTime::new( + field!(tm_year), + field!(tm_mon), + field!(tm_mday), + field!(tm_hour), + field!(tm_min), + field!(tm_sec), + 0, + ) + .map_err(|_| vm.new_overflow_error("mktime argument out of range")) } } @@ -809,8 +811,12 @@ mod decl { impl PyStructTime { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let (seq, _dict): (PyObjectRef, OptionalArg) = args.bind(vm)?; - struct_sequence_new(cls, seq, vm) + struct_sequence_new( + cls, + args.bind(vm)?, + StructTimeData::OPTIONAL_FIELD_NAMES, + vm, + ) } } diff --git a/crates/vm/src/stdlib/typevar.rs b/crates/vm/src/stdlib/typevar.rs index da018b552e1..b784d8799f6 100644 --- a/crates/vm/src/stdlib/typevar.rs +++ b/crates/vm/src/stdlib/typevar.rs @@ -47,20 +47,17 @@ pub(crate) mod typevar { /// /// Note: CPython's implementation (in typevarobject.c) gets the module from the /// frame's function object using PyFunction_GetModule(f->f_funcobj). However, - /// RustPython's Frame doesn't store a reference to the function object, so we + /// RustPython's FrameObject doesn't store a reference to the function object, so we /// get the module name from the frame's globals dictionary instead. fn caller(vm: &VirtualMachine) -> Option { - let frame = vm.current_frame()?; - - // In RustPython, we get the module name from frame's globals - // This is similar to CPython's sys._getframe().f_globals.get('__name__') - frame.globals.get_item("__name__", vm).ok() + let globals = crate::frame::current_globals()?; + globals.get_item("__name__", vm).ok() } /// Set __module__ attribute for an object based on the caller's module. /// This follows CPython's behavior for TypeVar and similar objects. fn set_module_from_caller(obj: &PyObject, vm: &VirtualMachine) -> PyResult<()> { - // Note: CPython gets module from frame->f_funcobj, but RustPython's Frame + // Note: CPython gets module from frame->f_funcobj, but RustPython's FrameObject // architecture is different - we use globals['__name__'] instead let module_value: PyObjectRef = if let Some(module_name) = caller(vm) { // Special handling for certain module names @@ -926,11 +923,12 @@ pub(crate) mod typevar { impl Representable for ParamSpecArgs { #[inline(always)] fn repr_str(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { - // Check if origin is a ParamSpec - if let Ok(name) = zelf.__origin__.get_attr("__name__", vm) { - return Ok(format!("{name}.args", name = name.str(vm)?)); + // A ParamSpec origin is named; anything else is shown by its repr, + // which carries the recursion guard a Rust `{:?}` walk does not. + if let Some(param_spec) = zelf.__origin__.downcast_ref::() { + return Ok(format!("{}.args", param_spec.__name__().str_utf8(vm)?)); } - Ok(format!("{:?}.args", zelf.__origin__)) + Ok(format!("{}.args", zelf.__origin__.repr(vm)?)) } } @@ -989,11 +987,12 @@ pub(crate) mod typevar { impl Representable for ParamSpecKwargs { #[inline(always)] fn repr_str(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { - // Check if origin is a ParamSpec - if let Ok(name) = zelf.__origin__.get_attr("__name__", vm) { - return Ok(format!("{name}.kwargs", name = name.str(vm)?)); + // A ParamSpec origin is named; anything else is shown by its repr, + // which carries the recursion guard a Rust `{:?}` walk does not. + if let Some(param_spec) = zelf.__origin__.downcast_ref::() { + return Ok(format!("{}.kwargs", param_spec.__name__().str_utf8(vm)?)); } - Ok(format!("{:?}.kwargs", zelf.__origin__)) + Ok(format!("{}.kwargs", zelf.__origin__.repr(vm)?)) } } diff --git a/crates/vm/src/stdlib/winsound.rs b/crates/vm/src/stdlib/winsound.rs index 67d7c8a7ffe..091a3f801aa 100644 --- a/crates/vm/src/stdlib/winsound.rs +++ b/crates/vm/src/stdlib/winsound.rs @@ -6,9 +6,10 @@ pub(crate) use winsound::module_def; #[pymodule] mod winsound { use crate::builtins::{PyBaseExceptionRef, PyBytes, PyStr}; - use crate::convert::{IntoPyException, TryFromBorrowedObject}; + use crate::convert::{IntoPyException, ToPyException}; + use crate::exceptions; use crate::host_env::windows::ToWideString; - use crate::protocol::PyBuffer; + use crate::protocol::{BufferFlags, PyBuffer}; use crate::{AsObject, PyObjectRef, PyResult, VirtualMachine}; use rustpython_host_env::winsound::{PlaySoundError, PlaySoundSource, play_sound}; @@ -89,7 +90,7 @@ mod winsound { } if flags & SND_MEMORY != 0 { - let buffer = PyBuffer::try_from_borrowed_object(vm, &sound)?; + let buffer = PyBuffer::from_object(vm, &sound, BufferFlags::SIMPLE)?; let buf = buffer .as_contiguous() .ok_or_else(|| vm.new_type_error("a bytes-like object is required, not 'str'"))?; @@ -142,12 +143,12 @@ mod winsound { // Check for embedded null characters if path.as_bytes().contains(&0) { - return Err(vm.new_value_error("embedded null character")); + return Err(exceptions::nul_char_error(vm)); } let wide = path.to_wide_with_nul(); - let wide_cstr = widestring::WideCStr::from_slice_truncate(&wide) - .map_err(|_| vm.new_value_error("embedded null character"))?; + let wide_cstr = + widestring::WideCStr::from_slice_truncate(&wide).map_err(|e| e.to_pyexception(vm))?; play_sound(PlaySoundSource::Name(wide_cstr), flags).map_err(map_play_err(vm)) } diff --git a/crates/vm/src/suggestion.rs b/crates/vm/src/suggestion.rs index b48b78af755..57323ebeac9 100644 --- a/crates/vm/src/suggestion.rs +++ b/crates/vm/src/suggestion.rs @@ -1,13 +1,14 @@ //! This module provides functionality to suggest similar names for attributes or variables. //! This is used during tracebacks. +use core::iter::ExactSizeIterator; + use crate::{ AsObject, Py, PyObject, PyObjectRef, VirtualMachine, builtins::{PyStr, PyStrRef}, exceptions::types::PyBaseException, sliceable::SliceableSequenceOp, }; -use core::iter::ExactSizeIterator; use rustpython_common::str::levenshtein::{MOVE_COST, levenshtein_distance}; const MAX_CANDIDATE_ITEMS: usize = 750; @@ -70,17 +71,23 @@ pub fn offer_suggestions(exc: &Py, vm: &VirtualMachine) -> Opti let tb = exc.__traceback__()?; let tb = tb.iter().last().unwrap_or(tb); - let varnames = tb.frame.code.clone().co_varnames(vm); + let varnames = tb.frame.iframe().code().to_owned().co_varnames(vm); if let Some(suggestions) = calculate_suggestions(varnames.iter(), &name) { return Some(suggestions); }; - let globals: Vec<_> = tb.frame.globals.as_object().try_to_value(vm).ok()?; + let globals: Vec<_> = tb + .frame + .iframe() + .globals() + .as_object() + .try_to_value(vm) + .ok()?; if let Some(suggestions) = calculate_suggestions(globals.iter(), &name) { return Some(suggestions); }; - let builtins: Vec<_> = tb.frame.builtins.try_to_value(vm).ok()?; + let builtins: Vec<_> = tb.frame.iframe().builtins().try_to_value(vm).ok()?; calculate_suggestions(builtins.iter(), &name) } else if exc.class().fast_issubclass(vm.ctx.exceptions.import_error) { let mod_name = exc.as_object().get_attr("name", vm).ok()?; diff --git a/crates/vm/src/types/mod.rs b/crates/vm/src/types/mod.rs index b17a737545f..11c3a4dc51e 100644 --- a/crates/vm/src/types/mod.rs +++ b/crates/vm/src/types/mod.rs @@ -5,5 +5,7 @@ mod zoo; pub use slot::*; pub use slot_defs::{SLOT_DEFS, SlotAccessor, SlotDef}; -pub use structseq::{PyStructSequence, PyStructSequenceData, struct_sequence_new}; +pub use structseq::{ + PyStructSequence, PyStructSequenceData, StructSequenceNewArgs, struct_sequence_new, +}; pub(crate) use zoo::TypeZoo; diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index a56d1c493f9..c0b9142c780 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -9,7 +9,7 @@ use crate::{ convert::ToPyObject, function::{Either, FromArgs, FuncArgs, PyComparisonValue, PyMethodDef, PySetterValue}, protocol::{ - PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PyMappingSlots, PyNumber, + BufferFlags, PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PyMappingSlots, PyNumber, PyNumberMethods, PyNumberSlots, PySequence, PySequenceMethods, PySequenceSlots, }, types::slot_defs::{SlotAccessor, find_slot_defs_by_name}, @@ -149,7 +149,12 @@ pub struct PyTypeSlots { pub setattro: AtomicCell>, // Functions to access object as input/output buffer - pub as_buffer: Option, + pub as_buffer: AtomicCell>, + /// bf_releasebuffer: releasing an export of this type is observable, so the + /// type exposes `__release_buffer__`. + pub has_release_buffer: AtomicCell, + /// True when a Python-level `__release_buffer__` must be invoked on release. + pub python_release_buffer: AtomicCell, // Assigned meaning in release 2.1 // rich comparisons @@ -202,10 +207,13 @@ impl PyTypeSlots { #[must_use] pub fn heap_default() -> Self { + /* Self { - // init: AtomicCell::new(Some(init_wrapper)), + init: AtomicCell::new(Some(init_wrapper)), ..Default::default() } + */ + Self::default() } } @@ -293,7 +301,8 @@ pub(crate) type StringifyFunc = fn(&PyObject, &VirtualMachine) -> PyResult, &VirtualMachine) -> PyResult; pub(crate) type SetattroFunc = fn(&PyObject, &Py, PySetterValue, &VirtualMachine) -> PyResult<()>; -pub(crate) type AsBufferFunc = fn(&PyObject, &VirtualMachine) -> PyResult; +/// bf_getbuffer +pub(crate) type AsBufferFunc = fn(&PyObject, BufferFlags, &VirtualMachine) -> PyResult; pub(crate) type RichCompareFunc = fn( &PyObject, &PyObject, @@ -326,6 +335,15 @@ pub(crate) type MapSubscriptFunc = fn(PyMapping<'_>, &PyObject, &VirtualMachine) pub(crate) type MapAssSubscriptFunc = fn(PyMapping<'_>, &PyObject, Option, &VirtualMachine) -> PyResult<()>; +// slot_bf_getbuffer +pub(crate) fn python_as_buffer( + obj: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, +) -> PyResult { + crate::builtins::memory::buffer_from_python_getbuffer(obj, flags, vm) +} + // slot_sq_length pub(crate) fn len_wrapper(obj: &PyObject, vm: &VirtualMachine) -> PyResult { let ret = vm.call_special_method(obj, identifier!(vm, __len__), ())?; @@ -509,7 +527,11 @@ pub fn hash_not_implemented(zelf: &PyObject, vm: &VirtualMachine) -> PyResult PyResult { - vm.call_special_method(zelf, identifier!(vm, __call__), args) + // `__call__` can name the object being called, and dispatching it pushes no + // Python frame, so nothing else counts the nesting. + vm.with_recursion("while calling a Python object", || { + vm.call_special_method(zelf, identifier!(vm, __call__), args) + }) } fn getattro_wrapper(zelf: &PyObject, name: &Py, vm: &VirtualMachine) -> PyResult { @@ -598,7 +620,11 @@ fn descr_get_wrapper( cls: Option, vm: &VirtualMachine, ) -> PyResult { - vm.call_special_method(&zelf, identifier!(vm, __get__), (obj, cls)) + // A descriptor whose `__get__` is the descriptor itself resolves it by + // fetching `__get__` again, and none of that pushes a Python frame. + vm.with_recursion("while calling a Python object", || { + vm.call_special_method(&zelf, identifier!(vm, __get__), (obj, cls)) + }) } fn descr_set_wrapper( @@ -663,7 +689,7 @@ impl PyType { // NOTE: Collect into Vec first to avoid issues during iteration let defs: Vec<_> = find_slot_defs_by_name(name.as_str()).collect(); for def in defs { - self.update_one_slot::(&def.accessor, name, ctx); + self.update_one_slot::(def.accessor, name, ctx); } // Recursively update subclasses that don't have their own definition @@ -689,7 +715,7 @@ impl PyType { // Update subclass's slots for def in find_slot_defs_by_name(name.as_str()) { - subclass.update_one_slot::(&def.accessor, name, ctx); + subclass.update_one_slot::(def.accessor, name, ctx); } // Recurse into subclass's subclasses @@ -700,7 +726,7 @@ impl PyType { /// Update a single slot fn update_one_slot( &self, - accessor: &SlotAccessor, + accessor: SlotAccessor, name: &'static PyStrInterned, ctx: &Context, ) { @@ -736,6 +762,18 @@ impl PyType { // Helper macro for number/sequence/mapping sub-slots macro_rules! update_sub_slot { ($group:ident, $slot:ident, $wrapper:expr, $variant:ident) => {{ + // Fall back to the value inherited for this exact field. Left and + // right binary ops (e.g. add / right_add) share one accessor but + // occupy distinct fields, so the fallback must target this field + // rather than the accessor's default field, otherwise resolving + // an absent right op would overwrite the left op's dispatcher. + let inherit_this_field = || { + let mro = self.mro.read(); + let inherited = mro[1..] + .iter() + .find_map(|cls| cls.slots.$group.$slot.load()); + self.slots.$group.$slot.store(inherited); + }; if ADD { // Check if this type defines any method that maps to this slot. // Some slots like SqAssItem/MpAssSubscript are shared by multiple @@ -757,8 +795,15 @@ impl PyType { } result }; + // Reify the wrapper at a single site so the own and inherited + // branches store the same fn item. binary_op1 compares slot + // fn addresses to decide whether a subclass overrides the op; + // duplicating the wrapper closure across branches yields + // distinct addresses in unmerged debug builds and breaks that + // comparison for an inherited slot. + let store_wrapper = || self.slots.$group.$slot.store(Some($wrapper)); if has_own { - self.slots.$group.$slot.store(Some($wrapper)); + store_wrapper(); } else { match self.lookup_slot_in_mro(name, ctx, |sf| { if let SlotFunc::$variant(f) = sf { @@ -771,15 +816,15 @@ impl PyType { self.slots.$group.$slot.store(Some(func)); } SlotLookupResult::PythonMethod => { - self.slots.$group.$slot.store(Some($wrapper)); + store_wrapper(); } SlotLookupResult::NotFound => { - accessor.inherit_from_mro(self); + inherit_this_field(); } } } } else { - accessor.inherit_from_mro(self); + inherit_this_field(); } }}; } @@ -840,12 +885,31 @@ impl PyType { } } SlotAccessor::TpNew => { - // __new__ is not wrapped via PyWrapper - if ADD { + // __new__ is a staticmethod, not a PyWrapper descriptor, so + // lookup_slot_in_mro cannot classify it. Resolve __new__ + // through the MRO dicts instead: a Python-level definition + // needs the dynamic new_wrapper, while a native type's + // builtin __new__ entry (or no entry at all) means the slot + // is inherited from the solid base, matching update_one_slot's + // tp_new special case over the tp_base-inherited value. + let needs_wrapper = if ADD && self.attributes.read().contains_key(name) { + true + } else { + // mro[0] is self, so skip it + self.mro.read()[1..] + .iter() + .find(|cls| cls.attributes.read().contains_key(name)) + .is_some_and(|cls| { + cls.slots.new.load().map(|f| fn_addr(f)) + == Some(fn_addr(new_wrapper as NewFunc)) + }) + }; + if needs_wrapper { self.slots.new.store(Some(new_wrapper)); self.slots.vectorcall.store(None); } else { - accessor.inherit_from_mro(self); + let inherited = self.base.deref().and_then(|base| base.slots.new.load()); + self.slots.new.store(inherited); } } SlotAccessor::TpDel => update_main_slot!(del, del_wrapper, Del), @@ -894,46 +958,72 @@ impl PyType { } } SlotAccessor::TpSetattro => { - // __setattr__ and __delattr__ share the same slot - if ADD { - match self.lookup_slot_in_mro(name, ctx, |sf| match sf { - SlotFunc::SetAttro(f) | SlotFunc::DelAttro(f) => Some(*f), - _ => None, - }) { - SlotLookupResult::NativeSlot(func) => { - self.slots.setattro.store(Some(func)); - } - SlotLookupResult::PythonMethod => { - self.slots.setattro.store(Some(setattro_wrapper)); - } - SlotLookupResult::NotFound => { - accessor.inherit_from_mro(self); - } + // __setattr__ and __delattr__ share the same slot, so both + // names must be resolved together: a Python-level override of + // either one forces the dispatching wrapper, and the native + // slot is only usable when every resolved name agrees on it. + // This resolution reads the current attribute dicts, so it + // applies the same whether a name was just added or removed. + let extract = |sf: &SlotFunc| match sf { + SlotFunc::SetAttro(f) | SlotFunc::DelAttro(f) => Some(*f), + _ => None, + }; + let setattr = self.lookup_slot_in_mro(identifier!(ctx, __setattr__), ctx, extract); + let delattr = self.lookup_slot_in_mro(identifier!(ctx, __delattr__), ctx, extract); + use SlotLookupResult::{NativeSlot, NotFound, PythonMethod}; + match (setattr, delattr) { + (PythonMethod, _) | (_, PythonMethod) => { + self.slots.setattro.store(Some(setattro_wrapper)); + } + (NativeSlot(set), NativeSlot(del)) => { + let func = if fn_addr(set) == fn_addr(del) { + set + } else { + setattro_wrapper + }; + self.slots.setattro.store(Some(func)); + } + (NativeSlot(func), NotFound) | (NotFound, NativeSlot(func)) => { + self.slots.setattro.store(Some(func)); + } + (NotFound, NotFound) => { + accessor.inherit_from_mro(self); } - } else { - accessor.inherit_from_mro(self); } } SlotAccessor::TpDescrGet => update_main_slot!(descr_get, descr_get_wrapper, DescrGet), SlotAccessor::TpDescrSet => { - // __set__ and __delete__ share the same slot - if ADD { - match self.lookup_slot_in_mro(name, ctx, |sf| match sf { - SlotFunc::DescrSet(f) | SlotFunc::DescrDel(f) => Some(*f), - _ => None, - }) { - SlotLookupResult::NativeSlot(func) => { - self.slots.descr_set.store(Some(func)); - } - SlotLookupResult::PythonMethod => { - self.slots.descr_set.store(Some(descr_set_wrapper)); - } - SlotLookupResult::NotFound => { - accessor.inherit_from_mro(self); - } + // __set__ and __delete__ share the same slot, so both names + // must be resolved together: a Python-level definition of + // either one forces the dispatching wrapper, and the native + // slot is only usable when every resolved name agrees on it. + // This resolution reads the current attribute dicts, so it + // applies the same whether a name was just added or removed. + let extract = |sf: &SlotFunc| match sf { + SlotFunc::DescrSet(f) | SlotFunc::DescrDel(f) => Some(*f), + _ => None, + }; + let set = self.lookup_slot_in_mro(identifier!(ctx, __set__), ctx, extract); + let delete = self.lookup_slot_in_mro(identifier!(ctx, __delete__), ctx, extract); + use SlotLookupResult::{NativeSlot, NotFound, PythonMethod}; + match (set, delete) { + (PythonMethod, _) | (_, PythonMethod) => { + self.slots.descr_set.store(Some(descr_set_wrapper)); + } + (NativeSlot(set), NativeSlot(delete)) => { + let func = if fn_addr(set) == fn_addr(delete) { + set + } else { + descr_set_wrapper + }; + self.slots.descr_set.store(Some(func)); + } + (NativeSlot(func), NotFound) | (NotFound, NativeSlot(func)) => { + self.slots.descr_set.store(Some(func)); + } + (NotFound, NotFound) => { + accessor.inherit_from_mro(self); } - } else { - accessor.inherit_from_mro(self); } } @@ -1512,6 +1602,58 @@ impl PyType { } } + // === Buffer protocol === + SlotAccessor::BfGetBuffer => { + if ADD { + match self.lookup_slot_in_mro(name, ctx, |sf| { + if let SlotFunc::GetBuffer(f) = sf { + Some(*f) + } else { + None + } + }) { + SlotLookupResult::NativeSlot(func) => { + self.slots.as_buffer.store(Some(func)); + } + SlotLookupResult::PythonMethod => { + self.slots.as_buffer.store(Some(python_as_buffer)); + } + SlotLookupResult::NotFound => { + accessor.inherit_from_mro(self); + } + } + } else { + accessor.inherit_from_mro(self); + } + } + SlotAccessor::BfReleaseBuffer => { + // Which of the two implementations `__release_buffer__` resolves to + // decides whether buffer release has to call back into Python. + if ADD { + match self.lookup_slot_in_mro(name, ctx, |sf| { + if matches!(sf, SlotFunc::ReleaseBuffer) { + Some(()) + } else { + None + } + }) { + SlotLookupResult::NativeSlot(()) => { + self.slots.python_release_buffer.store(false); + self.slots.has_release_buffer.store(true); + } + SlotLookupResult::PythonMethod => { + self.slots.python_release_buffer.store(true); + self.slots.has_release_buffer.store(true); + } + SlotLookupResult::NotFound => { + accessor.inherit_from_mro(self); + } + } + } else { + accessor.inherit_from_mro(self); + } + } + // Reserved slots - no-op _ => {} } @@ -1664,11 +1806,10 @@ pub trait Initializer: PyPayload { .matches(&class_name_for_debug as &str) .count() == 2; - if double_appearance { - panic!( - "This type `{class_name_for_debug}` doesn't seem to support `init`. Override `slot_init` instead: {msg}" - ); - } + assert!( + !double_appearance, + "This type `{class_name_for_debug}` doesn't seem to support `init`. Override `slot_init` instead: {msg}" + ) } } return Err(err); @@ -1929,6 +2070,29 @@ impl PyComparisonOp { self.map_eq(|| a.borrow().is(b.borrow())) } + /// The answer to this comparison for two operands that `equal` reports as + /// equal or not, or `None` for an ordering operator, which equality alone + /// cannot settle -- `equal` is not called in that case. + /// + /// This is what lets a type answer `==` and `!=` with an equality test + /// rather than with an ordering: the two agree on the answer, but equality + /// can settle a length mismatch without looking at the contents at all. + /// + /// The two neighbouring helpers answer different questions: [`Self::map_eq`] + /// answers only where its predicate holds, so a caller still handles the + /// other side, and [`Self::eq_only`] declares the comparison + /// `NotImplemented` for an ordering operator. This one leaves the ordering + /// operators to the caller, which is what a type with a real ordering + /// needs. + #[inline] + pub fn eval_eq(self, equal: impl FnOnce() -> bool) -> Option { + match self { + Self::Eq => Some(equal()), + Self::Ne => Some(!equal()), + _ => None, + } + } + /// Returns `Some(true)` when self is `Eq` and `f()` returns true. Returns `Some(false)` when self /// is `Ne` and `f()` returns true. Otherwise returns `None`. #[inline] @@ -1981,14 +2145,29 @@ pub trait SetAttr: PyPayload { #[pyclass] pub trait AsBuffer: PyPayload { - // TODO: `flags` parameter + /// bf_releasebuffer: set when releasing an export of this type is observable, + /// i.e. the exporter counts exports. Such types expose `__release_buffer__`. + const RELEASE_BUFFER: bool = false; + #[inline] #[pyslot] - fn slot_as_buffer(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { + fn slot_as_buffer( + zelf: &PyObject, + flags: BufferFlags, + vm: &VirtualMachine, + ) -> PyResult { let zelf = zelf .downcast_ref() .ok_or_else(|| vm.new_type_error("unexpected payload for as_buffer"))?; - Self::as_buffer(zelf, vm) + let buffer = Self::as_buffer(zelf, vm)?; + if let Err(exc) = flags.check_writable(buffer.desc.readonly, "Object is not writable.", vm) + { + // An acquisition that cannot be served never happened, so the + // exporter's release is undone without running the Python hook. + buffer.abort_acquisition(); + return Err(exc); + } + Ok(buffer) } fn as_buffer(zelf: &Py, vm: &VirtualMachine) -> PyResult; @@ -2105,3 +2284,24 @@ where debug_assert!(prev.is_some()); // slot_iter would be set } } + +/// Extract the raw address of a function pointer as `usize` without +/// triggering miri's "pointer not dereferenceable" UB. +/// +/// The standard `fn_ptr as usize` cast goes through `FnPtr::addr()` +/// which attempts to dereference the function pointer's provenance — +/// miri considers this UB for function items. `transmute_copy` bypasses +/// that path and reads the address as plain integer bytes. +/// +/// The result is suitable for identity comparison only: two function +/// pointers with the same address are the same function. The converse +/// is not always guaranteed (the compiler may merge identical function +/// bodies), but this matches CPython's slot comparison semantics. +#[inline(always)] +pub(crate) fn fn_addr(f: T) -> usize { + assert!( + core::mem::size_of::() == core::mem::size_of::(), + "fn_addr: T must be pointer-sized" + ); + unsafe { core::mem::transmute_copy::(&f) } +} diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index 8637f811272..300ee319907 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -2,7 +2,7 @@ //! //! This module provides a centralized array of all slot definitions, -use super::{PyComparisonOp, PyTypeSlots}; +use super::{PyComparisonOp, PyTypeSlots, fn_addr}; use crate::builtins::descriptor::SlotFunc; /// Slot operation type @@ -71,7 +71,7 @@ pub struct SlotDef { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum SlotAccessor { - // Buffer protocol (1-2) - Reserved, not used in RustPython + // Buffer protocol (1-2) BfGetBuffer = 1, BfReleaseBuffer = 2, @@ -173,9 +173,7 @@ impl SlotAccessor { pub fn is_reserved(&self) -> bool { matches!( self, - Self::BfGetBuffer - | Self::BfReleaseBuffer - | Self::TpAlloc + Self::TpAlloc | Self::TpBase | Self::TpBases | Self::TpClear @@ -411,6 +409,10 @@ impl SlotAccessor { ) } + // Buffer protocol + Self::BfGetBuffer => matches!(slot_func, SlotFunc::GetBuffer(_)), + Self::BfReleaseBuffer => matches!(slot_func, SlotFunc::ReleaseBuffer), + // New and reserved slots Self::TpNew => false, _ => false, // Reserved slots @@ -539,6 +541,18 @@ impl SlotAccessor { Self::MpSubscript => inherit_mapping!(subscript), Self::MpAssSubscript => inherit_mapping!(ass_subscript), + // Buffer protocol + Self::BfGetBuffer => { + let inherited = mro.iter().find_map(|cls| cls.slots.as_buffer.load()); + typ.slots.as_buffer.store(inherited); + } + Self::BfReleaseBuffer => { + let has_release = mro.iter().any(|cls| cls.slots.has_release_buffer.load()); + typ.slots.has_release_buffer.store(has_release); + let py_release = mro.iter().any(|cls| cls.slots.python_release_buffer.load()); + typ.slots.python_release_buffer.store(py_release); + } + // Reserved slots - no-op _ => {} } @@ -608,8 +622,8 @@ impl SlotAccessor { if typ.slots.init.load().is_none() && let Some(base_val) = base.slots.init.load() { - let slot_defined = base.base.as_ref().is_none_or(|bb| { - bb.slots.init.load().map(|v| v as usize) != Some(base_val as usize) + let slot_defined = base.base.deref().is_none_or(|bb| { + bb.slots.init.load().map(|v| fn_addr(v)) != Some(fn_addr(base_val)) }); if slot_defined { typ.slots.init.store(Some(base_val)); @@ -677,6 +691,25 @@ impl SlotAccessor { Self::MpSubscript => copy_mapping!(subscript), Self::MpAssSubscript => copy_mapping!(ass_subscript), + // Buffer protocol + Self::BfGetBuffer => { + if typ.slots.as_buffer.load().is_none() + && let Some(base_val) = base.slots.as_buffer.load() + { + typ.slots.as_buffer.store(Some(base_val)); + } + } + Self::BfReleaseBuffer => { + if !typ.slots.has_release_buffer.load() && base.slots.has_release_buffer.load() { + typ.slots.has_release_buffer.store(true); + } + if !typ.slots.python_release_buffer.load() + && base.slots.python_release_buffer.load() + { + typ.slots.python_release_buffer.store(true); + } + } + // Reserved slots - no-op _ => {} } @@ -816,6 +849,16 @@ impl SlotAccessor { .load() .map(SlotFunc::MapSetSubscript), + // Buffer protocol + Self::BfGetBuffer => slots.as_buffer.load().map(SlotFunc::GetBuffer), + Self::BfReleaseBuffer => { + if slots.has_release_buffer.load() || slots.python_release_buffer.load() { + Some(SlotFunc::ReleaseBuffer) + } else { + None + } + } + // Reserved slots _ => None, } @@ -973,6 +1016,19 @@ pub const SLOT_DEFS_COUNT: usize = SLOT_DEFS.len(); /// All slot definitions pub static SLOT_DEFS: &[SlotDef] = &[ + // Buffer protocol (bf_*) + SlotDef { + name: "__buffer__", + accessor: SlotAccessor::BfGetBuffer, + op: None, + doc: "Return a buffer object that exposes the underlying memory of the object.", + }, + SlotDef { + name: "__release_buffer__", + accessor: SlotAccessor::BfReleaseBuffer, + op: None, + doc: "Release the buffer object that exposes the underlying memory of the object.", + }, // Type slots (tp_*) SlotDef { name: "__init__", diff --git a/crates/vm/src/types/structseq.rs b/crates/vm/src/types/structseq.rs index b7468d4a702..7f8099e7efb 100644 --- a/crates/vm/src/types/structseq.rs +++ b/crates/vm/src/types/structseq.rs @@ -1,9 +1,12 @@ use crate::common::lock::LazyLock; +use crate::common::wtf8::Wtf8; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, atomic_func, - builtins::{PyBaseExceptionRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef}, + builtins::{ + PyBaseExceptionRef, PyDict, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, + }, class::{PyClassImpl, StaticType}, - function::{Either, FuncArgs, PyComparisonValue, PyMethodDef, PyMethodFlags}, + function::{Either, FuncArgs, OptionalArg, PyComparisonValue, PyMethodDef, PyMethodFlags}, iter::PyExactSizeIterator, protocol::{PyMappingMethods, PySequenceMethods}, sliceable::{SequenceIndex, SliceableSequenceOp}, @@ -20,12 +23,35 @@ const DEFAULT_STRUCTSEQ_REDUCE: PyMethodDef = PyMethodDef::new_const( None, ); +/// The arguments every struct sequence constructor takes. +#[derive(FromArgs)] +pub struct StructSequenceNewArgs { + #[pyarg(any)] + pub sequence: PyObjectRef, + #[pyarg(any, optional)] + pub dict: OptionalArg, +} + /// Create a new struct sequence instance from a sequence. /// +/// `dict` supplies the hidden fields — the ones past `n_sequence_fields`, named +/// by `hidden_field_names` in order — that the sequence itself did not cover. It +/// may not name a field the sequence already supplied, nor one that does not +/// exist. +/// /// The class must have `n_sequence_fields` and `n_fields` attributes set /// (done automatically by `PyStructSequence::extend_pyclass`). -pub fn struct_sequence_new(cls: PyTypeRef, seq: PyObjectRef, vm: &VirtualMachine) -> PyResult { +pub fn struct_sequence_new( + cls: PyTypeRef, + args: StructSequenceNewArgs, + hidden_field_names: &[&str], + vm: &VirtualMachine, +) -> PyResult { // = structseq_new + let StructSequenceNewArgs { + sequence: seq, + dict, + } = args; #[cold] fn length_error( @@ -59,6 +85,16 @@ pub fn struct_sequence_new(cls: PyTypeRef, seq: PyObjectRef, vm: &VirtualMachine .ok_or_else(|| vm.new_type_error("missing n_fields attribute"))? .try_into_value(vm)?; + let dict = match dict { + OptionalArg::Missing => None, + OptionalArg::Present(dict) => Some(dict.downcast::().map_err(|_| { + vm.new_type_error(format!( + "{}() takes a dict as second arg, if any", + cls.slot_name() + )) + })?), + }; + let seq: Vec = seq.try_into_value(vm)?; let len = seq.len(); @@ -66,10 +102,30 @@ pub fn struct_sequence_new(cls: PyTypeRef, seq: PyObjectRef, vm: &VirtualMachine return Err(length_error(&cls.slot_name(), min_len, max_len, len, vm)); } - // Copy items and pad with None + // Copy items and pad the hidden fields the sequence did not cover with None. let mut items = seq; items.resize_with(max_len, || vm.ctx.none()); + // Fill those padded slots from `dict`. Every key has to land in one of them: + // a key naming a field the sequence already supplied, or no field at all, + // would otherwise be silently dropped. + if let Some(dict) = dict.filter(|dict| !dict.is_empty()) { + let mut found = 0; + let names = hidden_field_names.get(len - min_len..).unwrap_or(&[]); + for (item, name) in items[len..].iter_mut().zip(names) { + if let Some(value) = dict.get_item_opt(*name, vm)? { + *item = value; + found += 1; + } + } + if found != dict.__len__() { + return Err(vm.new_type_error(format!( + "{}() got duplicate or unexpected field name(s)", + cls.slot_name() + ))); + } + } + PyTuple::new_unchecked(items.into_boxed_slice()) .into_ref_with_type(vm, cls) .map(Into::into) @@ -192,6 +248,11 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { /// The Data struct that provides field definitions. type Data: PyStructSequenceData; + #[pyslot] + fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + struct_sequence_new(cls, args.bind(vm)?, Self::Data::OPTIONAL_FIELD_NAMES, vm) + } + /// Convert a Data struct into a PyStructSequence instance. fn from_data(data: Self::Data, vm: &VirtualMachine) -> PyTupleRef { let tuple = @@ -276,7 +337,7 @@ pub trait PyStructSequence: StaticType + PyClassImpl + Sized + 'static { // Check for unexpected keyword arguments if !kwargs.is_empty() { - let names: Vec<&str> = kwargs.keys().map(|k| k.as_str()).collect(); + let names: Vec<&Wtf8> = kwargs.keys().map(|k| k.as_ref()).collect(); return Err(vm.new_type_error(format!("Got unexpected field name(s): {names:?}"))); } diff --git a/crates/vm/src/types/zoo.rs b/crates/vm/src/types/zoo.rs index 13d439345f7..c8667050bf0 100644 --- a/crates/vm/src/types/zoo.rs +++ b/crates/vm/src/types/zoo.rs @@ -2,10 +2,10 @@ use crate::{ Py, builtins::{ asyncgenerator, bool_, builtin_func, bytearray, bytes, capsule, classmethod, code, complex, - coroutine, descriptor, dict, enumerate, filter, float, frame, function, generator, - genericalias, getset, int, interpolation, iter, list, map, mappingproxy, memory, module, - namespace, object, property, pystr, range, set, singletons, slice, staticmethod, super_, - template, traceback, tuple, + coroutine, descriptor, dict, enumerate, filter, float, frame, frame_locals_proxy, function, + generator, genericalias, getset, int, interpolation, iter, list, map, mappingproxy, memory, + module, namespace, object, property, pystr, range, set, singletons, slice, staticmethod, + super_, template, traceback, tuple, type_::{self, PyType}, union_, weakproxy, weakref, zip, }, @@ -39,6 +39,7 @@ pub struct TypeZoo { pub filter_type: &'static Py, pub float_type: &'static Py, pub frame_type: &'static Py, + pub frame_locals_proxy_type: &'static Py, pub frozenset_type: &'static Py, pub generator_type: &'static Py, pub int_type: &'static Py, @@ -177,7 +178,8 @@ impl TypeZoo { dict_itemiterator_type: dict::PyDictItemIterator::init_builtin_type(), dict_reverseitemiterator_type: dict::PyDictReverseItemIterator::init_builtin_type(), ellipsis_type: slice::PyEllipsis::init_builtin_type(), - frame_type: crate::frame::Frame::init_builtin_type(), + frame_type: crate::frame::FrameObject::init_builtin_type(), + frame_locals_proxy_type: frame_locals_proxy::FrameLocalsProxy::init_builtin_type(), function_type: function::PyFunction::init_builtin_type(), generator_type: generator::PyGenerator::init_builtin_type(), getset_type: getset::PyGetSet::init_builtin_type(), @@ -253,6 +255,7 @@ impl TypeZoo { bool_::init(context); code::init(context); frame::init(context); + frame_locals_proxy::init(context); weakref::init(context); weakproxy::init(context); singletons::init(context); diff --git a/crates/vm/src/utils.rs b/crates/vm/src/utils.rs index 51e27123fc8..8a28a32f663 100644 --- a/crates/vm/src/utils.rs +++ b/crates/vm/src/utils.rs @@ -1,12 +1,9 @@ -use core::fmt; - use rustpython_common::wtf8::{Wtf8, Wtf8Buf}; use crate::{ PyObjectRef, PyResult, VirtualMachine, builtins::{PyStr, PyUtf8Str}, convert::{ToPyException, ToPyObject}, - exceptions::cstring_error, }; pub fn hash_iter<'a, I: IntoIterator>( @@ -26,13 +23,6 @@ pub trait ToCString: AsRef { fn to_cstring(&self, vm: &VirtualMachine) -> PyResult { alloc::ffi::CString::new(self.as_ref().as_bytes()).map_err(|err| err.to_pyexception(vm)) } - fn ensure_no_nul(&self, vm: &VirtualMachine) -> PyResult<()> { - if self.as_ref().as_bytes().contains(&b'\0') { - Err(cstring_error(vm)) - } else { - Ok(()) - } - } } impl ToCString for &str {} @@ -43,6 +33,7 @@ pub(crate) fn collection_repr<'a, I>( class_name: Option<&str>, prefix: &str, suffix: &str, + empty: &str, iter: I, vm: &VirtualMachine, ) -> PyResult @@ -57,10 +48,9 @@ where repr.push_str(prefix); { let mut parts_iter = iter.map(|o| o.repr(vm)); - let first = parts_iter - .next() - .transpose()? - .expect("this is not called for empty collection"); + let Some(first) = parts_iter.next().transpose()? else { + return Ok(Wtf8Buf::from(empty)); + }; repr.push_wtf8(first.as_wtf8()); for part in parts_iter { repr.push_str(", "); @@ -74,16 +64,3 @@ where Ok(repr) } - -/// Wrapper around a bytes vector that implements [`fmt::Write`]. -/// -/// # Safety -/// Don't assume the contents of the internal vector are valid UTF-8/WTF-8. -pub(crate) struct VecFmtWriter(pub Vec); - -impl fmt::Write for VecFmtWriter { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.0.extend(s.bytes()); - Ok(()) - } -} diff --git a/crates/vm/src/vm/compile.rs b/crates/vm/src/vm/compile.rs index 2dbdb17ff4a..2beaf24f07c 100644 --- a/crates/vm/src/vm/compile.rs +++ b/crates/vm/src/vm/compile.rs @@ -2,19 +2,382 @@ //! //! For code execution functions, see python_run.rs +use core::fmt; + use crate::{ - PyRef, VirtualMachine, - builtins::PyCode, + AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyCode}, compiler::{self, CompileError, CompileOpts}, + vm::compile_mode::{ + CompilerFlags, PY_EVAL_INPUT, PY_FILE_INPUT, PY_FUNC_TYPE_INPUT, PY_SINGLE_INPUT, + compile_future_features_from_flags, + }, }; +#[derive(Debug)] +pub enum VmCompileError { + Compile(CompileError), + Warning(CompileWarningError), +} + +#[derive(Debug)] +pub struct CompileWarningError { + exception: PyBaseExceptionRef, + filename: String, + lineno: usize, + offset: usize, +} + +impl From for VmCompileError { + fn from(err: CompileError) -> Self { + Self::Compile(err) + } +} + +impl fmt::Display for VmCompileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Compile(err) => err.fmt(f), + Self::Warning(_) => f.write_str("compiler warning raised as an exception"), + } + } +} + +impl VmCompileError { + pub fn into_pyexception(self, vm: &VirtualMachine, source: Option<&str>) -> PyBaseExceptionRef { + self.into_pyexception_maybe_incomplete(vm, source, false) + } + + pub fn into_pyexception_maybe_incomplete( + self, + vm: &VirtualMachine, + source: Option<&str>, + allow_incomplete: bool, + ) -> PyBaseExceptionRef { + match self { + Self::Compile(err) => { + vm.new_syntax_error_maybe_incomplete(&err, source, allow_incomplete) + } + Self::Warning(err) => err.into_pyexception(vm, source), + } + } +} + +impl CompileWarningError { + fn into_pyexception(self, vm: &VirtualMachine, source: Option<&str>) -> PyBaseExceptionRef { + if !self + .exception + .fast_isinstance(vm.ctx.exceptions.syntax_warning) + { + return self.exception; + } + let Ok(message) = self.exception.as_object().str(vm) else { + return self.exception; + }; + let syntax_error = vm.new_exception_msg( + vm.ctx.exceptions.syntax_error.to_owned(), + message.as_wtf8().to_owned(), + ); + syntax_error + .as_object() + .set_attr("lineno", vm.ctx.new_int(self.lineno), vm) + .unwrap(); + syntax_error + .as_object() + .set_attr("offset", vm.ctx.new_int(self.offset), vm) + .unwrap(); + syntax_error + .as_object() + .set_attr("filename", vm.ctx.new_str(self.filename), vm) + .unwrap(); + let text = source + .and_then(|source| source.split('\n').nth(self.lineno.saturating_sub(1))) + .map_or_else( + || vm.ctx.none(), + |line| { + vm.ctx + .new_str(format!("{}\n", line.trim_end_matches('\r'))) + .into() + }, + ); + syntax_error.as_object().set_attr("text", text, vm).unwrap(); + syntax_error + } +} + impl VirtualMachine { + #[cfg(feature = "parser")] + fn detect_source_encoding(source: &[u8]) -> Option { + fn find_encoding_in_line(line: &[u8]) -> Option { + let hash_pos = line.iter().position(|&b| b == b'#')?; + if !line[..hash_pos] + .iter() + .all(|&b| b == b' ' || b == b'\t' || b == b'\x0c' || b == b'\r') + { + return None; + } + let after_hash = &line[hash_pos..]; + let coding_pos = after_hash.windows(6).position(|w| w == b"coding")?; + let after_coding = &after_hash[coding_pos + 6..]; + let rest = if after_coding.first() == Some(&b':') || after_coding.first() == Some(&b'=') + { + &after_coding[1..] + } else { + return None; + }; + let name: String = rest + .iter() + .copied() + .skip_while(|&b| b == b' ' || b == b'\t') + .take_while(|&b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'.') + .map(|b| b as char) + .collect(); + (!name.is_empty()).then(|| VirtualMachine::normalize_source_encoding(&name)) + } + + let mut lines = source.splitn(3, |&b| b == b'\n'); + if let Some(first) = lines.next() { + let first = first.strip_prefix(b"\xef\xbb\xbf").unwrap_or(first); + if let Some(enc) = find_encoding_in_line(first) { + return Some(enc); + } + let trimmed = first + .iter() + .skip_while(|&&b| b == b' ' || b == b'\t' || b == b'\x0c' || b == b'\r') + .copied() + .collect::>(); + if !trimmed.is_empty() && trimmed[0] != b'#' { + return None; + } + } + lines.next().and_then(find_encoding_in_line) + } + + #[cfg(feature = "parser")] + fn normalize_source_encoding(name: &str) -> String { + let mut normalized = String::with_capacity(name.len().min(12)); + for ch in name.chars().take(12) { + if ch == '_' { + normalized.push('-'); + } else { + normalized.push(ch.to_ascii_lowercase()); + } + } + + if normalized == "utf-8" || normalized.starts_with("utf-8-") { + "utf-8".to_owned() + } else if normalized == "latin-1" + || normalized == "iso-8859-1" + || normalized == "iso-latin-1" + || normalized.starts_with("latin-1-") + || normalized.starts_with("iso-8859-1-") + || normalized.starts_with("iso-latin-1-") + { + "iso-8859-1".to_owned() + } else { + name.to_owned() + } + } + + #[cfg(feature = "parser")] + fn is_utf8_encoding(name: &str) -> bool { + name == "utf-8" + } + + #[cfg(feature = "parser")] + pub(crate) fn decode_source_bytes( + &self, + source: &[u8], + filename: &str, + ignore_cookie: bool, + ) -> PyResult { + let has_bom = source.starts_with(b"\xef\xbb\xbf"); + let encoding = if ignore_cookie { + None + } else { + Self::detect_source_encoding(source) + }; + let is_utf8 = encoding.as_deref().is_none_or(Self::is_utf8_encoding); + if has_bom && !is_utf8 { + let enc = encoding.as_deref().unwrap_or("utf-8"); + return Err(self.new_exception_msg( + self.ctx.exceptions.syntax_error.to_owned(), + format!("encoding problem: {enc} with BOM").into(), + )); + } + + if is_utf8 { + let src = if has_bom { &source[3..] } else { source }; + match core::str::from_utf8(src) { + Ok(s) => Ok(s.to_owned()), + Err(e) => { + let bad_byte = src[e.valid_up_to()]; + let line = src[..e.valid_up_to()] + .iter() + .filter(|&&b| b == b'\n') + .count() + + 1; + Err(self.new_exception_msg( + self.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 { + let encoding = encoding.as_deref().unwrap(); + let bytes = self.ctx.new_bytes(source.to_vec()); + let decoded = self + .state + .codec_registry + .decode_text(bytes.into(), encoding, None, self) + .map_err(|exc| { + if exc.fast_isinstance(self.ctx.exceptions.lookup_error) { + self.new_exception_msg( + self.ctx.exceptions.syntax_error.to_owned(), + format!("unknown encoding for '{filename}': {encoding}").into(), + ) + } else { + exc + } + })?; + Ok(decoded.to_string_lossy().into_owned()) + } + } + + #[cfg(feature = "parser")] + pub fn compile_string_object_with_flags( + &self, + source: &[u8], + filename: &str, + start: i32, + flags: i32, + feature_version: i32, + optimize: i32, + ) -> PyResult { + use crate::convert::ToPyException; + use crate::stdlib::_ast; + + let cf = CompilerFlags::from_bits_retain(flags); + let source = + self.decode_source_bytes(source, filename, cf.contains(CompilerFlags::IGNORE_COOKIE))?; + let source = source.as_str(); + let optimize = match optimize { + -1 => self.state.config.settings.optimize.min(2), + 0..=2 => optimize as u8, + _ => return Err(self.new_value_error("compile(): invalid optimize value")), + }; + let allow_incomplete = cf.contains(CompilerFlags::ALLOW_INCOMPLETE_INPUT); + let type_comments = cf.contains(CompilerFlags::TYPE_COMMENTS); + let dont_imply_dedent = cf.contains(CompilerFlags::DONT_IMPLY_DEDENT); + let is_ast_only = cf.contains(CompilerFlags::ONLY_AST); + let optimized_ast = cf.contains(CompilerFlags::OPTIMIZED_AST); + let future_features = compile_future_features_from_flags(flags); + let explicit_future_annotations = + future_features.contains(crate::bytecode::CodeFlags::FUTURE_ANNOTATIONS); + let target_version = if is_ast_only { + Some(ruff_python_ast::PythonVersion { + major: 3, + minor: u8::try_from(feature_version).unwrap_or(crate::version::MINOR as u8), + }) + } else { + None + }; + + if is_ast_only { + if start == PY_FUNC_TYPE_INPUT { + return _ast::parse_func_type(self, source, optimize, target_version) + .map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(self)); + } + let (parser_mode, interactive) = match start { + PY_SINGLE_INPUT => (ruff_python_parser::Mode::Module, true), + PY_FILE_INPUT => (ruff_python_parser::Mode::Module, false), + PY_EVAL_INPUT => (ruff_python_parser::Mode::Expression, false), + _ => { + return Err( + self.new_system_error("Invalid start argument passed to Py_CompileString") + ); + } + }; + let parsed = _ast::parse( + self, + source, + parser_mode, + optimize, + target_version, + type_comments, + optimized_ast, + interactive, + explicit_future_annotations, + dont_imply_dedent, + ) + .map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(self))?; + if start == PY_SINGLE_INPUT { + return _ast::wrap_interactive(self, parsed); + } + return Ok(parsed); + } + + if type_comments { + let parser_mode = match start { + PY_SINGLE_INPUT | PY_FILE_INPUT => ruff_python_parser::Mode::Module, + PY_EVAL_INPUT => ruff_python_parser::Mode::Expression, + _ => { + return Err( + self.new_system_error("Invalid start argument passed to Py_CompileString") + ); + } + }; + _ast::parse( + self, + source, + parser_mode, + optimize, + None, + type_comments, + false, + start == PY_SINGLE_INPUT, + explicit_future_annotations, + dont_imply_dedent, + ) + .map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(self))?; + } + + let mode = match start { + PY_SINGLE_INPUT => compiler::Mode::Single, + PY_FILE_INPUT => compiler::Mode::Exec, + PY_EVAL_INPUT => compiler::Mode::Eval, + PY_FUNC_TYPE_INPUT => compiler::Mode::BlockExpr, + _ => { + return Err( + self.new_system_error("Invalid start argument passed to Py_CompileString") + ); + } + }; + let mut opts = self.compile_opts(); + opts.optimize = optimize; + opts.allow_top_level_await = cf.contains(CompilerFlags::ALLOW_TOP_LEVEL_AWAIT); + opts.future_features = future_features; + opts.dont_imply_dedent = dont_imply_dedent; + let code = self + .compile_with_opts(source, mode, filename, opts) + .map_err(|err| { + err.into_pyexception_maybe_incomplete(self, Some(source), allow_incomplete) + })?; + Ok(code.into()) + } + pub fn compile( &self, source: &str, mode: compiler::Mode, - source_path: &str, - ) -> Result, CompileError> { + source_path: impl Into, + ) -> Result, VmCompileError> { self.compile_with_opts(source, mode, source_path, self.compile_opts()) } @@ -22,18 +385,55 @@ impl VirtualMachine { &self, source: &str, mode: compiler::Mode, - source_path: &str, + source_path: impl Into, opts: CompileOpts, - ) -> Result, CompileError> { - let code = compiler::compile(source, mode, source_path, opts) - .map(|code| PyCode::new_ref_from_bytecode(self, code)); - + ) -> Result, VmCompileError> { + let source_path = source_path.into(); #[cfg(feature = "parser")] - if code.is_ok() { - self.emit_string_escape_warnings(source, source_path); + { + self.emit_tokenizer_syntax_warnings(source, &source_path) + .map_err(VmCompileError::Warning)?; + self.emit_string_escape_warnings(source, &source_path) + .map_err(VmCompileError::Warning)?; } - - code + #[cfg(feature = "parser")] + let code = { + // A warning the filter escalates to an exception is stashed here so + // its precise category survives; codegen only sees an abort marker. + let escalated: core::cell::Cell> = + core::cell::Cell::new(None); + let mut syntax_warning_handler = |location, message| { + escape_warnings::warn_syntax_at_location(&source_path, location, message, self) + .map_err(|warning| { + escalated.set(Some(warning)); + // Recovered below via `escalated`, so this is never surfaced. + compiler::codegen::error::CodegenError { + location: Some(location), + error: compiler::codegen::error::CodegenErrorType::SyntaxError( + String::new(), + ), + source_path: source_path.clone(), + } + }) + }; + let result = compiler::compile_with_syntax_warning_handler( + source, + mode, + &source_path, + opts, + &mut syntax_warning_handler, + ); + match escalated.take() { + Some(warning) => return Err(VmCompileError::Warning(warning)), + None => result, + } + }; + #[cfg(not(feature = "parser"))] + let code = compiler::compile(source, mode, &source_path, opts); + let code = code + .map(|code| PyCode::new_ref_from_bytecode(self, code)) + .map_err(VmCompileError::Compile)?; + Ok(code) } } @@ -59,6 +459,30 @@ mod escape_warnings { + 1 } + fn line_offset_at(source: &str, offset: usize) -> (usize, usize) { + let offset = offset.min(source.len()); + let prefix = &source[..offset]; + let lineno = prefix.bytes().filter(|&b| b == b'\n').count() + 1; + let line_start = prefix.rfind('\n').map_or(0, |index| index + 1); + let column = source[line_start..offset].chars().count() + 1; + (lineno, column) + } + + fn compile_warning_error( + exception: PyBaseExceptionRef, + source: &str, + filename: &str, + offset: usize, + ) -> CompileWarningError { + let (lineno, offset) = line_offset_at(source, offset); + CompileWarningError { + exception, + filename: filename.to_owned(), + lineno, + offset, + } + } + /// Get content bounds (start, end byte offsets) of a quoted string literal, /// excluding prefix characters and quote delimiters. fn content_bounds(source: &str, range: TextRange) -> Option<(usize, usize)> { @@ -180,7 +604,7 @@ mod escape_warnings { offset: usize, filename: &str, vm: &VirtualMachine, - ) { + ) -> Result<(), CompileWarningError> { let lineno = line_number_at(source, offset); let message = vm.ctx.new_str(format!( "\"\\{ch}\" is an invalid escape sequence. \ @@ -188,7 +612,7 @@ mod escape_warnings { Did you mean \"\\\\{ch}\"? A raw string is also an option." )); let fname = vm.ctx.new_str(filename); - let _ = warn::warn_explicit( + warn::warn_explicit( Some(vm.ctx.exceptions.syntax_warning.to_owned()), message.into(), fname, @@ -198,23 +622,273 @@ mod escape_warnings { None, None, vm, - ); + ) + .map_err(|err| compile_warning_error(err, source, filename, offset)) + } + + fn warn_syntax_at_offset( + source: &str, + filename: &str, + offset: usize, + message: String, + vm: &VirtualMachine, + ) -> Result<(), CompileWarningError> { + let lineno = line_number_at(source, offset); + let fname = vm.ctx.new_str(filename); + let message = vm.ctx.new_str(message); + warn::warn_explicit( + Some(vm.ctx.exceptions.syntax_warning.to_owned()), + message.into(), + fname, + lineno, + None, + vm.ctx.none(), + None, + None, + vm, + ) + .map_err(|err| compile_warning_error(err, source, filename, offset)) + } + + pub(super) fn warn_syntax_at_location( + filename: &str, + location: compiler::core::SourceLocation, + message: String, + vm: &VirtualMachine, + ) -> Result<(), CompileWarningError> { + let fname = vm.ctx.new_str(filename); + let message = vm.ctx.new_str(message); + warn::warn_explicit( + Some(vm.ctx.exceptions.syntax_warning.to_owned()), + message.into(), + fname, + location.line.get(), + None, + vm.ctx.none(), + None, + None, + vm, + ) + .map_err(|exception| CompileWarningError { + exception, + filename: filename.to_owned(), + lineno: location.line.get(), + offset: location.character_offset.get(), + }) + } + + fn is_ascii_identifier_char(byte: u8) -> bool { + byte == b'_' || byte.is_ascii_alphanumeric() + } + + fn numeric_keyword_suffix(rest: &[u8]) -> bool { + rest.starts_with(b"and") + || rest.starts_with(b"else") + || rest.starts_with(b"for") + || rest.starts_with(b"if") + || rest.starts_with(b"in") + || rest.starts_with(b"is") + || rest.starts_with(b"or") + || rest.starts_with(b"not") + } + + fn consume_decimal_digits(bytes: &[u8], mut index: usize) -> usize { + while index < bytes.len() { + match bytes[index] { + b'0'..=b'9' => index += 1, + b'_' if bytes + .get(index + 1) + .is_some_and(|byte| byte.is_ascii_digit()) => + { + index += 2; + } + _ => break, + } + } + index + } + + fn consume_radix_digits( + bytes: &[u8], + mut index: usize, + is_digit: impl Fn(u8) -> bool, + ) -> usize { + while index < bytes.len() { + if is_digit(bytes[index]) { + index += 1; + } else if bytes.get(index) == Some(&b'_') + && bytes.get(index + 1).is_some_and(|&byte| is_digit(byte)) + { + index += 2; + } else { + break; + } + } + index + } + + fn number_literal_end(bytes: &[u8], start: usize) -> Option<(&'static str, usize)> { + if bytes.get(start) == Some(&b'.') { + if !bytes + .get(start + 1) + .is_some_and(|byte| byte.is_ascii_digit()) + { + return None; + } + let mut index = consume_decimal_digits(bytes, start + 1); + index = consume_exponent(bytes, index); + if matches!(bytes.get(index), Some(b'j' | b'J')) { + return Some(("imaginary", index + 1)); + } + return Some(("decimal", index)); + } + + if !bytes.get(start).is_some_and(|byte| byte.is_ascii_digit()) { + return None; + } + + if bytes.get(start) == Some(&b'0') { + match bytes.get(start + 1) { + Some(b'x' | b'X') => { + let end = + consume_radix_digits(bytes, start + 2, |byte| byte.is_ascii_hexdigit()); + return Some(("hexadecimal", end)); + } + Some(b'o' | b'O') => { + let end = + consume_radix_digits(bytes, start + 2, |byte| matches!(byte, b'0'..=b'7')); + return Some(("octal", end)); + } + Some(b'b' | b'B') => { + let end = + consume_radix_digits(bytes, start + 2, |byte| matches!(byte, b'0' | b'1')); + return Some(("binary", end)); + } + _ => {} + } + } + + let mut index = consume_decimal_digits(bytes, start); + if bytes.get(index) == Some(&b'.') { + index = consume_decimal_digits(bytes, index + 1); + } + index = consume_exponent(bytes, index); + if matches!(bytes.get(index), Some(b'j' | b'J')) { + return Some(("imaginary", index + 1)); + } + Some(("decimal", index)) + } + + fn consume_exponent(bytes: &[u8], index: usize) -> usize { + if !matches!(bytes.get(index), Some(b'e' | b'E')) { + return index; + } + let mut cursor = index + 1; + if matches!(bytes.get(cursor), Some(b'+' | b'-')) { + cursor += 1; + } + if bytes.get(cursor).is_some_and(|byte| byte.is_ascii_digit()) { + consume_decimal_digits(bytes, cursor) + } else { + index + } + } + + fn skip_quoted_string(bytes: &[u8], mut index: usize) -> usize { + let quote = bytes[index]; + let triple = bytes.get(index + 1) == Some("e) && bytes.get(index + 2) == Some("e); + let quote_len = if triple { 3 } else { 1 }; + index += quote_len; + while index < bytes.len() { + if bytes[index] == b'\\' { + index = (index + 2).min(bytes.len()); + } else if triple + && bytes.get(index) == Some("e) + && bytes.get(index + 1) == Some("e) + && bytes.get(index + 2) == Some("e) + { + return index + 3; + } else if !triple && bytes[index] == quote { + return index + 1; + } else { + index += 1; + } + } + index + } + + fn emit_numeric_literal_warnings( + source: &str, + filename: &str, + vm: &VirtualMachine, + ) -> Result<(), CompileWarningError> { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'#' => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\'' | b'"' => { + index = skip_quoted_string(bytes, index); + } + byte if byte >= 0x80 || byte == b'_' || byte.is_ascii_alphabetic() => { + index += 1; + while index < bytes.len() + && (bytes[index] >= 0x80 || is_ascii_identifier_char(bytes[index])) + { + index += 1; + } + } + b'.' | b'0'..=b'9' => { + let Some((kind, end)) = number_literal_end(bytes, index) else { + index += 1; + continue; + }; + if end > index && numeric_keyword_suffix(&bytes[end..]) { + warn_syntax_at_offset( + source, + filename, + index, + format!("invalid {kind} literal"), + vm, + )?; + } + index = end.max(index + 1); + } + _ => index += 1, + } + } + Ok(()) } struct EscapeWarningVisitor<'a> { source: &'a str, filename: &'a str, vm: &'a VirtualMachine, + error: Option, } impl<'a> EscapeWarningVisitor<'a> { + fn record_warning(&mut self, result: Result<(), CompileWarningError>) { + if self.error.is_none() + && let Err(err) = result + { + self.error = Some(err); + } + } + /// Check a quoted string/bytes literal for invalid escapes. /// The range must include the prefix and quote delimiters. - fn check_quoted_literal(&self, range: TextRange, is_bytes: bool) { + fn check_quoted_literal(&mut self, range: TextRange, is_bytes: bool) { if let Some((start, end)) = content_bounds(self.source, range) && let Some((ch, offset)) = first_invalid_escape(self.source, start, end, is_bytes) { - warn_invalid_escape_sequence(self.source, ch, offset, self.filename, self.vm); + let result = + warn_invalid_escape_sequence(self.source, ch, offset, self.filename, self.vm); + self.record_warning(result); } } @@ -224,14 +898,16 @@ mod escape_warnings { /// Also handles `\{` / `\}` at the literal–interpolation boundary, /// equivalent to `_PyTokenizer_warn_invalid_escape_sequence` handling /// `FSTRING_MIDDLE` / `FSTRING_END` tokens. - fn check_fstring_literal(&self, range: TextRange) { + fn check_fstring_literal(&mut self, range: TextRange) { let start = range.start().to_usize(); let end = range.end().to_usize(); if start >= end || end > self.source.len() { return; } if let Some((ch, offset)) = first_invalid_escape(self.source, start, end, false) { - warn_invalid_escape_sequence(self.source, ch, offset, self.filename, self.vm); + let result = + warn_invalid_escape_sequence(self.source, ch, offset, self.filename, self.vm); + self.record_warning(result); return; } // In CPython, _PyTokenizer_warn_invalid_escape_sequence handles @@ -249,13 +925,14 @@ mod escape_warnings { && let Some(&after) = self.source.as_bytes().get(end) && (after == b'{' || after == b'}') { - warn_invalid_escape_sequence( + let result = warn_invalid_escape_sequence( self.source, after as char, end - 1, self.filename, self.vm, ); + self.record_warning(result); } } @@ -263,6 +940,9 @@ mod escape_warnings { /// interpolation expressions and format specs. fn visit_fstring_elements(&mut self, elements: &'a ast::InterpolatedStringElements) { for element in elements { + if self.error.is_some() { + return; + } match element { ast::InterpolatedStringElement::Literal(lit) => { self.check_fstring_literal(lit.range); @@ -280,6 +960,9 @@ mod escape_warnings { impl<'a> Visitor<'a> for EscapeWarningVisitor<'a> { fn visit_expr(&mut self, expr: &'a ast::Expr) { + if self.error.is_some() { + return; + } match expr { // Regular string literals — decode_unicode_with_escapes path ast::Expr::StringLiteral(string) => { @@ -334,21 +1017,36 @@ mod escape_warnings { } impl VirtualMachine { + /// Emit tokenizer-level SyntaxWarnings raised before + /// code generation. + pub(super) fn emit_tokenizer_syntax_warnings( + &self, + source: &str, + filename: &str, + ) -> Result<(), CompileWarningError> { + emit_numeric_literal_warnings(source, filename, self) + } + /// Walk all string literals in `source` and emit `SyntaxWarning` for /// each that contains an invalid escape sequence. - pub(super) fn emit_string_escape_warnings(&self, source: &str, filename: &str) { + pub(super) fn emit_string_escape_warnings( + &self, + source: &str, + filename: &str, + ) -> Result<(), CompileWarningError> { let Ok(parsed) = ruff_python_parser::parse(source, ruff_python_parser::Mode::Module.into()) else { - return; + return Ok(()); }; let ast = parsed.into_syntax(); let mut visitor = EscapeWarningVisitor { source, filename, vm: self, + error: None, }; - match ast { + match &ast { ast::Mod::Module(module) => { for stmt in &module.body { visitor.visit_stmt(stmt); @@ -358,6 +1056,227 @@ mod escape_warnings { visitor.visit_expr(&expr.body); } } + visitor.error.map_or(Ok(()), Err) + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::{Interpreter, builtins::PyTuple}; + + fn install_syntax_warning_error_filter(vm: &VirtualMachine) { + let error_filter = PyTuple::new_ref( + vec![ + vm.ctx.new_str("error").into(), + vm.ctx.none(), + vm.ctx.exceptions.syntax_warning.as_object().to_owned(), + vm.ctx.none(), + vm.ctx.new_int(0).into(), + ], + &vm.ctx, + ); + vm.state + .warnings + .filters + .borrow_vec_mut() + .insert(0, error_filter.into()); + vm.state.warnings.filters_mutated(); + } + + fn first_compiler_warning(source: &str) -> String { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + install_syntax_warning_error_filter(vm); + let err = vm + .compile(source, compiler::Mode::Exec, "") + .expect_err("expected compiler SyntaxWarning"); + let exception = err.into_pyexception(vm, Some(source)); + exception + .as_object() + .str(vm) + .expect("warning message should stringify") + .as_wtf8() + .to_string() + }) + } + + fn compile_error_message(source: &str) -> String { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + install_syntax_warning_error_filter(vm); + let err = match vm.compile(source, compiler::Mode::Exec, "") { + Ok(_) => panic!("expected compile error"), + Err(err) => err, + }; + err.into_pyexception(vm, Some(source)) + .as_object() + .str(vm) + .expect("compile error should stringify") + .as_wtf8() + .to_string() + }) + } + + #[test] + fn codegen_caller_warning_precedes_later_return_error() { + let message = compile_error_message("(1)()\nreturn\n"); + assert!( + message.contains("'int' object is not callable"), + "expected caller SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn symboltable_error_still_precedes_codegen_caller_warning() { + let message = compile_error_message("(1)()\ndef f():\n from x import *\n"); + assert!( + message.contains("import * only allowed at module level"), + "expected symboltable error first, got {message:?}" + ); + } + + #[test] + fn codegen_compare_warning_precedes_later_return_error() { + let message = compile_error_message("1 is 1\nreturn\n"); + assert!( + message.contains("\"is\" with 'int' literal"), + "expected compare SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn codegen_assert_warning_precedes_later_return_error() { + let message = compile_error_message("assert (1,)\nreturn\n"); + assert!( + message.contains("assertion is always true"), + "expected assert SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn codegen_subscript_warning_precedes_later_return_error() { + let message = compile_error_message("(1)[None]\nreturn\n"); + assert!( + message.contains("'int' object is not subscriptable"), + "expected subscript SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn codegen_index_warning_precedes_later_return_error() { + let message = compile_error_message("'x'[None]\nreturn\n"); + assert!( + message.contains("str indices must be integers or slices, not NoneType"), + "expected index SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn string_escape_warning_precedes_later_return_error() { + let message = compile_error_message("\"\\z\"\nreturn\n"); + assert!( + message.contains("\"\\z\" is an invalid escape sequence"), + "expected invalid escape SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn string_escape_warning_precedes_later_symboltable_error() { + let message = compile_error_message("\"\\z\"\ndef f():\n from x import *\n"); + assert!( + message.contains("\"\\z\" is an invalid escape sequence"), + "expected invalid escape SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn ast_preprocess_finally_warning_precedes_later_return_error() { + let message = compile_error_message("try:\n pass\nfinally:\n return\nreturn\n"); + assert!( + message.contains("'return' in a 'finally' block"), + "expected finally SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn ast_preprocess_finally_warning_precedes_symboltable_error() { + let message = compile_error_message( + "def f():\n from x import *\ntry:\n pass\nfinally:\n return\n", + ); + assert!( + message.contains("'return' in a 'finally' block"), + "expected finally SyntaxWarning first, got {message:?}" + ); + } + + #[test] + fn compiler_warning_visits_function_decorators_before_defaults_and_body() { + let message = first_compiler_warning( + r#" +@(b"decorator")() +def f(x=(1)()): + assert (1,) +"#, + ); + assert!( + message.contains("'bytes' object is not callable"), + "expected decorator warning first, got {message:?}" + ); + } + + #[test] + fn compiler_warning_visits_function_defaults_before_annotations() { + let message = first_compiler_warning( + r#" +def f(x: (1)() = ("default")()): + pass +"#, + ); + assert!( + message.contains("'str' object is not callable"), + "expected default warning before annotation warning, got {message:?}" + ); + } + + #[test] + fn compiler_warning_visits_class_decorators_before_body_and_bases() { + let message = first_compiler_warning( + r#" +@(b"decorator")() +class C((1)()): + assert (1,) +"#, + ); + assert!( + message.contains("'bytes' object is not callable"), + "expected class decorator warning first, got {message:?}" + ); + } + + #[test] + fn compiler_warning_visits_class_body_before_bases() { + let message = first_compiler_warning( + r#" +class C((1)()): + assert (1,) +"#, + ); + assert!( + message.contains("assertion is always true"), + "expected class body warning before base warning, got {message:?}" + ); + } + + #[test] + fn compiler_warning_visits_type_alias_type_params_before_value() { + let message = first_compiler_warning( + r#" +type Alias[T: (1)()] = ("value")() +"#, + ); + assert!( + message.contains("'int' object is not callable"), + "expected type parameter warning before alias value warning, got {message:?}" + ); } } } diff --git a/crates/vm/src/vm/compile_mode.rs b/crates/vm/src/vm/compile_mode.rs new file mode 100644 index 00000000000..9885ba2e1f7 --- /dev/null +++ b/crates/vm/src/vm/compile_mode.rs @@ -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()) +} diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 226d5f1a1a7..5deaffb3f6a 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -14,7 +14,6 @@ use crate::{ object, pystr, type_::PyAttributes, }, - bytecode::{self, CodeFlags, CodeUnit, Instruction, Opcode}, class::StaticType, common::rc::PyRc, exceptions, @@ -31,7 +30,6 @@ use malachite_bigint::BigInt; use num_complex::Complex64; use num_traits::ToPrimitive; use rustpython_common::lock::PyRwLock; -use rustpython_compiler_core::{OneIndexed, SourceLocation}; #[derive(Debug)] pub struct Context { @@ -52,15 +50,11 @@ pub struct Context { pub int_cache_pool: Vec, pub(crate) latin1_char_cache: Vec>, pub(crate) ascii_char_cache: Vec>, - pub(crate) init_cleanup_code: PyRef, // there should only be exact objects of str in here, no non-str objects and no subclasses pub(crate) string_pool: StringPool, pub(crate) slot_new_wrapper: PyMethodDef, pub names: ConstName, - // GC module state (callbacks and garbage lists) - pub gc_callbacks: PyListRef, - pub gc_garbage: PyListRef, } macro_rules! declare_const_name { @@ -109,6 +103,7 @@ declare_const_name! { __await__, __bases__, __bool__, + __buffer__, __build_class__, __builtins__, __bytes__, @@ -211,6 +206,7 @@ declare_const_name! { __rdivmod__, __reduce__, __reduce_ex__, + __release_buffer__, __repr__, __reversed__, __rfloordiv__, @@ -362,14 +358,10 @@ impl Context { PyMethodFlags::METHOD, None, ); - let init_cleanup_code = Self::new_init_cleanup_code(&types, &names); - let empty_str = unsafe { string_pool.intern("", types.str_type.to_owned()) }; let empty_bytes = create_object(PyBytes::from(Vec::new()), types.bytes_type); // GC callbacks and garbage lists - let gc_callbacks = PyRef::new_ref(PyList::default(), types.list_type.to_owned(), None); - let gc_garbage = PyRef::new_ref(PyList::default(), types.list_type.to_owned(), None); Self { true_value, @@ -389,59 +381,12 @@ impl Context { int_cache_pool, latin1_char_cache, ascii_char_cache, - init_cleanup_code, string_pool, slot_new_wrapper, names, - - gc_callbacks, - gc_garbage, } } - fn new_init_cleanup_code(types: &TypeZoo, names: &ConstName) -> PyRef { - let loc = SourceLocation { - line: OneIndexed::MIN, - character_offset: OneIndexed::from_zero_indexed(0), - }; - let instructions = [ - CodeUnit { - op: Instruction::ExitInitCheck, - arg: 0.into(), - }, - CodeUnit { - op: Instruction::ReturnValue, - arg: 0.into(), - }, - CodeUnit { - op: Opcode::Resume.into(), - arg: 0.into(), - }, - ]; - let code = bytecode::CodeObject { - instructions: instructions.into(), - locations: vec![(loc, loc); instructions.len()].into_boxed_slice(), - flags: CodeFlags::OPTIMIZED, - posonlyarg_count: 0, - arg_count: 0, - kwonlyarg_count: 0, - source_path: names.__init__, - first_line_number: None, - max_stackdepth: 2, - obj_name: names.__init__, - qualname: names.__init__, - constants: core::iter::empty().collect(), - names: Vec::new().into_boxed_slice(), - varnames: Vec::new().into_boxed_slice(), - cellvars: Vec::new().into_boxed_slice(), - freevars: Vec::new().into_boxed_slice(), - localspluskinds: Vec::new().into_boxed_slice(), - linetable: Vec::new().into_boxed_slice(), - exceptiontable: Vec::new().into_boxed_slice(), - }; - PyRef::new_ref(PyCode::new(code), types.code_type.to_owned(), None) - } - pub fn intern_str(&self, s: S) -> &'static PyStrInterned { unsafe { self.string_pool.intern(s, self.types.str_type.to_owned()) } } @@ -491,6 +436,14 @@ impl Context { PyInt::from(i).into_ref(self) } + /// Borrow a cached small integer whose lifetime is tied to this context. + #[inline(always)] + pub(crate) fn cached_int(&self, i: i32) -> &PyIntRef { + debug_assert!(Self::INT_CACHE_POOL_RANGE.contains(&i)); + let inner_idx = (i - Self::INT_CACHE_POOL_MIN) as usize; + &self.int_cache_pool[inner_idx] + } + #[inline] pub fn new_bigint(&self, i: &BigInt) -> PyIntRef { if let Some(i) = i.to_i32() diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index 79e3e190a2f..c538eb32ec8 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -1,6 +1,11 @@ -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] use super::StopTheWorldState; -use super::{Context, PyConfig, PyGlobalState, VirtualMachine, setting::Settings, thread}; +use super::{ + Context, PyConfig, PyGlobalState, VirtualMachine, + runtime::{self, InterpreterWhence}, + setting::Settings, + thread, +}; use crate::{ PyResult, builtins, common::rc::PyRc, frozen::FrozenModule, getpath, py_freeze, stdlib::atexit, vm::PyBaseExceptionRef, @@ -36,18 +41,34 @@ pub struct InterpreterBuilder { init_hooks: Vec, } -/// Private helper to initialize a VM with settings, context, and custom initialization. -fn initialize_main_vm( +/// Options for constructing a main or sub-interpreter VM. +struct InitializeVmOpts<'a> { settings: Settings, ctx: PyRc, module_defs: Vec<&'static builtins::PyModuleDef>, frozen_modules: Vec<(&'static str, FrozenModule)>, init_hooks: Vec, - init: F, -) -> (VirtualMachine, PyRc) + is_main: bool, + whence: InterpreterWhence, + /// When `Some`, reuse parent module_defs/frozen/config seeds for a subinterpreter. + parent_state: Option<&'a PyGlobalState>, +} + +/// Shared constructor for main and sub-interpreters. +fn initialize_vm(opts: InitializeVmOpts<'_>, init: F) -> (VirtualMachine, PyRc) where F: FnOnce(&mut VirtualMachine), { + let InitializeVmOpts { + settings, + ctx, + module_defs, + frozen_modules, + init_hooks, + is_main, + whence, + parent_state, + } = opts; use crate::codecs::CodecsRegistry; use crate::common::hash::HashSecret; use crate::common::lock::PyMutex; @@ -55,55 +76,85 @@ where use core::sync::atomic::{AtomicBool, AtomicU64}; use crossbeam_utils::atomic::AtomicCell; - let paths = getpath::init_path_config(&settings); - let config = PyConfig::new(settings, paths); + let (config, all_module_defs, frozen, hash_secret, int_max_str_digits) = + if let Some(parent) = parent_state { + // Subinterpreter: clone config and module tables from parent, fresh runtime state. + let int_max_str_digits = AtomicCell::new(parent.int_max_str_digits.load()); + ( + parent.config.clone(), + parent.module_defs.clone(), + parent.frozen.clone(), + parent.hash_secret, + int_max_str_digits, + ) + } else { + let paths = getpath::init_path_config(&settings); + let config = PyConfig::new(settings, paths); - // Build module_defs map from builtin modules + additional modules - let mut all_module_defs: BTreeMap<&'static str, &'static builtins::PyModuleDef> = - crate::stdlib::builtin_module_defs(&ctx) - .into_iter() - .chain(module_defs) - .map(|def| (def.name.as_str(), def)) - .collect(); + // Build module_defs map from builtin modules + additional modules + let mut all_module_defs: BTreeMap<&'static str, &'static builtins::PyModuleDef> = + crate::stdlib::builtin_module_defs(&ctx) + .into_iter() + .chain(module_defs) + .map(|def| (def.name.as_str(), def)) + .collect(); - // Register sysconfigdata under platform-specific name as well - if let Some(&sysconfigdata_def) = all_module_defs.get("_sysconfigdata") { - use std::sync::OnceLock; - static SYSCONFIGDATA_NAME: OnceLock<&'static str> = OnceLock::new(); - let leaked_name = *SYSCONFIGDATA_NAME.get_or_init(|| { - let name = crate::stdlib::sys::sysconfigdata_name(); - Box::leak(name.into_boxed_str()) - }); - all_module_defs.insert(leaked_name, sysconfigdata_def); - } + // Register sysconfigdata under platform-specific name as well + if let Some(&sysconfigdata_def) = all_module_defs.get("_sysconfigdata") { + use std::sync::OnceLock; + static SYSCONFIGDATA_NAME: OnceLock<&'static str> = OnceLock::new(); + let leaked_name = *SYSCONFIGDATA_NAME.get_or_init(|| { + let name = crate::stdlib::sys::sysconfigdata_name(); + Box::leak(name.into_boxed_str()) + }); + all_module_defs.insert(leaked_name, sysconfigdata_def); + } - // Create hash secret - let seed = match config.settings.hash_seed { - Some(seed) => seed, - None => super::process_hash_secret_seed(), - }; - let hash_secret = HashSecret::new(seed); + let seed = match config.settings.hash_seed { + Some(seed) => seed, + None => super::process_hash_secret_seed(), + }; + let hash_secret = HashSecret::new(seed); + + let int_max_str_digits = AtomicCell::new(match config.settings.int_max_str_digits { + -1 => 4300, + other => other, + } as usize); + + let mut frozen: std::collections::HashMap< + &'static str, + FrozenModule, + rapidhash::quality::RandomState, + > = core_frozen_inits().collect(); + frozen.extend(frozen_modules); + + ( + config, + all_module_defs, + frozen, + hash_secret, + int_max_str_digits, + ) + }; - // Create codec registry and warnings state + // Per-interpreter ephemeral state (must not be shared across interpreters). let codec_registry = CodecsRegistry::new(&ctx); let warnings = WarningsState::init_state(&ctx); - // Create int_max_str_digits - let int_max_str_digits = AtomicCell::new(match config.settings.int_max_str_digits { - -1 => 4300, - other => other, - } as usize); - - // Initialize frozen modules (core + user-provided) - let mut frozen: std::collections::HashMap< - &'static str, - FrozenModule, - rapidhash::quality::RandomState, - > = core_frozen_inits().collect(); - frozen.extend(frozen_modules); - - // Create PyGlobalState + let interpreter_id = runtime::alloc_interpreter_id(); + + // Process main OS thread identity is process-global; subinterpreters inherit + // it from the parent so `is_main_thread()` stays correct when running on the + // main OS thread under a subinterpreter. + #[cfg(feature = "threading")] + let main_thread_ident = AtomicCell::new(parent_state.map_or(0, |p| p.main_thread_ident.load())); + + // Create PyGlobalState (≈ PyInterpreterState) let global_state = PyRc::new(PyGlobalState { + gc: crate::gc_state::GcInterpreterState::new(&ctx), + interpreter_id, + whence, + is_main, config, module_defs: all_module_defs, frozen, @@ -122,8 +173,9 @@ where switch_interval: AtomicCell::new(0.005), global_trace_func: PyMutex::default(), global_profile_func: PyMutex::default(), + type_mutex: PyMutex::default(), #[cfg(feature = "threading")] - main_thread_ident: AtomicCell::new(0), + main_thread_ident, #[cfg(feature = "threading")] thread_frames: parking_lot::Mutex::new(std::collections::HashMap::new()), #[cfg(feature = "threading")] @@ -133,7 +185,7 @@ where monitoring: PyMutex::default(), monitoring_events: AtomicCell::new(0), instrumentation_version: AtomicU64::new(0), - #[cfg(all(unix, feature = "threading"))] + #[cfg(feature = "threading")] stop_the_world: StopTheWorldState::new(), }); @@ -149,7 +201,19 @@ where // Call custom init function (can mutate vm.state) init(&mut vm); + // Register before `initialize()` runs any Python: it allocates GC-tracked + // objects, so a collection on another thread has to be able to stop this + // interpreter while that happens. It cannot be registered earlier — the + // hooks above take `PyRc::get_mut` on the state, which fails once the + // registry holds a weak reference to it. + runtime::register_interpreter(&vm.state); + + // `initialize()` runs Python bytecode directly (e.g. importing `codecs` + // and `encodings`) before any `enter_vm` scope exists, so attach this + // thread for the duration so type cache reads see it as ATTACHED. + let vm_guard = thread::VmBootstrapGuard::new(&vm); vm.initialize(); + drop(vm_guard); // Clone global_state for Interpreter after all initialization is done let global_state = vm.state.clone(); @@ -265,12 +329,17 @@ impl InterpreterBuilder { /// This consumes the configuration and returns a fully initialized Interpreter. #[must_use] pub fn build(self) -> Interpreter { - let (vm, global_state) = initialize_main_vm( - self.settings, - self.ctx, - self.module_defs, - self.frozen_modules, - self.init_hooks, + let (vm, global_state) = initialize_vm( + InitializeVmOpts { + settings: self.settings, + ctx: self.ctx, + module_defs: self.module_defs, + frozen_modules: self.frozen_modules, + init_hooks: self.init_hooks, + is_main: true, + whence: InterpreterWhence::Runtime, + parent_state: None, + }, |_| {}, // No additional init needed ); Interpreter { global_state, vm } @@ -289,7 +358,13 @@ impl Default for InterpreterBuilder { } } -/// The general interface for the VM +/// One isolated Python interpreter in the process (≈ CPython `PyInterpreterState` + main tstate). +/// +/// Historically RustPython exposed a single process-level `Interpreter`. For PEP 734 +/// (multiple interpreters / subinterpreters) this type is now the owned handle for +/// **one** interpreter. Use [`Interpreter::create_subinterpreter`] to create additional +/// isolated interpreters that share the process-wide type context but not modules or +/// `PyGlobalState`. /// /// # Examples /// Runs a simple embedded hello world program. @@ -300,8 +375,10 @@ impl Default for InterpreterBuilder { /// let scope = vm.new_scope_with_builtins(); /// let source = r#"print("Hello World!")"#; /// let code_obj = vm.compile( -/// source, Mode::Exec, "" -/// ).map_err(|err| vm.new_syntax_error(&err, Some(source))).unwrap(); +/// source, +/// Mode::Exec, +/// "", +/// ).map_err(|err| err.into_pyexception(vm, Some(source))).unwrap(); /// vm.run_code_obj(code_obj, scope).unwrap(); /// }); /// ``` @@ -342,17 +419,113 @@ impl Interpreter { where F: FnOnce(&mut VirtualMachine), { - let (vm, global_state) = initialize_main_vm( - settings, - Context::genesis().clone(), - Vec::new(), // No module_defs - Vec::new(), // No frozen_modules - Vec::new(), // No init_hooks + let (vm, global_state) = initialize_vm( + InitializeVmOpts { + settings, + ctx: Context::genesis().clone(), + module_defs: Vec::new(), + frozen_modules: Vec::new(), + init_hooks: Vec::new(), + is_main: true, + whence: InterpreterWhence::Runtime, + parent_state: None, + }, init, ); Self { global_state, vm } } + /// Process-global interpreter id (main is [`super::MAIN_INTERPRETER_ID`]). + #[inline] + #[must_use] + pub fn id(&self) -> i64 { + self.global_state.interpreter_id + } + + /// Where this interpreter was created. + #[inline] + #[must_use] + pub fn whence(&self) -> InterpreterWhence { + self.global_state.whence + } + + /// Whether this is a top-level interpreter rather than a subinterpreter. + /// + /// Every top-level interpreter answers `true`; for *the* process main, use + /// [`Interpreter::is_process_main`]. + #[inline] + #[must_use] + pub fn is_main(&self) -> bool { + self.global_state.is_main + } + + /// Whether this is the PEP 734 process main interpreter (`get_main()`). + /// + /// Unlike [`Interpreter::is_main`], which is set for every top-level + /// interpreter, this is true for only the single first-registered main. + #[inline] + #[must_use] + pub fn is_process_main(&self) -> bool { + runtime::main_interpreter_id() == Some(self.id()) + } + + /// Create a subinterpreter and hand ownership to the runtime, returning its + /// id. The runtime keeps it alive until [`runtime::take_owned_interpreter`]. + /// + /// This is the shape `_interpreters.create()` will use: Python receives an + /// id, not an owned handle. + #[cfg(feature = "threading")] + #[must_use] + pub fn create_owned_subinterpreter(&self) -> i64 { + runtime::store_owned_interpreter(self.create_subinterpreter()) + } + + /// Create an isolated subinterpreter sharing this interpreter's type context + /// (`Context`) and module definitions, but with its own `sys.modules`, + /// builtins module instance, thread registry, and stop-the-world state. + /// + /// This is the Rust-side foundation for PEP 734 / `_interpreters.create()`. + /// It does not yet expose a Python module API. + /// + /// May be called while the parent is entered (matching CPython, where + /// `_interpreters.create()` runs under the main interpreter). When the + /// calling thread is currently attached to a VM, that attachment is + /// temporarily saved so the subinterpreter can bootstrap as an outermost + /// enter (correct thread-slot / stop-the-world state). + #[must_use] + pub fn create_subinterpreter(&self) -> Self { + // Suspend the caller's current VM attachment (if any) for the duration + // of subinterpreter initialization. Nested bootstrap would otherwise + // swap `CURRENT_THREAD_SLOT` to the new interpreter while leaving the + // outer interpreter's attach state inconsistent. Always restore, even + // if initialization panics. + #[cfg(feature = "threading")] + let _restore_parent = { + let saved = thread::current_vm_is_set().then(thread::save_current_thread); + scopeguard::guard(saved, |saved| { + if let Some(saved) = saved { + thread::restore_current_thread(saved); + } + }) + }; + + let (vm, global_state) = initialize_vm( + InitializeVmOpts { + // settings unused when parent_state is Some + settings: Settings::default(), + ctx: self.vm.ctx.clone(), + module_defs: Vec::new(), + frozen_modules: Vec::new(), + init_hooks: Vec::new(), + is_main: false, + whence: InterpreterWhence::Stdlib, + parent_state: Some(&self.global_state), + }, + |_| {}, + ); + Self { global_state, vm } + } + /// Run a function with the main virtual machine and return a PyResult of the result. /// /// To enter vm context multiple times or to avoid buffer/exception management, this function is preferred. @@ -448,7 +621,7 @@ impl Interpreter { vm.state.finalizing.store(true, Ordering::Release); // GC pass - collect cycles before module cleanup - crate::gc_state::gc_state().collect_force(2); + vm.state.gc.collect_force(2); // Module finalization: remove modules from sys.modules, GC collect // (while builtins is still available for __del__), then clear module dicts. @@ -459,11 +632,18 @@ impl Interpreter { } // Match CPython: if exit_code is 0 and stdout flush failed, exit 120 - if exit_code == 0 && flush_status < 0 { + let exit_code = if exit_code == 0 && flush_status < 0 { EXITCODE_FLUSH_FAILURE } else { exit_code - } + }; + + // Daemon threads may still exist, so use the safe `process()`, + // not `drain_all()`. + #[cfg(feature = "threading")] + crate::object::qsbr::QSBR.process(); + + exit_code }) } } @@ -567,8 +747,9 @@ fn core_frozen_inits() -> impl Iterator { mod tests { use super::*; use crate::{ - PyObjectRef, + AsObject, PyObjectRef, builtins::{PyStr, int}, + vm::{MAIN_INTERPRETER_ID, runtime}, }; use malachite_bigint::ToBigInt; @@ -593,4 +774,883 @@ mod tests { assert_eq!(value.as_wtf8(), "Hello Hello Hello Hello ") }) } + + /// Main interpreter is marked main with Runtime whence and is registered. + #[test] + fn main_interpreter_identity() { + let main = Interpreter::without_stdlib(Default::default()); + assert!(main.is_main()); + assert_eq!(main.whence(), InterpreterWhence::Runtime); + assert!( + runtime::list_interpreters() + .iter() + .any(|info| info.id == main.id() && info.whence == InterpreterWhence::Runtime) + ); + // When this is the sole sequential main in a quiet process, id is 0; + // under parallel tests the id is still unique and registered. + assert!(main.id() >= MAIN_INTERPRETER_ID); + } + + /// Subinterpreters get distinct ids, Stdlib whence, and appear in the registry. + #[test] + fn create_subinterpreter_registers_distinct_ids() { + let main = Interpreter::without_stdlib(Default::default()); + let sub1 = main.create_subinterpreter(); + let sub2 = main.create_subinterpreter(); + + assert!(main.is_main()); + assert!(!sub1.is_main()); + assert!(!sub2.is_main()); + assert_eq!(sub1.whence(), InterpreterWhence::Stdlib); + assert_eq!(sub2.whence(), InterpreterWhence::Stdlib); + assert_ne!(main.id(), sub1.id()); + assert_ne!(main.id(), sub2.id()); + assert_ne!(sub1.id(), sub2.id()); + + let ids: Vec = runtime::list_interpreters() + .into_iter() + .map(|i| i.id) + .collect(); + assert!(ids.contains(&main.id())); + assert!(ids.contains(&sub1.id())); + assert!(ids.contains(&sub2.id())); + } + + /// An interpreter stays looked-up-able until nothing holds its state. + /// + /// Dropping the handle is not the end of its life: `new_thread()` workers + /// hold their own reference, and a collection in progress holds one for + /// every live interpreter while the world is stopped. So the registry entry + /// goes away eventually rather than at the drop. + fn wait_until_unregistered(id: i64) { + use core::time::Duration; + use std::time::Instant; + + let deadline = Instant::now() + Duration::from_secs(30); + while runtime::lookup_interpreter(id).is_some() { + assert!( + Instant::now() < deadline, + "interpreter {id} still registered long after its last reference" + ); + std::thread::yield_now(); + } + } + + /// A collection snapshots the registry and then reads tracked objects with + /// the interpreters it found parked. An interpreter that registered inside + /// that window would be missing from the snapshot, so nothing would stop it + /// and its bootstrap would run under the scan; registration therefore waits + /// for the stop to end. + #[cfg(feature = "threading")] + #[test] + fn registering_waits_for_an_in_flight_stop() { + use core::time::Duration; + use std::sync::mpsc; + + // Stands in for a collector between its snapshot and its restart. + let admission = runtime::lock_admission_for_stop(); + + let (tx, rx) = mpsc::channel(); + let worker = std::thread::spawn(move || { + let interp = Interpreter::without_stdlib(Default::default()); + tx.send(interp.id()).expect("receiver is alive"); + interp + }); + + assert!( + matches!( + rx.recv_timeout(Duration::from_millis(200)), + Err(mpsc::RecvTimeoutError::Timeout) + ), + "an interpreter registered while a stop-the-world was in flight" + ); + + drop(admission); + let id = rx + .recv_timeout(Duration::from_secs(30)) + .expect("registration proceeds once the world restarts"); + assert!(runtime::lookup_interpreter(id).is_some()); + drop(worker.join().expect("worker did not panic")); + wait_until_unregistered(id); + } + + /// Dropping a subinterpreter releases it; main remains. + #[test] + fn drop_subinterpreter_unregisters() { + let main = Interpreter::without_stdlib(Default::default()); + let sub_id = { + let sub = main.create_subinterpreter(); + let id = sub.id(); + assert!(runtime::lookup_interpreter(id).is_some()); + id + }; + wait_until_unregistered(sub_id); + assert!(runtime::lookup_interpreter(main.id()).is_some()); + } + + /// Each interpreter has its own `sys.modules` / builtins module instance. + #[test] + fn subinterpreters_isolate_modules() { + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + let (main_sys_ptr, main_builtins_ptr, main_ctx_ptr, main_state_ptr) = main.enter(|vm| { + ( + vm.sys_module.as_object() as *const _, + vm.builtins.as_object() as *const _, + PyRc::as_ptr(&vm.ctx), + PyRc::as_ptr(&vm.state), + ) + }); + let (sub_sys_ptr, sub_builtins_ptr, sub_ctx_ptr, sub_state_ptr) = sub.enter(|vm| { + ( + vm.sys_module.as_object() as *const _, + vm.builtins.as_object() as *const _, + PyRc::as_ptr(&vm.ctx), + PyRc::as_ptr(&vm.state), + ) + }); + + assert_ne!(main_sys_ptr, sub_sys_ptr); + assert_ne!(main_builtins_ptr, sub_builtins_ptr); + // Distinct per-interpreter state. + assert_ne!(main_state_ptr, sub_state_ptr); + // Shared process-wide type context (immortal / builtin types). + assert_eq!(main_ctx_ptr, sub_ctx_ptr); + } + + /// Mutations to interpreter-owned modules must not leak between interpreters. + #[test] + fn subinterpreters_behaviorally_isolate_builtins_and_sys_modules() { + const PROBE: &str = "__rustpython_subinterpreter_isolation_probe__"; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + main.enter(|vm| { + vm.builtins + .set_attr(PROBE, vm.ctx.new_int(11_i32), vm) + .unwrap(); + vm.sys_module + .get_attr("modules", vm) + .unwrap() + .set_item(PROBE, vm.ctx.new_int(12_i32).into(), vm) + .unwrap(); + }); + + sub.enter(|vm| { + assert!(vm.builtins.get_attr(PROBE, vm).is_err()); + let modules = vm.sys_module.get_attr("modules", vm).unwrap(); + assert!(modules.get_item(PROBE, vm).is_err()); + + vm.builtins + .set_attr(PROBE, vm.ctx.new_int(21_i32), vm) + .unwrap(); + modules + .set_item(PROBE, vm.ctx.new_int(22_i32).into(), vm) + .unwrap(); + }); + + main.enter(|vm| { + let builtin_probe = vm.builtins.get_attr(PROBE, vm).unwrap(); + assert_eq!(*int::get_value(&builtin_probe), 11_i32.to_bigint().unwrap()); + + let module_probe = vm + .sys_module + .get_attr("modules", vm) + .unwrap() + .get_item(PROBE, vm) + .unwrap(); + assert_eq!(*int::get_value(&module_probe), 12_i32.to_bigint().unwrap()); + }); + } + + /// Creating a subinterpreter while the parent is entered must not corrupt + /// the parent's current-VM / thread-slot state. + #[test] + fn create_subinterpreter_while_parent_entered() { + let main = Interpreter::without_stdlib(Default::default()); + main.enter(|vm| { + let before = vm.state.interpreter_id; + let sub = main.create_subinterpreter(); + assert_ne!(sub.id(), before); + // Still the parent after create returns. + assert_eq!(vm.state.interpreter_id, before); + // Can still use the parent VM. + let n: PyObjectRef = vm.ctx.new_int(7_i32).into(); + assert_eq!(int::get_value(&n), &7_i32.to_bigint().unwrap()); + // And the sub is independently usable after parent section. + drop(sub); + }); + } + + /// Sequential enter of main then sub on the same OS thread is safe. + #[test] + fn sequential_enter_main_and_sub() { + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + main.enter(|vm| { + assert!(vm.state.is_main_interpreter()); + let a: PyObjectRef = vm.ctx.new_int(1_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(2_i32).into(); + let res = vm._add(&a, &b).unwrap(); + assert_eq!(*int::get_value(&res), 3_i32.to_bigint().unwrap()); + }); + sub.enter(|vm| { + assert!(!vm.state.is_main_interpreter()); + let a: PyObjectRef = vm.ctx.new_int(10_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(5_i32).into(); + let res = vm._mul(&a, &b).unwrap(); + assert_eq!(*int::get_value(&res), 50_i32.to_bigint().unwrap()); + }); + // Re-enter main after sub. + main.enter(|vm| { + assert!(vm.state.is_main_interpreter()); + }); + } + + /// Concurrent use of main + subinterpreter on different OS threads. + #[cfg(feature = "threading")] + #[test] + fn concurrent_main_and_subinterpreter_threads() { + use alloc::sync::Arc; + use core::sync::atomic::{AtomicUsize, Ordering}; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let counter = Arc::new(AtomicUsize::new(0)); + + let c1 = Arc::clone(&counter); + let h_main = main.enter(|vm| { + let thread_vm = vm.new_thread(); + let c = Arc::clone(&c1); + std::thread::spawn(move || { + thread_vm.run(|vm| { + for _ in 0..100 { + let a: PyObjectRef = vm.ctx.new_int(1_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(1_i32).into(); + let _ = vm._add(&a, &b).unwrap(); + c.fetch_add(1, Ordering::Relaxed); + } + assert!(vm.state.is_main_interpreter()); + }); + }) + }); + + let c2 = Arc::clone(&counter); + let h_sub = sub.enter(|vm| { + let thread_vm = vm.new_thread(); + let c = Arc::clone(&c2); + std::thread::spawn(move || { + thread_vm.run(|vm| { + for _ in 0..100 { + let a: PyObjectRef = vm.ctx.new_int(2_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(3_i32).into(); + let _ = vm._mul(&a, &b).unwrap(); + c.fetch_add(1, Ordering::Relaxed); + } + assert!(!vm.state.is_main_interpreter()); + }); + }) + }); + + h_main.join().expect("main worker panicked"); + h_sub.join().expect("sub worker panicked"); + assert_eq!(counter.load(Ordering::Relaxed), 200); + } + + /// Entering one interpreter must not serialize entry into another interpreter. + #[cfg(feature = "threading")] + #[test] + fn main_and_subinterpreter_run_sections_overlap() { + use alloc::sync::Arc; + use core::time::Duration; + use std::{ + sync::{Condvar, Mutex}, + time::Instant, + }; + + #[derive(Default)] + struct OverlapState { + entered: usize, + release: bool, + } + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let state = Arc::new((Mutex::new(OverlapState::default()), Condvar::new())); + + let spawn_worker = |interpreter: &Interpreter| { + let state = Arc::clone(&state); + interpreter.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + thread_vm.run(|vm| { + let a: PyObjectRef = vm.ctx.new_int(20_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(22_i32).into(); + assert_eq!( + *int::get_value(&vm._add(&a, &b).unwrap()), + 42_i32.to_bigint().unwrap() + ); + + let (lock, ready) = &*state; + let mut state = lock.lock().unwrap(); + state.entered += 1; + ready.notify_all(); + while !state.release { + state = ready.wait(state).unwrap(); + } + }); + }) + }) + }; + + let main_worker = spawn_worker(&main); + let sub_worker = spawn_worker(&sub); + + let (lock, ready) = &*state; + let deadline = Instant::now() + Duration::from_secs(30); + let mut state_guard = lock.lock().unwrap(); + while state_guard.entered < 2 { + let now = Instant::now(); + if now >= deadline { + break; + } + let (next, _) = ready.wait_timeout(state_guard, deadline - now).unwrap(); + state_guard = next; + } + let overlapped = state_guard.entered == 2; + state_guard.release = true; + ready.notify_all(); + drop(state_guard); + + main_worker.join().expect("main worker panicked"); + sub_worker.join().expect("subinterpreter worker panicked"); + assert!( + overlapped, + "main and subinterpreter run sections were serialized" + ); + } + + /// A busy interpreter must not prevent another interpreter from making progress. + #[cfg(feature = "threading")] + #[test] + fn busy_main_interpreter_does_not_block_subinterpreter() { + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + use std::time::Instant; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let main_started = Arc::new(AtomicBool::new(false)); + let sub_finished = Arc::new(AtomicBool::new(false)); + + let main_started_worker = Arc::clone(&main_started); + let sub_finished_worker = Arc::clone(&sub_finished); + let main_worker = main.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + thread_vm.run(|vm| { + main_started_worker.store(true, Ordering::Release); + let deadline = Instant::now() + Duration::from_secs(30); + let mut operations = 0; + while !sub_finished_worker.load(Ordering::Acquire) && Instant::now() < deadline + { + let a: PyObjectRef = vm.ctx.new_int(20_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(22_i32).into(); + let result = vm._add(&a, &b).unwrap(); + assert_eq!(*int::get_value(&result), 42_i32.to_bigint().unwrap()); + operations += 1; + std::thread::yield_now(); + } + (sub_finished_worker.load(Ordering::Acquire), operations) + }) + }) + }); + + let main_started_worker = Arc::clone(&main_started); + let sub_finished_worker = Arc::clone(&sub_finished); + let sub_worker = sub.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + while !main_started_worker.load(Ordering::Acquire) { + std::thread::yield_now(); + } + thread_vm.run(|vm| { + let a: PyObjectRef = vm.ctx.new_int(6_i32).into(); + let b: PyObjectRef = vm.ctx.new_int(7_i32).into(); + let result = vm._mul(&a, &b).unwrap(); + assert_eq!(*int::get_value(&result), 42_i32.to_bigint().unwrap()); + sub_finished_worker.store(true, Ordering::Release); + }); + }) + }); + + let (sub_progressed_while_main_was_busy, main_operations) = + main_worker.join().expect("main worker panicked"); + sub_worker.join().expect("subinterpreter worker panicked"); + + assert!(main_operations > 0); + assert!( + sub_progressed_while_main_was_busy, + "subinterpreter made no progress until the busy main interpreter exited" + ); + } + + /// `new_thread` on a subinterpreter shares that subinterpreter's state, not main's. + #[cfg(feature = "threading")] + #[test] + fn subinterpreter_new_thread_shares_sub_state() { + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let sub_id = sub.id(); + + let handle = sub.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + thread_vm.run(|vm| { + assert_eq!(vm.state.interpreter_id, sub_id); + assert!(!vm.state.is_main_interpreter()); + }); + }) + }); + handle.join().expect("thread panicked"); + } + + /// Multiple subinterpreters can each run bytecode via compile+exec. + #[cfg(feature = "rustpython-compiler")] + #[test] + fn subinterpreter_runs_python_code() { + use crate::compiler::Mode; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + sub.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + let source = "x = 40 + 2\n"; + let code = vm + .compile(source, Mode::Exec, "") + .map_err(|err| err.into_pyexception(vm, Some(source))) + .unwrap(); + vm.run_code_obj(code, scope.clone()).unwrap(); + let x = scope.globals.get_item("x", vm).unwrap(); + assert_eq!(*int::get_value(&x), 42_i32.to_bigint().unwrap()); + }); + } + + /// Subclassing a shared type records the subclass on an object every + /// interpreter reaches, but only the interpreter that created it lists it. + fn run(vm: &VirtualMachine, scope: &crate::scope::Scope, source: &str) { + let code = vm + .compile(source, crate::compiler::Mode::Exec, "") + .map_err(|err| err.into_pyexception(vm, Some(source))) + .unwrap(); + vm.run_code_obj(code, scope.clone()).unwrap(); + } + + #[test] + fn subinterpreter_subclasses_are_scoped_to_their_interpreter() { + use crate::scope::Scope; + + fn lists_subclass(vm: &VirtualMachine, scope: &Scope, name: &str) -> bool { + run( + vm, + scope, + &format!("found = any(c.__name__ == {name:?} for c in int.__subclasses__())\n"), + ); + let found = scope.globals.get_item("found", vm).unwrap(); + found.try_to_bool(vm).unwrap() + } + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + // The scopes are what keep the classes alive; a subclass list holds + // only weak references, so both must outlive every assertion below. + let main_scope = main.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + run(vm, &scope, "class MainOnly(int): pass\n"); + scope + }); + let sub_scope = sub.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + run(vm, &scope, "class SubOnly(int): pass\n"); + scope + }); + + main.enter(|vm| { + assert!(lists_subclass(vm, &main_scope, "MainOnly")); + assert!(!lists_subclass(vm, &main_scope, "SubOnly")); + // A subclass built before either interpreter existed belongs to the + // shared context, so it stays visible to both. + assert!(lists_subclass(vm, &main_scope, "bool")); + }); + sub.enter(|vm| { + assert!(lists_subclass(vm, &sub_scope, "SubOnly")); + assert!(!lists_subclass(vm, &sub_scope, "MainOnly")); + assert!(lists_subclass(vm, &sub_scope, "bool")); + }); + + main.enter(|_| drop(main_scope)); + sub.enter(|_| drop(sub_scope)); + } + + /// A cycle allocated in one interpreter is not the parent's to collect. + #[test] + fn collections_only_reach_the_collecting_interpreter() { + use core::time::Duration; + use std::time::Instant; + + const CYCLE: &str = "class Node:\n pass\n\ + a = Node()\n\ + b = Node()\n\ + a.other = b\n\ + b.other = a\n\ + del a\n\ + del b\n"; + + fn live_nodes(vm: &VirtualMachine) -> usize { + vm.state + .gc + .get_objects(None) + .iter() + .filter(|obj| &*obj.class().name() == "Node") + .count() + } + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + let sub_scope = sub.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + run(vm, &scope, CYCLE); + assert_eq!(live_nodes(vm), 2); + scope + }); + + // A collection in the parent walks its own tracked objects and leaves + // the sub's cycle where it is. Collections are serialized process-wide + // by a `try_lock`, so one running elsewhere in the suite makes + // `collect_force` a no-op; retry until this one gets to run. Each retry + // waits outside `enter`, since a thread that is entered but not running + // bytecode never reaches a safepoint, and the collection this is + // waiting for cannot stop it. + let deadline = Instant::now() + Duration::from_secs(30); + while !main.enter(|vm| vm.state.gc.collect_force(2).candidates > 0) { + assert!( + Instant::now() < deadline, + "no collection ran in the parent interpreter" + ); + std::thread::sleep(Duration::from_millis(5)); + } + sub.enter(|vm| assert_eq!(live_nodes(vm), 2)); + + sub.enter(|_| drop(sub_scope)); + } + + /// And it is not the parent's to enumerate either. + #[test] + fn get_objects_only_reports_the_calling_interpreter() { + fn tracks_class(vm: &VirtualMachine, name: &str) -> bool { + vm.state + .gc + .get_objects(None) + .iter() + .any(|obj| &*obj.class().name() == name) + } + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + + let main_scope = main.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + run(vm, &scope, "class MainNode:\n pass\nkeep = MainNode()\n"); + scope + }); + let sub_scope = sub.enter(|vm| { + let scope = vm.new_scope_with_builtins(); + run(vm, &scope, "class SubNode:\n pass\nkeep = SubNode()\n"); + scope + }); + + main.enter(|vm| { + assert!(tracks_class(vm, "MainNode")); + assert!(!tracks_class(vm, "SubNode")); + }); + sub.enter(|vm| { + assert!(tracks_class(vm, "SubNode")); + assert!(!tracks_class(vm, "MainNode")); + }); + + main.enter(|_| drop(main_scope)); + sub.enter(|_| drop(sub_scope)); + } + + /// The runtime can own a subinterpreter by id and hand it back on destroy. + #[cfg(feature = "threading")] + #[test] + fn runtime_owned_interpreter_lifecycle() { + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let id = sub.id(); + + assert_eq!(runtime::store_owned_interpreter(sub), id); + assert!(runtime::is_owned_interpreter(id)); + assert!(runtime::lookup_interpreter(id).is_some()); + // The owned table is process-global and other tests store into it in + // parallel, so only this entry's own membership is deterministic. + assert!(runtime::owned_interpreter_count() >= 1); + + // Reclaiming removes ownership but keeps the interpreter alive while the + // returned handle is held. + let reclaimed = runtime::take_owned_interpreter(id).expect("owned by runtime"); + assert_eq!(reclaimed.id(), id); + assert!(!runtime::is_owned_interpreter(id)); + assert!(runtime::lookup_interpreter(id).is_some()); + assert!(runtime::take_owned_interpreter(id).is_none()); + + // Dropping the reclaimed handle releases it. + drop(reclaimed); + wait_until_unregistered(id); + } + + /// `create_owned_subinterpreter` stores the sub and returns only its id. + #[cfg(feature = "threading")] + #[test] + fn create_owned_subinterpreter_returns_id() { + let main = Interpreter::without_stdlib(Default::default()); + let id = main.create_owned_subinterpreter(); + assert!(runtime::is_owned_interpreter(id)); + assert_ne!(id, main.id()); + + let sub = runtime::take_owned_interpreter(id).expect("owned by runtime"); + assert_eq!(sub.id(), id); + assert!(!sub.is_main()); + } + + /// A collection must stop every interpreter, not just the collecting one: + /// the generation lists are process-global, so the reachability walk reads + /// objects owned by other interpreters while their threads would otherwise + /// still be mutating them. + #[cfg(all(feature = "threading", feature = "rustpython-compiler"))] + #[test] + fn gc_collect_is_safe_while_another_interpreter_runs() { + use crate::compiler::Mode; + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + use std::time::Instant; + + // Each interpreter churns reference cycles so both contribute tracked + // objects to the shared generation lists. + const CHURN: &str = "\ +for _ in range(40): + a = {} + b = {'peer': a} + a['peer'] = b +"; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let stop = Arc::new(AtomicBool::new(false)); + + let run_source = |vm: &VirtualMachine, source: &str| { + let scope = vm.new_scope_with_builtins(); + let code = vm + .compile(source, Mode::Exec, "") + .map_err(|err| err.into_pyexception(vm, Some(source))) + .unwrap(); + vm.run_code_obj(code, scope).unwrap(); + }; + + // Subinterpreter thread: allocate cycles continuously. + let stop_worker = Arc::clone(&stop); + let churner = sub.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + thread_vm.run(|vm| { + while !stop_worker.load(Ordering::Acquire) { + run_source(vm, CHURN); + } + }); + }) + }); + + // Main interpreter: force collections while the sub keeps mutating. + main.enter(|vm| { + run_source(vm, CHURN); + let deadline = Instant::now() + Duration::from_secs(2); + let mut collections = 0; + while Instant::now() < deadline && collections < 20 { + vm.state.gc.collect_force(2); + collections += 1; + } + assert!(collections > 0); + }); + + stop.store(true, Ordering::Release); + churner.join().expect("churn worker panicked"); + } + + /// A thread entered in one interpreter can park another interpreter's + /// threads. This is what makes a collection safe: the generation lists are + /// process-global, so the collector must be able to stop every interpreter, + /// not only its own. + #[cfg(all(feature = "threading", feature = "rustpython-compiler"))] + #[test] + fn stop_the_world_parks_threads_of_another_interpreter() { + use crate::compiler::Mode; + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, AtomicU64, Ordering}, + time::Duration, + }; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let sub_state = sub.enter(|vm| vm.state.clone()); + + let progress = Arc::new(AtomicU64::new(0)); + let stop = Arc::new(AtomicBool::new(false)); + + // Sub-interpreter worker: runs bytecode (so it reaches safepoints) and + // reports progress every iteration. + let progress_worker = Arc::clone(&progress); + let stop_worker = Arc::clone(&stop); + let worker = sub.enter(|vm| { + let thread_vm = vm.new_thread(); + std::thread::spawn(move || { + thread_vm.run(|vm| { + let source = "x = 1 + 1\n"; + let code = vm + .compile(source, Mode::Exec, "") + .map_err(|err| err.into_pyexception(vm, Some(source))) + .unwrap(); + while !stop_worker.load(Ordering::Acquire) { + let scope = vm.new_scope_with_builtins(); + vm.run_code_obj(code.clone(), scope).unwrap(); + progress_worker.fetch_add(1, Ordering::Release); + } + }); + }) + }); + + // Wait until the worker is actually running. + while progress.load(Ordering::Acquire) == 0 { + std::thread::yield_now(); + } + + main.enter(|_vm| { + // Stop the *subinterpreter* from a thread whose current interpreter + // is main — the cross-interpreter stop a collection performs. + sub_state.stop_the_world.stop_the_world(&sub_state); + + let parked_at = progress.load(Ordering::Acquire); + std::thread::sleep(Duration::from_millis(50)); + assert_eq!( + progress.load(Ordering::Acquire), + parked_at, + "subinterpreter thread kept running while its world was stopped" + ); + + sub_state.stop_the_world.start_the_world(&sub_state); + }); + + // After restart the worker makes progress again. + let resumed_from = progress.load(Ordering::Acquire); + while progress.load(Ordering::Acquire) == resumed_from { + std::thread::yield_now(); + } + + stop.store(true, Ordering::Release); + worker.join().expect("worker panicked"); + } + + /// Entering a subinterpreter from inside the parent's `enter` must attach + /// the subinterpreter's thread slot (and detach the parent's). Otherwise the + /// thread runs the sub's bytecode with a DETACHED slot, and a collector + /// stopping that interpreter force-parks the slot and wrongly concludes the + /// world is stopped while this thread keeps mutating objects. + #[cfg(all(feature = "threading", feature = "rustpython-compiler"))] + #[test] + fn nested_enter_of_subinterpreter_is_stoppable() { + use crate::compiler::Mode; + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, AtomicU64, Ordering}, + time::Duration, + }; + + let main = Interpreter::without_stdlib(Default::default()); + let sub = main.create_subinterpreter(); + let sub_state = sub.enter(|vm| vm.state.clone()); + + let progress = Arc::new(AtomicU64::new(0)); + let stop = Arc::new(AtomicBool::new(false)); + + // Worker runs the SUB nested inside an active MAIN section. + let progress_worker = Arc::clone(&progress); + let stop_worker = Arc::clone(&stop); + let main_vm = main.enter(|vm| vm.new_thread()); + let sub_vm = sub.enter(|vm| vm.new_thread()); + let worker = std::thread::spawn(move || { + main_vm.run(|_main| { + sub_vm.run(|vm| { + let source = "x = 1 + 1\n"; + let code = vm + .compile(source, Mode::Exec, "") + .map_err(|err| err.into_pyexception(vm, Some(source))) + .unwrap(); + while !stop_worker.load(Ordering::Acquire) { + let scope = vm.new_scope_with_builtins(); + vm.run_code_obj(code.clone(), scope).unwrap(); + progress_worker.fetch_add(1, Ordering::Release); + } + }); + }); + }); + + while progress.load(Ordering::Acquire) == 0 { + std::thread::yield_now(); + } + + sub_state.stop_the_world.stop_the_world(&sub_state); + let parked_at = progress.load(Ordering::Acquire); + std::thread::sleep(Duration::from_millis(50)); + assert_eq!( + progress.load(Ordering::Acquire), + parked_at, + "nested subinterpreter thread kept running while the sub's world was stopped" + ); + sub_state.stop_the_world.start_the_world(&sub_state); + + let resumed_from = progress.load(Ordering::Acquire); + while progress.load(Ordering::Acquire) == resumed_from { + std::thread::yield_now(); + } + + stop.store(true, Ordering::Release); + worker.join().expect("nested worker panicked"); + } + + /// The process main id is recorded once and is stable across later creates. + #[test] + fn process_main_id_recorded_and_stable() { + // At least one main exists by now (this one, if not an earlier test), so + // `get_main()` is populated. + let main = Interpreter::without_stdlib(Default::default()); + let recorded = runtime::main_interpreter_id().expect("a process main exists"); + + // Recording is once-only: further interpreters do not displace it. + let _sub = main.create_subinterpreter(); + let _main2 = Interpreter::without_stdlib(Default::default()); + assert_eq!(runtime::main_interpreter_id(), Some(recorded)); + } } diff --git a/crates/vm/src/vm/method.rs b/crates/vm/src/vm/method.rs index 9e4f7185552..2beeb95ceb5 100644 --- a/crates/vm/src/vm/method.rs +++ b/crates/vm/src/vm/method.rs @@ -6,7 +6,7 @@ use crate::{ builtins::{PyBaseObject, PyStr, PyStrInterned, descriptor::PyMethodDescriptor}, function::{IntoFuncArgs, PyMethodFlags}, object::{AsObject, Py, PyObject, PyObjectRef, PyResult}, - types::PyTypeFlags, + types::{GetattroFunc, PyTypeFlags, fn_addr}, }; #[derive(Debug)] @@ -22,7 +22,7 @@ impl PyMethod { pub(crate) fn get(obj: PyObjectRef, name: &Py, vm: &VirtualMachine) -> PyResult { let cls = obj.class(); let getattro = cls.slots.getattro.load().unwrap(); - if getattro as usize != PyBaseObject::getattro as *const () as usize { + if fn_addr(getattro) != fn_addr(PyBaseObject::getattro as GetattroFunc) { return obj.get_attr(name, vm).map(Self::Attribute); } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index eb6546c02fe..54d3e813eec 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -5,11 +5,15 @@ #[cfg(feature = "rustpython-compiler")] mod compile; +pub(crate) mod compile_mode; +#[cfg(feature = "rustpython-compiler")] +pub use compile::VmCompileError; mod context; mod interpreter; mod method; #[cfg(feature = "rustpython-compiler")] mod python_run; +pub mod runtime; mod setting; pub mod thread; mod vm_new; @@ -30,7 +34,7 @@ use crate::{ common::{hash::HashSecret, lock::PyMutex, rc::PyRc}, convert::ToPyObject, exceptions::types::PyBaseException, - frame::{ExecutionResult, Frame, FrameRef}, + frame::{ExecutionResult, FrameObject, FrameObjectRef}, frozen::FrozenModule, function::{ArgMapping, FuncArgs, PySetterValue}, import, @@ -41,11 +45,12 @@ use crate::{ warn::WarningsState, }; use alloc::{borrow::Cow, collections::BTreeMap}; -#[cfg(all(unix, feature = "threading"))] +#[cfg(all(not(unix), feature = "threading"))] +use core::ptr::NonNull; +#[cfg(feature = "threading")] use core::sync::atomic::AtomicI64; use core::{ cell::{Cell, OnceCell, RefCell}, - ptr::NonNull, sync::atomic::{AtomicBool, AtomicU64, Ordering}, }; use crossbeam_utils::atomic::AtomicCell; @@ -57,21 +62,26 @@ use std::{ pub use context::Context; pub use interpreter::{Interpreter, InterpreterBuilder}; pub(crate) use method::PyMethod; +pub use runtime::{InterpreterInfo, InterpreterWhence, MAIN_INTERPRETER_ID}; pub use setting::{CheckHashPycsMode, Paths, PyConfig, Settings}; pub const MAX_MEMORY_SIZE: usize = isize::MAX as usize; // Objects are live when they are on stack, or referenced by a name (for now) -/// Top level container of a python virtual machine. In theory you could -/// create more instances of this struct and have them operate fully isolated. +/// Per-thread execution context for a single interpreter (≈ CPython `PyThreadState`). +/// +/// A `VirtualMachine` holds thread-local eval state (exceptions, recursion, frames, +/// datastack) plus shared references to interpreter-owned data (`state`, +/// `builtins`, `sys_module`, `ctx`). Multiple VMs may share the same +/// [`PyGlobalState`] via `VirtualMachine::new_thread`; distinct interpreters +/// each have their own `PyGlobalState` (see [`Interpreter::create_subinterpreter`]). /// -/// To construct this, please refer to the [`Interpreter`] +/// To construct the main VM of an interpreter, use [`Interpreter`]. pub struct VirtualMachine { pub builtins: PyRef, pub sys_module: PyRef, pub ctx: PyRc, - pub frames: RefCell>, /// Thread-local data stack for bump-allocating frame-local data /// (localsplus arrays for non-generator frames). datastack: core::cell::UnsafeCell, @@ -82,6 +92,7 @@ pub struct VirtualMachine { pub profile_func: RefCell, pub trace_func: RefCell, pub use_tracing: Cell, + tracing_depth: Cell, pub recursion_limit: Cell, pub(crate) signal_handlers: OnceCell, pub(crate) signal_rx: Option, @@ -89,6 +100,11 @@ pub struct VirtualMachine { pub state: PyRc, pub initialized: bool, recursion_depth: Cell, + /// Depth of native recursion that pushes no Python frame, counted only + /// where the stack pointer cannot be read. Everywhere else the native + /// stack itself answers, and nothing needs counting. + #[cfg(any(miri, target_env = "musl"))] + native_recursion_depth: Cell, /// C stack soft limit for detecting stack overflow (like c_stack_soft_limit) #[cfg_attr(any(miri, target_env = "musl"), allow(dead_code))] c_stack_soft_limit: Cell, @@ -102,24 +118,39 @@ pub struct VirtualMachine { pub asyncio_running_task: RefCell>, pub(crate) callable_cache: CallableCache, pub(crate) audit_hooks: RefCell>, + /// Side channel for TailCall: the bytecode loop stores the new frame + /// pointer here before returning `ExecutionResult::TailCall`. + /// Access only via `set_pending_tailcall` / `take_pending_tailcall`. + pending_tailcall_frame: Cell>, + /// Owned reference that keeps callee raw pointers valid during TailCall. + /// Set by the exact-call handlers and moved into the trampoline's + /// `SuspendedFrame`. Uses UnsafeCell because the VM is per-thread and this + /// field is only accessed on the owning thread. + pending_tailcall_owner: core::cell::UnsafeCell>, } -/// Non-owning frame pointer for the frames stack. +/// Non-owning frame pointer for the non-unix threading frames stack. /// The pointed-to frame is kept alive by the caller of with_frame/resume_gen_frame. +/// Unix threading builds publish the top frame through `ThreadSlot::top_frame` +/// and walk the rest via `FrameObject::previous`, so they do not use this type. +#[cfg(all(not(unix), feature = "threading"))] #[derive(Copy, Clone)] -pub struct FramePtr(NonNull>); +pub struct FramePtr(NonNull>); +#[cfg(all(not(unix), feature = "threading"))] impl FramePtr { /// # Safety /// The pointed-to frame must still be alive. #[must_use] - pub unsafe fn as_ref(&self) -> &Py { + pub unsafe fn as_ref(&self) -> &Py { unsafe { self.0.as_ref() } } } -// SAFETY: FramePtr is only stored in the VM's frames Vec while the corresponding -// FrameRef is alive on the call stack. The Vec is always empty when the VM moves between threads. +// SAFETY: FramePtr is only stored in a thread's shared frame stack +// (`ThreadSlot::frames`) while the corresponding FrameObjectRef is alive on that +// thread's call stack; readers dereference it under the slot mutex. +#[cfg(all(not(unix), feature = "threading"))] unsafe impl Send for FramePtr {} #[derive(Debug)] @@ -139,7 +170,7 @@ impl Default for ExceptionStack { /// Stop-the-world state for fork safety. Before `fork()`, the requester /// stops all other Python threads so they are not holding internal locks. -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub struct StopTheWorldState { /// Fast-path flag checked in the bytecode loop (like `_PY_EVAL_PLEASE_STOP_BIT`) pub(crate) requested: AtomicBool, @@ -147,6 +178,11 @@ pub struct StopTheWorldState { world_stopped: AtomicBool, /// Ident of the thread that requested the stop (like `stw->requester`) requester: AtomicU64, + /// Single exclusion held for the whole stop→start span. Fork and GC are + /// both stop-the-world requesters driving this shared state; only one may + /// hold it at a time. Acquired before any stop bookkeeping (see + /// `acquire_exclusion`) and released by `start_the_world`/`reset_after_fork`. + exclusion: AtomicBool, /// Signaled by suspending threads when their state transitions to SUSPENDED notify_mutex: std::sync::Mutex<()>, notify_cv: std::sync::Condvar, @@ -174,7 +210,7 @@ pub struct StopTheWorldState { stats_suspend_wait_yields: AtomicU64, } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] #[derive(Debug, Clone, Copy)] pub struct StopTheWorldStats { pub stop_calls: u64, @@ -190,14 +226,14 @@ pub struct StopTheWorldStats { pub world_stopped: bool, } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] impl Default for StopTheWorldState { fn default() -> Self { Self::new() } } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] impl StopTheWorldState { #[must_use] pub const fn new() -> Self { @@ -205,6 +241,7 @@ impl StopTheWorldState { requested: AtomicBool::new(false), world_stopped: AtomicBool::new(false), requester: AtomicU64::new(0), + exclusion: AtomicBool::new(false), notify_mutex: std::sync::Mutex::new(()), notify_cv: std::sync::Condvar::new(), thread_countdown: AtomicI64::new(0), @@ -232,9 +269,9 @@ impl StopTheWorldState { } #[inline] - fn init_thread_countdown(&self, vm: &VirtualMachine) -> i64 { + fn init_thread_countdown(&self, state: &PyGlobalState) -> i64 { let requester = self.requester.load(Ordering::Relaxed); - let registry = vm.state.thread_frames.lock(); + let registry = state.thread_frames.lock(); // Keep requested/count initialization serialized with thread-slot // registration (which also takes this lock), matching the // HEAD_LOCK-guarded stop-the-world bookkeeping. @@ -263,16 +300,22 @@ impl StopTheWorldState { /// Try to CAS detached threads directly to SUSPENDED and check whether /// stop countdown reached zero after parking detached threads. - fn park_detached_threads(&self, vm: &VirtualMachine) -> bool { + fn park_detached_threads(&self, state: &PyGlobalState) -> bool { use thread::{THREAD_ATTACHED, THREAD_DETACHED, THREAD_SUSPENDED}; let requester = self.requester.load(Ordering::Relaxed); - let registry = vm.state.thread_frames.lock(); + let registry = state.thread_frames.lock(); let mut attached_seen = 0u64; let mut forced_parks = 0u64; + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&id, slot) in registry.iter() { if id == requester { continue; } + let state = slot.state.load(Ordering::Relaxed); if state == THREAD_DETACHED { // CAS DETACHED → SUSPENDED (park without thread cooperation) @@ -326,23 +369,79 @@ impl StopTheWorldState { forced_parks != 0 && self.thread_countdown.load(Ordering::Acquire) == 0 } + /// Acquire the single stop-the-world exclusion in a park-friendly way. + /// + /// Fork and GC both request stop-the-world through the same shared state; + /// without this exclusion their `requester`/`requested`/countdown words + /// could be clobbered by an interleaving requester, so the completion + /// check could never converge and a requester would wait on itself forever. + /// + /// The acquire must be park-friendly. While another requester's stop is in + /// progress it sets this thread's stop bit and waits for it to suspend; + /// blocking on a plain lock here would keep this thread from ever reaching + /// that safepoint, so the active requester would wait for this thread while + /// this thread waits for the lock — a deadlock swap. Instead we poll and + /// honor the suspend request between tries. Suspending here is safe as long + /// as any lock a spinning requester still holds is never acquired + /// attached-blocking by another thread. The fork requester holds IMP_LOCK, + /// but its acquisition detaches (`allow_threads`), so no attached thread + /// blocks on it; the GC requester holds only the `collecting` mutex, which + /// is only ever `try_lock`'d. The active requester therefore force-parks + /// this thread, finishes its whole stop→start span, releases the exclusion, + /// and only then does this thread resume and acquire it. + fn acquire_exclusion(&self, state: &PyGlobalState) { + if self + .exclusion + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed) + .is_ok() + { + return; + } + loop { + crate::vm::thread::suspend_if_needed(state); + std::thread::yield_now(); + if self + .exclusion + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed) + .is_ok() + { + return; + } + } + } + + /// Release the stop-the-world exclusion taken by `acquire_exclusion`. + fn release_exclusion(&self) { + self.exclusion.store(false, Ordering::Release); + } + /// Stop all non-requester threads (`stop_the_world`). /// /// 1. Sets `requested`, marking the requester thread. /// 2. CAS detached threads to SUSPENDED. /// 3. Waits (polling with 1 ms condvar timeout) for attached threads /// to self-suspend in `check_signals`. - pub fn stop_the_world(&self, vm: &VirtualMachine) { + /// + /// Takes the shared exclusion first so at most one requester (fork or GC) + /// drives the stop→start span at a time; it is released by + /// `start_the_world`/`reset_after_fork`. + pub fn stop_the_world(&self, state: &PyGlobalState) { + self.acquire_exclusion(state); let start = std::time::Instant::now(); let requester_ident = crate::stdlib::_thread::get_ident(); self.requester.store(requester_ident, Ordering::Relaxed); self.stats_stop_calls.fetch_add(1, Ordering::Relaxed); - let initial_countdown = self.init_thread_countdown(vm); + let initial_countdown = self.init_thread_countdown(state); stw_trace(format_args!("stop begin requester={requester_ident}")); - if initial_countdown == 0 { + // Park detached threads and set stop bits, then confirm every other + // thread is SUSPENDED. The completion condition is level-triggered + // (`all_non_requester_suspended`) so an already-suspended thread that + // was counted but will not notify again cannot stall the stop. + self.park_detached_threads(state); + if initial_countdown == 0 || self.all_non_requester_suspended(state) { self.world_stopped.store(true, Ordering::Release); #[cfg(debug_assertions)] - self.debug_assert_all_non_requester_suspended(vm); + self.debug_assert_all_non_requester_suspended(state); stw_trace(format_args!( "stop end requester={requester_ident} wait_ns=0 polls=0" )); @@ -351,7 +450,8 @@ impl StopTheWorldState { let mut polls = 0u64; loop { - if self.park_detached_threads(vm) { + self.park_detached_threads(state); + if self.all_non_requester_suspended(state) { break; } polls = polls.saturating_add(1); @@ -359,8 +459,7 @@ impl StopTheWorldState { // Re-check under the wait mutex first to avoid a lost-wake race: // a thread may have suspended and notified right before we enter wait. let guard = self.notify_mutex.lock().unwrap(); - if self.thread_countdown.load(Ordering::Acquire) == 0 || self.park_detached_threads(vm) - { + if self.all_non_requester_suspended(state) { drop(guard); break; } @@ -389,18 +488,18 @@ impl StopTheWorldState { } self.world_stopped.store(true, Ordering::Release); #[cfg(debug_assertions)] - self.debug_assert_all_non_requester_suspended(vm); + self.debug_assert_all_non_requester_suspended(state); stw_trace(format_args!( "stop end requester={requester_ident} wait_ns={wait_ns} polls={polls}" )); } /// Resume all suspended threads (`start_the_world`). - pub fn start_the_world(&self, vm: &VirtualMachine) { + pub fn start_the_world(&self, state: &PyGlobalState) { use thread::{THREAD_DETACHED, THREAD_SUSPENDED}; let requester = self.requester.load(Ordering::Relaxed); stw_trace(format_args!("start begin requester={requester}")); - let registry = vm.state.thread_frames.lock(); + let registry = state.thread_frames.lock(); // Clear the request flag BEFORE waking threads. Otherwise a thread // returning from allow_threads → attach_thread could observe // `requested == true`, re-suspend itself, and stay parked forever. @@ -408,10 +507,16 @@ impl StopTheWorldState { // thread-slot initialization. self.requested.store(false, Ordering::Release); self.world_stopped.store(false, Ordering::Release); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&id, slot) in registry.iter() { if id == requester { continue; } + slot.stop_requested.store(false, Ordering::Release); let state = slot.state.load(Ordering::Relaxed); debug_assert!( @@ -423,11 +528,15 @@ impl StopTheWorldState { slot.thread.unpark(); } } + drop(registry); self.thread_countdown.store(0, Ordering::Release); self.requester.store(0, Ordering::Relaxed); #[cfg(debug_assertions)] - self.debug_assert_all_non_requester_detached(vm); + self.debug_assert_all_non_requester_detached(state); + // Release the exclusion last, ending the stop→start span so the next + // requester (fork or GC) can proceed. + self.release_exclusion(); stw_trace(format_args!("start end requester={requester}")); } @@ -437,6 +546,9 @@ impl StopTheWorldState { self.world_stopped.store(false, Ordering::Relaxed); self.requester.store(0, Ordering::Relaxed); self.thread_countdown.store(0, Ordering::Relaxed); + // The surviving child thread inherited the exclusion taken by the + // pre-fork `stop_the_world`; release it (no start_the_world runs here). + self.release_exclusion(); stw_trace(format_args!("reset-after-fork")); } @@ -497,15 +609,48 @@ impl StopTheWorldState { } } + /// Whether every non-requester registered thread is currently SUSPENDED. + /// + /// Level-triggered stop-the-world completion check. Relying on this rather + /// than solely on the edge-triggered `thread_countdown` avoids a + /// lost-decrement race under rapid back-to-back stops: a thread that is + /// already SUSPENDED when a new stop counts it neither notifies nor is + /// force-parked again, so an edge-based countdown could never reach zero. + fn all_non_requester_suspended(&self, state: &PyGlobalState) -> bool { + use thread::THREAD_SUSPENDED; + let requester = self.requester.load(Ordering::Relaxed); + let registry = state.thread_frames.lock(); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for (&id, slot) in registry.iter() { + if id == requester { + continue; + } + if slot.state.load(Ordering::Acquire) != THREAD_SUSPENDED { + return false; + } + } + true + } + #[cfg(debug_assertions)] - fn debug_assert_all_non_requester_suspended(&self, vm: &VirtualMachine) { + fn debug_assert_all_non_requester_suspended(&self, state: &PyGlobalState) { use thread::THREAD_SUSPENDED; let requester = self.requester.load(Ordering::Relaxed); - let registry = vm.state.thread_frames.lock(); + let registry = state.thread_frames.lock(); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&id, slot) in registry.iter() { if id == requester { continue; } + let state = slot.state.load(Ordering::Relaxed); debug_assert!( state == THREAD_SUSPENDED, @@ -515,14 +660,20 @@ impl StopTheWorldState { } #[cfg(debug_assertions)] - fn debug_assert_all_non_requester_detached(&self, vm: &VirtualMachine) { + fn debug_assert_all_non_requester_detached(&self, state: &PyGlobalState) { use thread::THREAD_SUSPENDED; let requester = self.requester.load(Ordering::Relaxed); - let registry = vm.state.thread_frames.lock(); + let registry = state.thread_frames.lock(); + + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] for (&id, slot) in registry.iter() { if id == requester { continue; } + let state = slot.state.load(Ordering::Relaxed); debug_assert!( state != THREAD_SUSPENDED, @@ -532,13 +683,13 @@ impl StopTheWorldState { } } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub(super) fn stw_trace_enabled() -> bool { static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); *ENABLED.get_or_init(|| crate::host_env::os::var_os("RUSTPYTHON_STW_TRACE").is_some()) } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub(super) fn stw_trace(msg: core::fmt::Arguments<'_>) { if stw_trace_enabled() { use core::fmt::Write as _; @@ -574,7 +725,13 @@ pub(super) fn stw_trace(msg: core::fmt::Arguments<'_>) { crate::stdlib::_thread::get_ident(), msg ); + #[cfg(unix)] crate::host_env::io::write_stderr_raw(&out.buf[..out.len]); + #[cfg(not(unix))] + { + use std::io::Write as _; + let _ = std::io::stderr().write_all(&out.buf[..out.len]); + } } } @@ -587,14 +744,30 @@ pub(crate) struct CallableCache { pub builtin_any: Option, } +/// Per-interpreter shared state (≈ CPython `PyInterpreterState`). +/// +/// Not process-global: each [`Interpreter`] (main or subinterpreter) owns its own +/// `PyGlobalState`. Process-wide pieces live elsewhere (`Context::genesis`, +/// GC, the interpreter registry in [`runtime`]). pub struct PyGlobalState { + /// Unique process-global interpreter id (main is [`MAIN_INTERPRETER_ID`]). + pub interpreter_id: i64, + /// How this interpreter was created. + pub whence: runtime::InterpreterWhence, + /// True for every top-level (non-sub) interpreter, each of which keeps its + /// own signal and main-thread bookkeeping. Only the first one registered + /// becomes *the* process main — see [`runtime::main_interpreter_id`]. + pub is_main: bool, pub config: PyConfig, pub module_defs: BTreeMap<&'static str, &'static builtins::PyModuleDef>, pub frozen: HashMap<&'static str, FrozenModule, rapidhash::quality::RandomState>, pub stacksize: AtomicCell, pub thread_count: AtomicCell, pub hash_secret: HashSecret, - pub atexit_funcs: PyMutex>>, + /// Registered `atexit` callbacks, newest first. Shared ownership so + /// `atexit.unregister` can keep the entry it is comparing alive while the + /// list is unlocked, and still recognize it afterwards by identity. + pub atexit_funcs: PyMutex>>, pub codec_registry: CodecsRegistry, pub finalizing: AtomicBool, pub warnings: WarningsState, @@ -608,6 +781,8 @@ pub struct PyGlobalState { pub global_trace_func: PyMutex>, /// Global profile function for all threads (set by sys._setprofileallthreads) pub global_profile_func: PyMutex>, + /// Global type mutation/versioning mutex for CPython-style FT type operations. + pub type_mutex: PyMutex<()>, /// Main thread identifier (pthread_self on Unix) #[cfg(feature = "threading")] pub main_thread_ident: AtomicCell, @@ -628,8 +803,18 @@ pub struct PyGlobalState { /// local version against this to decide whether re-instrumentation is needed. pub instrumentation_version: AtomicU64, /// Stop-the-world state for pre-fork thread suspension - #[cfg(all(unix, feature = "threading"))] + #[cfg(feature = "threading")] pub stop_the_world: StopTheWorldState, + /// This interpreter's garbage collector policy and results. + pub gc: crate::gc_state::GcInterpreterState, +} + +impl PyGlobalState { + #[inline] + #[must_use] + pub fn is_main_interpreter(&self) -> bool { + self.is_main + } } pub fn process_hash_secret_seed() -> u32 { @@ -639,6 +824,61 @@ pub fn process_hash_secret_seed() -> u32 { *SEED.get_or_init(|| u32::from_ne_bytes(rustpython_common::rand::os_random())) } +/// A `NonNull` wrapper that implements `Send + Sync`. +/// +/// # Safety contract +/// +/// This type bypasses Rust's `Send`/`Sync` bounds on `NonNull`. It is +/// sound **only** when the pointer is exclusively accessed by one thread +/// at a time. In this codebase, that invariant is upheld because +/// `VirtualMachine` is per-thread. +/// +/// **Do not use this type outside `pending_tailcall_frame`.** It exists +/// solely to let a `Cell>` field on the per-thread +/// VM satisfy `Send + Sync`. If you need a `Send`-able pointer +/// elsewhere, justify and document the safety invariant at that site. +#[repr(transparent)] +struct PendingFrame(core::ptr::NonNull); + +impl Copy for PendingFrame {} +impl Clone for PendingFrame { + fn clone(&self) -> Self { + *self + } +} + +// SAFETY: VirtualMachine is per-thread; the pointer is only ever +// accessed on the thread that wrote it. The pointed-to InterpreterFrame +// lives on that thread's datastack and is valid from set to take. +unsafe impl Send for PendingFrame {} +unsafe impl Sync for PendingFrame {} + +/// Saved state from `enter_iframe`, needed by `exit_iframe` to restore +/// the previous frame chain and exception state. +pub(crate) struct IframeEntryState { + pub(crate) iframe_ptr: *const crate::frame::InterpreterFrame, + pub(crate) old_chain: *const crate::frame::InterpreterFrame, + pub(crate) saved_exc: Option, + pub(crate) save_exc: bool, +} + +/// Caller frame suspended by a TailCall in the trampoline. +struct SuspendedFrame { + iframe: *mut crate::frame::InterpreterFrame, + entry_state: IframeEntryState, + /// Function that owns the callee's raw pointers (code, globals, builtins, + /// closure, and func_obj). Moved from `vm.pending_tailcall_owner` when the + /// callee's TailCall is consumed. + /// Dropped as soon as this SuspendedFrame is popped — the callee has + /// returned or raised and its frame is already released by then. + callee_owner: PyObjectRef, + /// True for the initial frame passed into the trampoline by the caller. + /// The caller owns the datastack allocation for the entry frame, so the + /// trampoline must NOT release it — only callee-allocated frames are + /// released here. + is_entry: bool, +} + impl VirtualMachine { fn init_callable_cache(&mut self) -> PyResult<()> { self.callable_cache.len = Some(self.builtins.get_attr("len", self)?); @@ -664,6 +904,13 @@ impl VirtualMachine { unsafe { (*self.datastack.get()).push(size) } } + /// Bump-allocate a full frame, returning whether the same cleared LIFO + /// block and size were reused. + #[inline(always)] + pub(crate) fn datastack_push_frame(&self, size: usize) -> (*mut u8, bool) { + unsafe { (*self.datastack.get()).push_frame(size) } + } + /// Check whether the thread data stack currently has room for `size` bytes. #[inline(always)] pub(crate) fn datastack_has_space(&self, size: usize) -> bool { @@ -680,6 +927,12 @@ impl VirtualMachine { unsafe { (*self.datastack.get()).pop(base) } } + /// Pop a full frame after its localsplus slots have been cleared. + #[inline(always)] + pub(crate) unsafe fn datastack_pop_frame(&self, base: *mut u8, size: usize) { + unsafe { (*self.datastack.get()).pop_frame(base, size) } + } + /// Temporarily detach the current thread (ATTACHED → DETACHED) while /// running `f`, then re-attach afterwards. Allows `stop_the_world` to /// park this thread during blocking syscalls. @@ -730,7 +983,6 @@ impl VirtualMachine { builtins, sys_module, ctx, - frames: RefCell::new(vec![]), datastack: core::cell::UnsafeCell::new(crate::datastack::DataStack::new()), wasm_id: None, exceptions: RefCell::default(), @@ -739,6 +991,7 @@ impl VirtualMachine { profile_func, trace_func, use_tracing: Cell::new(false), + tracing_depth: Cell::new(0), recursion_limit: Cell::new(if cfg!(debug_assertions) { 256 } else { 1000 }), signal_handlers, signal_rx: None, @@ -746,6 +999,8 @@ impl VirtualMachine { state, initialized: false, recursion_depth: Cell::new(0), + #[cfg(any(miri, target_env = "musl"))] + native_recursion_depth: Cell::new(0), c_stack_soft_limit: Cell::new(Self::calculate_c_stack_soft_limit()), async_gen_firstiter: RefCell::new(None), async_gen_finalizer: RefCell::new(None), @@ -753,6 +1008,8 @@ impl VirtualMachine { asyncio_running_task: RefCell::new(None), callable_cache: CallableCache::default(), audit_hooks: RefCell::new(vec![]), + pending_tailcall_frame: Cell::new(None), + pending_tailcall_owner: core::cell::UnsafeCell::new(None), }; if vm.state.hash_secret.hash_str("") @@ -878,13 +1135,14 @@ impl VirtualMachine { fn initialize(&mut self) { flame_guard!("init VirtualMachine"); - if self.initialized { - panic!("Double Initialize Error"); - } + assert!(!self.initialized, "Double Initialize Error"); - // Initialize main thread ident before any threading operations + // Process main-thread identity is owned by the main interpreter only + // (used for signal handling / `_thread._is_main_interpreter` helpers). #[cfg(feature = "threading")] - stdlib::_thread::init_main_thread_ident(self); + if self.state.is_main_interpreter() { + stdlib::_thread::init_main_thread_ident(self); + } stdlib::builtins::init_module(self, &self.builtins); let callable_cache_init = self.init_callable_cache(); @@ -1095,30 +1353,22 @@ impl VirtualMachine { } pub fn run_code_obj(&self, code: PyRef, scope: Scope) -> PyResult { - use crate::builtins::{PyFunction, PyModule}; - - // Create a function object for module code, similar to CPython's PyEval_EvalCode - let func = PyFunction::new(code.clone(), scope.globals.clone(), self)?; - let func_obj = func.into_ref(&self.ctx).into(); + self.run_code_obj_with_closure(code, scope, None) + } - // Extract builtins from globals["__builtins__"], like PyEval_EvalCode - let builtins = match scope - .globals - .get_item_opt(identifier!(self, __builtins__), self)? - { - Some(b) => { - if let Some(module) = b.downcast_ref::() { - module.dict().into() - } else { - b - } - } - None => self.builtins.dict().into(), - }; + pub(crate) fn run_code_obj_with_closure( + &self, + code: PyRef, + scope: Scope, + closure: Option>>, + ) -> PyResult { + use crate::builtins::PyFunction; - let frame = - Frame::new(code, scope, builtins, &[], Some(func_obj), false, self).into_ref(&self.ctx); - self.run_frame(frame) + // Create a function object for module code, similar to PyEval_EvalCode + let mut func = PyFunction::new(code, scope.globals.clone(), self)?; + func.closure = closure; + let func = func.into_ref(&self.ctx); + func.invoke_with_locals(FuncArgs::default(), scope.locals, self) } #[cold] @@ -1168,18 +1418,24 @@ impl VirtualMachine { } }; - let msg_str = if let Some(msg) = msg { - format!("{msg}: ") + if self.is_none(object) { + if let Some(msg) = msg { + write_to_stderr(&format!("{msg}:\n"), &stderr, self); + } } else { - "Exception ignored in: ".to_owned() - }; - write_to_stderr(&msg_str, &stderr, self); + let msg_str = if let Some(msg) = msg { + format!("{msg}: ") + } else { + "Exception ignored in: ".to_owned() + }; + write_to_stderr(&msg_str, &stderr, self); - let repr_result = object.repr(self); - let repr_wtf8 = repr_result - .as_ref() - .map_or_else(|_| "".as_ref(), |s| s.as_wtf8()); - write_to_stderr(&format!("{repr_wtf8}\n"), &stderr, self); + let repr_result = object.repr(self); + let repr_wtf8 = repr_result + .as_ref() + .map_or_else(|_| "".as_ref(), |s| s.as_wtf8()); + write_to_stderr(&format!("{repr_wtf8}\n"), &stderr, self); + } // Write exception type and message let exc_type_name = e.class().name(); @@ -1197,8 +1453,324 @@ impl VirtualMachine { } } + /// Store a callee frame pointer for the trampoline to pick up after + /// `TailCall` is returned. The pointed-to InterpreterFrame must live + /// on the current thread's datastack and remain valid until the + /// trampoline calls `take_pending_tailcall`. #[inline(always)] - pub fn run_frame(&self, frame: FrameRef) -> PyResult { + pub(crate) fn set_pending_tailcall(&self, iframe: &mut crate::frame::InterpreterFrame) { + self.pending_tailcall_frame + .set(Some(PendingFrame(core::ptr::NonNull::from(iframe)))); + } + + /// Store the function that owns the fields borrowed by the pending callee. + #[inline(always)] + pub(crate) fn set_pending_tailcall_owner(&self, owner: PyObjectRef) { + let slot = unsafe { &mut *self.pending_tailcall_owner.get() }; + debug_assert!(slot.is_none(), "pending TailCall owner was not consumed"); + *slot = Some(owner); + } + + /// Take the pending callee owner, resetting the side channel. + #[inline(always)] + fn take_pending_tailcall_owner(&self) -> PyObjectRef { + unsafe { &mut *self.pending_tailcall_owner.get() } + .take() + .expect("TailCall without pending owner") + } + + /// Take the pending tailcall frame pointer, resetting the side channel. + #[inline(always)] + fn take_pending_tailcall(&self) -> *mut crate::frame::InterpreterFrame { + self.pending_tailcall_frame + .take() + .expect("TailCall without pending frame") + .0 + .as_ptr() + } + + #[inline(always)] + /// Run a stack-allocated InterpreterFrame without heap allocation. + /// Uses a trampoline loop to flatten Python-to-Python calls: when the + /// bytecode loop returns `TailCall`, the trampoline swaps to the new + /// frame without adding a Rust stack frame. + pub fn run_frame_fast(&self, iframe: &mut crate::frame::InterpreterFrame) -> PyResult { + use crate::frame::ExecutionResult; + + let entry_state = self.enter_iframe(iframe)?; + let result = crate::frame::run_iframe(iframe, self); + + match result { + Ok(ExecutionResult::Return(value)) => { + self.exit_iframe(entry_state); + Ok(value) + } + Ok(ExecutionResult::TailCall) => self.run_frame_fast_trampoline(iframe, entry_state), + Ok(ExecutionResult::Yield(_)) => panic!("Yield in non-generator frame"), + Err(exc) => { + self.exit_iframe(entry_state); + Err(exc) + } + } + } + + /// Cold path: at least one TailCall was issued. Run the trampoline. + /// All frame dispatch happens in this single loop — no mutual recursion + /// between helper functions, so C stack depth is bounded. + #[cold] + #[inline(never)] + fn run_frame_fast_trampoline( + &self, + iframe: &mut crate::frame::InterpreterFrame, + entry_state: IframeEntryState, + ) -> PyResult { + use crate::frame::ExecutionResult; + + let mut frame_stack: Vec = Vec::with_capacity(8); + + // What we need to do next. + enum Action { + /// Enter and run a new callee frame (pointer from pending_tailcall_frame). + EnterCallee(*mut crate::frame::InterpreterFrame), + /// Push a return value onto the next caller and re-enter it. + ReturnValue(PyObjectRef), + /// Propagate an exception through suspended callers. + Unwind(PyBaseExceptionRef), + } + + let initial_ptr = self.take_pending_tailcall(); + let initial_owner = self.take_pending_tailcall_owner(); + frame_stack.push(SuspendedFrame { + iframe: iframe as *mut crate::frame::InterpreterFrame, + entry_state, + callee_owner: initial_owner, + is_entry: true, + }); + let mut action = Action::EnterCallee(initial_ptr); + + loop { + match action { + Action::EnterCallee(callee_ptr) => { + let callee = unsafe { &mut *callee_ptr }; + let callee_entry = match self.enter_iframe_unchecked(callee) { + Ok(state) => state, + Err(exc) => { + unsafe { + if let Some((base, size)) = callee.release_datastack_frame() { + self.datastack_pop_frame(base, size); + } + } + action = Action::Unwind(exc); + continue; + } + }; + + let result = crate::frame::run_iframe(callee, self); + match result { + Ok(ExecutionResult::TailCall) => { + let callee_owner = self.take_pending_tailcall_owner(); + frame_stack.push(SuspendedFrame { + iframe: callee_ptr, + entry_state: callee_entry, + callee_owner, + is_entry: false, + }); + action = Action::EnterCallee(self.take_pending_tailcall()); + } + Ok(ExecutionResult::Return(value)) => { + self.exit_iframe(callee_entry); + unsafe { + if let Some((base, size)) = callee.release_datastack_frame() { + self.datastack_pop_frame(base, size); + } + } + action = Action::ReturnValue(value); + } + Ok(ExecutionResult::Yield(_)) => panic!("Yield in non-generator frame"), + Err(exc) => { + self.exit_iframe(callee_entry); + unsafe { + if let Some((base, size)) = callee.release_datastack_frame() { + self.datastack_pop_frame(base, size); + } + } + action = Action::Unwind(exc); + } + } + } + + Action::ReturnValue(value) => { + let Some(caller) = frame_stack.pop() else { + // All frames consumed — this is the final return. + return Ok(value); + }; + let SuspendedFrame { + iframe: caller_iframe_ptr, + entry_state: caller_entry, + callee_owner, + is_entry: caller_is_entry, + } = caller; + // The callee's frame was released before this action was + // formed, and a materialized frame object holds its own + // references, so nothing borrows the callee's function any + // more. Release it here, at the callee's return, rather than + // holding it across the caller's next stretch of bytecode. + drop(callee_owner); + let caller_iframe = unsafe { &mut *caller_iframe_ptr }; + caller_iframe.localsplus.push_stack(value); + + let result = crate::frame::run_iframe(caller_iframe, self); + match result { + Ok(ExecutionResult::TailCall) => { + let next_callee_owner = self.take_pending_tailcall_owner(); + frame_stack.push(SuspendedFrame { + iframe: caller_iframe_ptr, + entry_state: caller_entry, + callee_owner: next_callee_owner, + is_entry: caller_is_entry, + }); + action = Action::EnterCallee(self.take_pending_tailcall()); + } + Ok(ExecutionResult::Return(value)) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::ReturnValue(value); + } + Ok(ExecutionResult::Yield(_)) => panic!("Yield in non-generator frame"), + Err(exc) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::Unwind(exc); + } + } + } + + Action::Unwind(exc) => { + let Some(caller) = frame_stack.pop() else { + return Err(exc); + }; + let SuspendedFrame { + iframe: caller_iframe_ptr, + entry_state: caller_entry, + callee_owner, + is_entry: caller_is_entry, + } = caller; + // Released at the callee's return, for the same reason as + // in `ReturnValue`: the exception carries owned references + // through its traceback, not borrows into the callee frame. + drop(callee_owner); + let caller_iframe = unsafe { &mut *caller_iframe_ptr }; + + let handled = + crate::frame::trampoline_handle_exception(caller_iframe, &exc, self); + + match handled { + Ok(None) => { + // Handler found — resume the caller's dispatch loop. + let result = crate::frame::run_iframe(caller_iframe, self); + match result { + Ok(ExecutionResult::TailCall) => { + let next_callee_owner = self.take_pending_tailcall_owner(); + frame_stack.push(SuspendedFrame { + iframe: caller_iframe_ptr, + entry_state: caller_entry, + callee_owner: next_callee_owner, + is_entry: caller_is_entry, + }); + action = Action::EnterCallee(self.take_pending_tailcall()); + } + Ok(ExecutionResult::Return(value)) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::ReturnValue(value); + } + Ok(ExecutionResult::Yield(_)) => { + panic!("Yield in non-generator frame") + } + Err(new_exc) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::Unwind(new_exc); + } + } + } + Ok(Some(ExecutionResult::Return(value))) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::ReturnValue(value); + } + Ok(Some(_)) => { + panic!("Unexpected execution result in trampoline unwind") + } + Err(new_exc) => { + self.exit_iframe(caller_entry); + if !caller_is_entry { + unsafe { + if let Some((base, size)) = + caller_iframe.release_datastack_frame() + { + self.datastack_pop_frame(base, size); + } + } + } + action = Action::Unwind(new_exc); + } + } + } + } + } + } + + pub fn run_frame(&self, frame: FrameObjectRef) -> PyResult { + // Only ordinary (datastack) call frames reach `run_frame`; generator + // and coroutine frames are resumed through `resume_gen_frame`. A + // datastack frame is created untracked and is tracked lazily only when + // it escapes, which happens no earlier than `release_datastack_frame` + // after this call returns. So it must be untracked on entry. + debug_assert!( + !frame.as_object().is_gc_tracked(), + "datastack frame is GC-tracked before execution" + ); match self.with_frame(frame, |f| f.run(self))? { ExecutionResult::Return(value) => Ok(value), _ => panic!("Got unexpected result from function"), @@ -1269,14 +1841,14 @@ impl VirtualMachine { // Phase 4: GC collect — modules removed from sys.modules are freed, // exposing cycles (e.g., dict ↔ function.__globals__). GC collects // these and calls __del__ while module dicts are still intact. - crate::gc_state::gc_state().collect_force(2); + self.state.gc.collect_force(2); // Phase 5: Clear module dicts in reverse import order using 2-pass algorithm. // Skip builtins and sys — those are cleared last. self.finalize_clear_module_dicts(&module_weakrefs); // Phase 6: GC collect — pick up anything freed by dict clearing. - crate::gc_state::gc_state().collect_force(2); + self.state.gc.collect_force(2); // Phase 7: Clear sys and builtins dicts last self.finalize_clear_sys_builtins_dict(); @@ -1434,9 +2006,21 @@ impl VirtualMachine { } /// Stack margin bytes (like _PyOS_STACK_MARGIN_BYTES). - /// 2048 * sizeof(void*) = 16KB for 64-bit. + /// The margin is doubled for debug/sanitized builds because frame + /// evaluation consumes more native stack in those configurations. #[cfg_attr(any(miri, target_env = "musl"), allow(dead_code))] - const STACK_MARGIN_BYTES: usize = 2048 * core::mem::size_of::(); + // 2× CPython's _PY_STACK_MARGIN_BYTES to account for both heavy and + // light frame native stack usage per recursion step. + const STACK_MARGIN_BYTES: usize = + (if cfg!(debug_assertions) { 16384 } else { 4096 }) * core::mem::size_of::(); + + /// How deep native recursion may go where the stack cannot be measured + /// (`Py_C_RECURSION_LIMIT`). A native step costs far more stack than a + /// Python one and debug builds cost more again, so this sits well under + /// what a default stack holds rather than at what it would just fit. + #[cfg(any(miri, target_env = "musl"))] + const NATIVE_RECURSION_LIMIT_UNMEASURED: usize = + if cfg!(debug_assertions) { 500 } else { 1500 }; /// Get the stack boundaries using platform-specific APIs. /// Returns (base, top) where base is the lowest address and top is the highest. @@ -1497,11 +2081,16 @@ impl VirtualMachine { } /// Calculate the C stack soft limit based on actual stack boundaries. - /// soft_limit = base + 2 * margin (for downward-growing stacks) + /// soft_limit = base + 2 * margin (for downward-growing stacks). + /// The margin is clamped to half the stack so threads created with a stack + /// smaller than 2 * (2 * margin) still get usable headroom instead of a + /// soft limit above their stack top (which would trip on entry). #[cfg(all(not(miri), not(target_env = "musl")))] fn calculate_c_stack_soft_limit() -> usize { - let (base, _top) = Self::get_stack_bounds(); - base + Self::STACK_MARGIN_BYTES * 2 + let (base, top) = Self::get_stack_bounds(); + let stack_size = top.saturating_sub(base); + let margin = (Self::STACK_MARGIN_BYTES * 2).min(stack_size / 2); + base + margin } /// Musl currently reports stack bounds in a way that trips the VM's @@ -1513,114 +2102,308 @@ impl VirtualMachine { } /// Check if we're near the C stack limit (like _Py_MakeRecCheck). - /// Returns true only when stack pointer is in the "danger zone" between - /// soft_limit and hard_limit (soft_limit - 2*margin). + /// One-sided: any stack pointer below the soft limit is in danger, since a + /// single native frame can exceed the margin and step past it. #[cfg(all(not(miri), not(target_env = "musl")))] #[inline(always)] - fn check_c_stack_overflow(&self) -> bool { + pub(crate) fn check_c_stack_overflow(&self) -> bool { let current_sp = psm::stack_pointer() as usize; let soft_limit = self.c_stack_soft_limit.get(); current_sp < soft_limit - && current_sp >= soft_limit.saturating_sub(Self::STACK_MARGIN_BYTES * 2) } /// Miri does not support the native stack probe, and musl currently trips /// the probe during stdlib bootstrap. #[cfg(any(miri, target_env = "musl"))] #[inline(always)] - fn check_c_stack_overflow(&self) -> bool { + pub(crate) fn check_c_stack_overflow(&self) -> bool { false } /// Used to run the body of a (possibly) recursive function. It will raise a /// RecursionError if recursive functions are nested far too many times, /// preventing a stack overflow. + /// `Py_EnterRecursiveCall`: bounds native recursion that pushes no Python + /// frame, against the native stack. That is a separate budget from the + /// frame limit `sys.setrecursionlimit()` sets, so nesting counted here does + /// not come out of what Python code has left to call with. pub fn with_recursion PyResult>(&self, _where: &str, f: F) -> PyResult { - self.check_recursive_call(_where)?; - - // Native stack guard: check C stack like _Py_MakeRecCheck - if self.check_c_stack_overflow() { - return Err(self.new_recursion_error(_where.to_string())); + // `check_c_stack_overflow()` answers no unconditionally where the stack + // pointer cannot be read, which would leave this guard with nothing to + // stop. A count of the nesting stands in for the measurement there. + #[cfg(any(miri, target_env = "musl"))] + let counted_too_deep = + self.native_recursion_depth.get() >= Self::NATIVE_RECURSION_LIMIT_UNMEASURED; + #[cfg(not(any(miri, target_env = "musl")))] + let counted_too_deep = false; + + if counted_too_deep || self.check_c_stack_overflow() { + return Err( + self.new_recursion_error(format!("maximum recursion depth exceeded {_where}")) + ); } - self.recursion_depth.update(|d| d + 1); - scopeguard::defer! { self.recursion_depth.update(|d| d - 1) } + #[cfg(any(miri, target_env = "musl"))] + let _native_depth_guard = { + self.native_recursion_depth.update(|d| d + 1); + scopeguard::guard((), |()| { + self.native_recursion_depth.update(|d| d.saturating_sub(1)) + }) + }; + f() } - pub fn with_frame PyResult>( + pub fn with_frame PyResult>( &self, - frame: FrameRef, + frame: FrameObjectRef, f: F, ) -> PyResult { - self.with_frame_impl(frame, true, f) + self.check_recursive_call("")?; + + // Check the native C stack periodically. The sampling interval + // (every 8th call) balances overhead against the risk of missing + // an overflow between checks, especially when light and heavy + // frames alternate (each recursion step uses different native + // stack amounts). + let depth = self.recursion_depth.get(); + if depth & 7 == 0 && self.check_c_stack_overflow() { + return Err(self.new_recursion_error(String::new())); + } + + self.recursion_depth.update(|d| d + 1); + // Decrement on all exit paths (including panic between here and + // the explicit decrement at the bottom). + let _depth_guard = scopeguard::guard((), |()| { + self.recursion_depth.update(|d| d.saturating_sub(1)) + }); + + #[cfg(all(not(unix), feature = "threading"))] + crate::vm::thread::push_thread_frame(FramePtr(NonNull::from(&*frame))); + let iframe = frame.iframe() as *const crate::frame::InterpreterFrame; + let old_chain = crate::vm::thread::set_current_frame(iframe); + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + frame + .iframe() + .previous + .store(old_chain as usize, core::sync::atomic::Ordering::Relaxed); + } + let save_exc = frame.iframe().code().has_exc_handling; + let saved_exc = if save_exc { + self.current_exception() + } else { + None + }; + let old_owner = frame.iframe().owner.swap( + crate::frame::FrameOwner::Thread as i8, + core::sync::atomic::Ordering::AcqRel, + ); + + let result = self.dispatch_traced_frame(&frame, |frame| f(frame.to_owned())); + + // Capture f_back before clearing previous so code holding a + // reference to this FrameObject can walk the chain after return. + if !old_chain.is_null() { + let strong = frame.as_object().strong_count(); + // Only set retained_back if someone else holds a reference (escaped) + // AND the caller already has a FrameObject. Materializing the caller + // here would add refcounts on its local variables, preventing timely + // __del__ / ResourceWarning on dealloc. If the caller hasn't been + // materialized, f_back will resolve via the TLS chain while the + // caller is still executing, or return None after it returns. + if strong > 1 { + let mut guard = frame.iframe().cold().retained_back.lock(); + if guard.is_none() { + let prev_iframe = unsafe { &*old_chain }; + if let Some(fo) = prev_iframe.frame_obj() { + *guard = Some(fo.to_owned()); + } + } + } + } + + frame + .iframe() + .owner + .store(old_owner, core::sync::atomic::Ordering::Release); + if save_exc { + self.restore_exception(saved_exc); + } + // Clear previous before popping — it may point to a stack-allocated + // iframe that will be freed when the caller releases its frame. + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + frame + .iframe() + .previous + .store(0, core::sync::atomic::Ordering::Relaxed); + } + let _ = crate::vm::thread::set_current_frame(old_chain); + #[cfg(all(not(unix), feature = "threading"))] + crate::vm::thread::pop_thread_frame(); + // Disarm the panic guard — normal decrement. + scopeguard::ScopeGuard::into_inner(_depth_guard); + self.recursion_depth.update(|d| d - 1); + + result } - pub(crate) fn with_frame_untraced PyResult>( + /// Push `iframe` onto the frame chain: recursion/C-stack check, TLS + /// link, exception save. Returns the saved state needed by + /// `exit_iframe`. + #[inline] + pub(crate) fn enter_iframe( &self, - frame: FrameRef, - f: F, - ) -> PyResult { - self.with_frame_impl(frame, false, f) + iframe: &mut crate::frame::InterpreterFrame, + ) -> PyResult { + self.check_recursive_call("")?; + + let depth = self.recursion_depth.get(); + if depth & 7 == 0 && self.check_c_stack_overflow() { + return Err(self.new_recursion_error(String::new())); + } + + self.enter_iframe_unchecked(iframe) } - fn with_frame_impl PyResult>( + /// Like `enter_iframe` but skips the Python recursion depth check + /// (already verified by `specialization_call_recursion_guard`). + /// Still checks C-stack overflow since each `run_iframe` call + /// consumes Rust stack space. + #[inline(always)] + pub(crate) fn enter_iframe_unchecked( &self, - frame: FrameRef, - traced: bool, - f: F, - ) -> PyResult { - self.with_recursion("", || { - // SAFETY: `frame` (FrameRef) stays alive for the entire closure scope, - // keeping the FramePtr valid. We pass a clone to `f` so that `f` - // consuming its FrameRef doesn't invalidate our pointer. - let fp = FramePtr(NonNull::from(&*frame)); - self.frames.borrow_mut().push(fp); - // Update the shared frame stack for sys._current_frames() and faulthandler - #[cfg(feature = "threading")] - crate::vm::thread::push_thread_frame(fp); - // Link frame into the signal-safe frame chain (previous pointer) - let old_frame = crate::vm::thread::set_current_frame((&**frame) as *const Frame); - frame.previous.store( - old_frame as *mut Frame, - core::sync::atomic::Ordering::Relaxed, - ); - // Normal frame calls share the caller's exc_info slot so that - // callees can see the caller's handled exception via sys.exc_info(). - // Save the current value to restore on exit — this prevents - // exc_info pollution from frames with unbalanced - // PUSH_EXC_INFO/POP_EXCEPT (e.g., exception escaping an except block - // whose cleanup entry is missing from the exception table). - let saved_exc = self.current_exception(); - let old_owner = frame.owner.swap( - crate::frame::FrameOwner::Thread as i8, - core::sync::atomic::Ordering::AcqRel, - ); + iframe: &mut crate::frame::InterpreterFrame, + ) -> PyResult { + let depth = self.recursion_depth.get(); + if depth & 7 == 0 && self.check_c_stack_overflow() { + return Err(self.new_recursion_error(String::new())); + } + + self.recursion_depth.update(|d| d + 1); + + let iframe_ptr = iframe as *const crate::frame::InterpreterFrame; + let old_chain = crate::vm::thread::set_current_frame(iframe_ptr); + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + iframe + .previous + .store(old_chain as usize, core::sync::atomic::Ordering::Relaxed); + } + let save_exc = iframe.code().has_exc_handling; + let saved_exc = if save_exc { + self.current_exception() + } else { + None + }; + + Ok(IframeEntryState { + iframe_ptr, + old_chain, + saved_exc, + save_exc, + }) + } - // Ensure cleanup on panic: restore owner, exc_info, frame chain, and frames Vec. - scopeguard::defer! { - frame.owner.store(old_owner, core::sync::atomic::Ordering::Release); - self.set_exception(saved_exc); - crate::vm::thread::set_current_frame(old_frame); - self.frames.borrow_mut().pop(); - #[cfg(feature = "threading")] - crate::vm::thread::pop_thread_frame(); + /// Pop `iframe` from the frame chain: sync materialized state, restore + /// exception, TLS unlink, GC tracking. + pub(crate) fn exit_iframe(&self, state: IframeEntryState) { + let IframeEntryState { + iframe_ptr, + old_chain, + saved_exc, + save_exc, + } = state; + + // If this iframe was materialized, capture f_back so that code + // holding a reference to the FrameObject can walk the chain after + // return. Read materialized through read_volatile to bypass + // LLVM's noalias on the &mut iframe borrow. + { + let mat_ptr = unsafe { + let field_ptr = core::ptr::addr_of!((*iframe_ptr).materialized); + core::ptr::read_volatile(field_ptr as *const usize) + }; + if mat_ptr != 0 { + let fo = unsafe { &*(mat_ptr as *const crate::Py) }; + unsafe { + let live_iframe = &*iframe_ptr; + fo.iframe_mut() + .localsplus + .sync_fastlocals_from(&live_iframe.localsplus); + fo.iframe_mut().prev_line.set(live_iframe.prev_line.get()); + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + fo.iframe_mut().lasti.store( + live_iframe + .lasti + .load(core::sync::atomic::Ordering::Relaxed), + core::sync::atomic::Ordering::Relaxed, + ); + } + // The slots above are the last write this thread makes into + // the frame object, so it is now readable from anywhere. + fo.iframe().detach(); + if !old_chain.is_null() { + let prev_iframe = unsafe { &*old_chain }; + let back_fo = prev_iframe.materialize_chain(self); + *fo.iframe().cold().retained_back.lock() = Some(back_fo); + } + fo.iframe().owner.store( + crate::frame::FrameOwner::FrameObject as i8, + core::sync::atomic::Ordering::Release, + ); } + } - if traced { - self.dispatch_traced_frame(&frame, |frame| f(frame.to_owned())) - } else { - f(frame.to_owned()) + if save_exc { + self.restore_exception(saved_exc); + } + // Clear previous before popping — it may point to a stack-allocated + // iframe that will be freed when the caller releases its frame. + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + unsafe { + (*iframe_ptr) + .previous + .store(0, core::sync::atomic::Ordering::Relaxed); } - }) + } + let _ = crate::vm::thread::set_current_frame(old_chain); + self.recursion_depth.update(|d| d - 1); + + // Track the materialized FrameObject in GC and release + // temporary_refs after the frame is off the chain. + { + let mat_ptr = unsafe { + let field_ptr = core::ptr::addr_of!((*iframe_ptr).materialized); + core::ptr::read_volatile(field_ptr as *const usize) + }; + if mat_ptr != 0 { + let fo = unsafe { &*(mat_ptr as *const crate::Py) }; + unsafe { + crate::gc_state::gc_state().track_object( + core::ptr::NonNull::from(fo.as_object()), + crate::gc_state::current_owner(), + ); + let live_iframe = &*iframe_ptr; + live_iframe.cold().temporary_refs.lock().clear(); + } + } + } } - /// Frame execution for generator/coroutine resume. + /// FrameObject execution for generator/coroutine resume. /// Pushes a new exc_info slot (gi_exc_state) onto the chain, /// linking the generator's saved handled-exception. - pub fn resume_gen_frame) -> PyResult>( + pub fn resume_gen_frame) -> PyResult>( &self, - frame: &FrameRef, + frame: &FrameObjectRef, exc: Option, f: F, ) -> PyResult { @@ -1630,21 +2413,22 @@ impl VirtualMachine { } self.recursion_depth.update(|d| d + 1); - // SAFETY: frame (&FrameRef) stays alive for the duration, so NonNull is valid until pop. - let fp = FramePtr(NonNull::from(&**frame)); - self.frames.borrow_mut().push(fp); - #[cfg(feature = "threading")] - crate::vm::thread::push_thread_frame(fp); - let old_frame = crate::vm::thread::set_current_frame((&***frame) as *const Frame); - frame.previous.store( - old_frame as *mut Frame, - core::sync::atomic::Ordering::Relaxed, - ); + // SAFETY: frame (&FrameObjectRef) stays alive for the duration, so NonNull is valid until pop. + #[cfg(all(not(unix), feature = "threading"))] + crate::vm::thread::push_thread_frame(FramePtr(NonNull::from(&**frame))); + let iframe = frame.iframe() as *const crate::frame::InterpreterFrame; + let old_chain = crate::vm::thread::set_current_frame(iframe); + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + frame + .iframe() + .previous + .store(old_chain as usize, core::sync::atomic::Ordering::Relaxed); + } // Push generator's exc_info slot onto the chain - // (gi_exc_state.previous_item = tstate->exc_info; - // tstate->exc_info = &gi_exc_state;) self.push_exception(exc); - let old_owner = frame.owner.swap( + let old_owner = frame.iframe().owner.swap( crate::frame::FrameOwner::Thread as i8, core::sync::atomic::Ordering::AcqRel, ); @@ -1652,11 +2436,17 @@ impl VirtualMachine { // Ensure cleanup on panic: restore owner, pop exc_info slot, frame chain, // frames Vec, and recursion depth. scopeguard::defer! { - frame.owner.store(old_owner, core::sync::atomic::Ordering::Release); + frame.iframe().owner.store(old_owner, core::sync::atomic::Ordering::Release); self.pop_exception(); - crate::vm::thread::set_current_frame(old_frame); - self.frames.borrow_mut().pop(); - #[cfg(feature = "threading")] + // Clear previous before popping — it may point to a stack-allocated + // iframe that will be freed when the caller releases its frame. + { + #[allow(unused_imports)] + use rustpython_common::atomic::Radium; + frame.iframe().previous.store(0, core::sync::atomic::Ordering::Relaxed); + } + let _ = crate::vm::thread::set_current_frame(old_chain); + #[cfg(all(not(unix), feature = "threading"))] crate::vm::thread::pop_thread_frame(); self.recursion_depth.update(|d| d - 1); @@ -1674,9 +2464,9 @@ impl VirtualMachine { /// - Fire `TraceEvent::Return` on both normal return **and** exception /// unwind (`PY_UNWIND` → `PyTrace_RETURN` with `arg = None`). /// Propagate any trace-function error, replacing the original exception. - fn dispatch_traced_frame) -> PyResult>( + fn dispatch_traced_frame) -> PyResult>( &self, - frame: &Py, + frame: &Py, f: F, ) -> PyResult { use crate::protocol::TraceEvent; @@ -1684,7 +2474,7 @@ impl VirtualMachine { // Fire 'call' trace event. current_frame() now returns the callee. let trace_result = self.trace_event(TraceEvent::Call, None)?; if let Some(local_trace) = trace_result { - *frame.trace.lock() = local_trace; + *frame.iframe().cold().trace.lock() = Some(local_trace); } let result = f(frame); @@ -1693,7 +2483,11 @@ impl VirtualMachine { // PY_UNWIND fires PyTrace_RETURN with arg=None — so we fire for // both Ok and Err, matching `call_trace_protected` behavior. if self.use_tracing.get() - && (!self.is_none(&frame.trace.lock()) || !self.is_none(&self.profile_func.borrow())) + && (!self.is_none(&self.profile_func.borrow()) + || frame + .iframe() + .cold_opt() + .is_some_and(|c| c.trace.lock().is_some())) { let ret_result = self.trace_event(TraceEvent::Return, None); // call_trace_protected: if trace function raises, its error @@ -1709,11 +2503,33 @@ impl VirtualMachine { #[cfg(feature = "rustpython-codegen")] pub fn compile_opts(&self) -> crate::compiler::CompileOpts { crate::compiler::CompileOpts { - optimize: self.state.config.settings.optimize, + optimize: self.state.config.settings.optimize.min(2), debug_ranges: self.state.config.settings.code_debug_ranges, + int_max_str_digits: self.state.int_max_str_digits.load(), + allow_top_level_await: false, + future_features: crate::bytecode::CodeFlags::empty(), + dont_imply_dedent: false, + recursion_limit: self.recursion_limit.get(), } } + #[inline] + pub(crate) fn enter_tracing(&self) { + self.tracing_depth.set(self.tracing_depth.get() + 1); + } + + #[inline] + pub(crate) fn leave_tracing(&self) { + let depth = self.tracing_depth.get(); + debug_assert!(depth > 0); + self.tracing_depth.set(depth.saturating_sub(1)); + } + + #[inline] + pub(crate) fn tracing_is_suppressed(&self) -> bool { + self.tracing_depth.get() != 0 + } + // To be called right before raising the recursion depth. fn check_recursive_call(&self, _where: &str) -> PyResult<()> { if self.recursion_depth.get() >= self.recursion_limit.get() { @@ -1723,24 +2539,23 @@ impl VirtualMachine { } } - pub fn current_frame(&self) -> Option { - self.frames.borrow().last().map(|fp| { - // SAFETY: the caller keeps the FrameRef alive while it's in the Vec - unsafe { fp.as_ref() }.to_owned() - }) + pub fn current_frame(&self) -> Option { + crate::frame::current_thread_frame_materialize(self) } pub fn current_locals(&self) -> PyResult { - self.current_frame() + // Must include light frames so locals() returns the correct scope. + crate::frame::current_thread_frame_materialize(self) .expect("called current_locals but no frames on the stack") .locals(self) } pub fn current_globals(&self) -> PyDictRef { - self.current_frame() - .expect("called current_globals but no frames on the stack") - .globals - .clone() + let ptr = crate::vm::thread::get_current_frame(); + if !ptr.is_null() { + return unsafe { (*ptr).globals().to_owned() }; + } + crate::frame::current_globals().expect("called current_globals but no frames on the stack") } pub fn try_class(&self, module: &'static str, class: &'static str) -> PyResult { @@ -1799,11 +2614,14 @@ impl VirtualMachine { .get_attr(identifier!(self, __import__), self) .map_err(|_| self.new_import_error("__import__ not found", module.to_owned()))?; - let (locals, globals) = if let Some(frame) = self.current_frame() { - ( - Some(frame.locals.clone_mapping(self)), - Some(frame.globals.clone()), - ) + let (locals, globals) = if let Some(globals) = crate::frame::current_globals() { + // Locals fallback: use the heavy frame if available, otherwise + // use globals as locals (light frame locals are on the data stack). + let locals_mapping = self.current_frame().map_or_else( + || ArgMapping::from_dict_exact(globals.clone()), + |f| f.iframe().locals.clone_mapping(self), + ); + (Some(locals_mapping), Some(globals)) } else { (None, None) }; @@ -1821,12 +2639,28 @@ impl VirtualMachine { // Objects/listobject.c. Each branch takes an atomic snapshot to avoid // race conditions from concurrent mutation (no GIL). let cls = value.class(); - let list_borrow; let slice = if cls.is(self.ctx.types.tuple_type) { value.downcast_ref::().unwrap().as_slice() } else if cls.is(self.ctx.types.list_type) { - list_borrow = value.downcast_ref::().unwrap().borrow_vec(); - &list_borrow + // The list is re-read on every step, the way map_iterable_object() + // does it: func() runs Python, which can mutate or even clear the + // same list, and a borrow held across that call deadlocks it. + let list = value.downcast_ref::().unwrap(); + let mut results = Vec::new(); + let mut i = 0; + loop { + let elem = { + let elements = list.borrow_vec(); + let Some(elem) = elements.get(i) else { + break; + }; + elem.clone() + // free the lock + }; + results.push(func(elem)?); + i += 1; + } + return Ok(results); } else if cls.is(self.ctx.types.dict_type) { let keys = value.downcast_ref::().unwrap().keys_vec(); return keys.into_iter().map(func).collect(); @@ -1938,10 +2772,7 @@ impl VirtualMachine { if exc.class().is(self.ctx.exceptions.attribute_error) { let exc = exc.as_object(); // Check if this exception was already augmented - let already_set = exc - .get_attr("name", self) - .ok() - .is_some_and(|v| !self.is_none(&v)); + let already_set = exc.get_attr("name", self).is_ok_and(|v| !self.is_none(&v)); if already_set { return; } @@ -1990,13 +2821,15 @@ impl VirtualMachine { return true; } - #[cfg(all(unix, feature = "threading"))] + #[cfg(feature = "threading")] if thread::stop_requested_for_current_thread() { return true; } + // Signal and QSBR bits share one word: a single relaxed load per + // instruction covers both. #[cfg(not(target_arch = "wasm32"))] - if crate::signal::is_triggered() { + if crate::signal::eval_breaker_pending() { return true; } @@ -2011,12 +2844,18 @@ impl VirtualMachine { if self.state.finalizing.load(Ordering::Acquire) && !self.is_main_thread() { // once finalization starts, // non-main Python threads should stop running bytecode. - return Err(self.new_exception(self.ctx.exceptions.system_exit.to_owned(), vec![])); + return Err(self.new_system_exit(vec![].into())); } // Suspend this thread if stop-the-world is in progress - #[cfg(all(unix, feature = "threading"))] - thread::suspend_if_needed(&self.state.stop_the_world); + #[cfg(feature = "threading")] + thread::suspend_if_needed(&self.state); + + // Pass a QSBR checkpoint if requested (deferred memory reclamation). + #[cfg(feature = "threading")] + if crate::signal::qsbr_bit_set() && thread::qsbr_break_requested() { + thread::qsbr_checkpoint(); + } #[cfg(not(target_arch = "wasm32"))] crate::signal::check_signals(self)?; @@ -2024,6 +2863,18 @@ impl VirtualMachine { Ok(()) } + /// Run an automatic collection scheduled by `maybe_collect`, if any. + /// + /// Called only from the bytecode-loop safepoint, where no interpreter + /// locks are held, so the stop-the-world it performs cannot deadlock + /// against a thread blocked on a lock this thread would otherwise hold. + #[cfg(feature = "threading")] + pub(crate) fn run_scheduled_gc(&self) { + if crate::signal::take_gc_scheduled() { + self.state.gc.collect(0); + } + } + /// Push a new exc_info slot (for generator/coroutine resume). pub(crate) fn push_exception(&self, exc: Option) { self.exceptions.borrow_mut().stack.push(exc); @@ -2068,6 +2919,25 @@ impl VirtualMachine { thread::update_thread_exception(self.topmost_exception()); } + /// Restore an exc_info slot value saved by `with_frame`, skipping the + /// store when the slot is unchanged. `saved` is a strong reference taken + /// at save time, so the object it points to cannot have been freed and + /// its address reused while the frame ran; pointer identity therefore + /// proves the slot still holds the same value and both the store and the + /// thread-exception mirror update would be no-ops. + pub(crate) fn restore_exception(&self, saved: Option) { + let excs = self.exceptions.borrow(); + let unchanged = match (excs.stack.last(), &saved) { + (Some(Some(current)), Some(saved)) => current.is(saved), + (Some(None), None) => true, + _ => false, + }; + drop(excs); + if !unchanged { + self.set_exception(saved); + } + } + pub fn take_raised_exception(&self) -> Option { let mut excs = self.exceptions.borrow_mut(); if let Some(top) = excs.stack.last_mut() { @@ -2116,29 +2986,28 @@ impl VirtualMachine { pub fn handle_exit_exception(&self, exc: PyBaseExceptionRef) -> u32 { if exc.fast_isinstance(self.ctx.exceptions.system_exit) { - let args = exc.args(); - let msg = match args.as_slice() { - [] => return 0, - [arg] => match_class!(match arg { - ref i @ PyInt => { - use num_traits::cast::ToPrimitive; - // Try u32 first, then i32 (for negative values), else -1 for overflow - let code = i - .as_bigint() - .to_u32() - .or_else(|| i.as_bigint().to_i32().map(|v| v as u32)) - .unwrap_or(-1i32 as u32); - return code; - } - arg => { - if self.is_none(arg) { - return 0; - } - arg.str(self).ok() + let code = exc + .as_object() + .get_attr("code", self) + .unwrap_or_else(|_| exc.as_object().to_owned()); + let msg = match_class!(match code { + ref i @ PyInt => { + use num_traits::cast::ToPrimitive; + // Try u32 first, then i32 (for negative values), else -1 for overflow + let code = i + .as_bigint() + .to_u32() + .or_else(|| i.as_bigint().to_i32().map(|v| v as u32)) + .unwrap_or(-1i32 as u32); + return code; + } + code => { + if self.is_none(&code) { + return 0; } - }), - _ => args.as_object().repr(self).ok(), - }; + code.str(self).ok() + } + }); if let Some(msg) = msg { // Write using Python's write() to use stderr's error handler (backslashreplace) if let Ok(stderr) = stdlib::sys::get_stderr(self) { @@ -2286,7 +3155,7 @@ mod tests { let source = "from dir_module.dir_module_inner import value2"; let code_obj = vm .compile(source, vm::compiler::Mode::Exec, "") - .map_err(|err| vm.new_syntax_error(&err, Some(source))) + .map_err(|err| err.into_pyexception(vm, Some(source))) .unwrap(); if let Err(e) = vm.run_code_obj(code_obj, scope) { diff --git a/crates/vm/src/vm/python_run.rs b/crates/vm/src/vm/python_run.rs index c21b437b575..a1c2552cef4 100644 --- a/crates/vm/src/vm/python_run.rs +++ b/crates/vm/src/vm/python_run.rs @@ -22,7 +22,7 @@ impl VirtualMachine { pub fn run_string(&self, scope: Scope, source: &str, source_path: &str) -> PyResult { let code_obj = self .compile(source, compiler::Mode::Exec, source_path) - .map_err(|err| self.new_syntax_error(&err, Some(source)))?; + .map_err(|err| err.into_pyexception(self, Some(source)))?; // linecache._register_code(code, source, filename) let _ = self.register_code_in_linecache(&code_obj, source); self.run_code_obj(code_obj, scope) @@ -47,7 +47,7 @@ impl VirtualMachine { pub fn run_block_expr(&self, scope: Scope, source: &str) -> PyResult { let code_obj = self .compile(source, compiler::Mode::BlockExpr, "") - .map_err(|err| self.new_syntax_error(&err, Some(source)))?; + .map_err(|err| err.into_pyexception(self, Some(source)))?; self.run_code_obj(code_obj, scope) } } @@ -105,11 +105,23 @@ mod file_run { if path != "" { set_main_loader(module_dict, path, "SourceFileLoader", self)?; } - match crate::host_env::fs::read_to_string(path) { - Ok(source) => { + match crate::host_env::fs::read(path) { + Ok(source_bytes) => { + if source_bytes.contains(&0) { + return Err(self.new_exception_msg( + self.ctx.exceptions.syntax_error.to_owned(), + "source code cannot contain null bytes".into(), + )); + } + #[cfg(feature = "parser")] + // Match compile() by honoring BOMs and encoding cookies in files. + let source = self.decode_source_bytes(&source_bytes, path, false)?; + #[cfg(not(feature = "parser"))] + let source = String::from_utf8(source_bytes) + .map_err(|err| self.new_os_error(err.to_string()))?; let code_obj = self .compile(&source, compiler::Mode::Exec, path) - .map_err(|err| self.new_syntax_error(&err, Some(&source)))?; + .map_err(|err| err.into_pyexception(self, Some(&source)))?; self.run_code_obj(code_obj, scope)?; } Err(err) => { diff --git a/crates/vm/src/vm/runtime.rs b/crates/vm/src/vm/runtime.rs new file mode 100644 index 00000000000..7c5168c45b1 --- /dev/null +++ b/crates/vm/src/vm/runtime.rs @@ -0,0 +1,326 @@ +//! Process-global runtime support for multiple interpreters (PEP 734 preparation). +//! +//! CPython maps roughly as: +//! - this module ≈ `_PyRuntimeState.interpreters` + ID allocation +//! - [`crate::vm::PyGlobalState`] ≈ `PyInterpreterState` +//! - [`crate::VirtualMachine`] ≈ `PyThreadState` (plus shared refs to interpreter state) +//! +//! Multiple [`crate::Interpreter`] instances can coexist in one process. Each owns +//! an isolated `PyGlobalState` (modules, codecs, thread registry, stop-the-world, …) +//! while sharing the process-wide [`crate::Context`] (builtin types / immortals). + +use crate::common::rc::PyRc; +use crate::vm::PyGlobalState; +use core::sync::atomic::{AtomicI64, Ordering}; +use parking_lot::Mutex; +use std::collections::HashMap; + +/// Where an interpreter state came from (mirrors CPython `_PyInterpreterState_GetWhence`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(i32)] +pub enum InterpreterWhence { + /// Unknown / not recorded. + Unknown = 0, + /// Created as the process main interpreter at runtime init. + Runtime = 1, + /// Legacy C-API creation path (reserved for C-API parity). + LegacyCapi = 2, + /// Modern C-API creation path (reserved for C-API parity). + Capi = 3, + /// Cross-interpreter C-API (reserved). + Xi = 4, + /// Created via the stdlib / Rust subinterpreter API (PEP 734). + Stdlib = 5, +} + +impl InterpreterWhence { + #[must_use] + pub const fn as_i32(self) -> i32 { + self as i32 + } +} + +/// Snapshot of a registered interpreter for enumeration APIs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InterpreterInfo { + pub id: i64, + pub whence: InterpreterWhence, +} + +struct RegistryEntry { + whence: InterpreterWhence, + /// Weak handle so the registry does not keep interpreters alive. + /// Type matches `PyRc` (Arc when threading, Rc otherwise). + #[cfg(feature = "threading")] + state: alloc::sync::Weak, + #[cfg(not(feature = "threading"))] + state: alloc::rc::Weak, +} + +/// `main_id` value before any main interpreter has been registered. +const NO_MAIN_INTERPRETER: i64 = -1; + +struct InterpreterRegistry { + next_id: AtomicI64, + /// Id of the first registered `is_main` interpreter (PEP 734 `get_main()`), + /// or [`NO_MAIN_INTERPRETER`]. + main_id: AtomicI64, + /// id → entry. Main interpreter is always id 0 when created first. + entries: Mutex>, +} + +impl InterpreterRegistry { + fn new() -> Self { + Self { + // Monotonic ids starting at 0. Concurrent Interpreter construction + // (e.g. cargo test threads) must never share an id. + next_id: AtomicI64::new(0), + main_id: AtomicI64::new(NO_MAIN_INTERPRETER), + entries: Mutex::new(HashMap::new()), + } + } +} + +/// The interpreter registry. +/// +/// With `threading` this is one process-global table. Without it, `PyRc` is +/// `Rc` and each OS thread owns an independent `Context::genesis()` and +/// `GcState`, so the registry is thread-local for the same reason `gc_state()` +/// is: an `Rc` handle must never be reachable from another thread. +/// `static_cell!` provides exactly that split. +fn registry() -> &'static InterpreterRegistry { + rustpython_common::static_cell! { + static REGISTRY: InterpreterRegistry; + } + REGISTRY.get_or_init(InterpreterRegistry::new) +} + +/// Conventional id of the first process main interpreter when allocation is +/// sequential (CPython parity). Concurrent construction may assign other ids; +/// use [`PyGlobalState::is_main`] / [`crate::Interpreter::is_main`] to identify +/// a main interpreter, not this constant alone. +pub const MAIN_INTERPRETER_ID: i64 = 0; + +/// Backs `sys.implementation.supports_isolated_interpreters`. +/// +/// The Rust substrate already isolates interpreters (`PyGlobalState` per +/// interpreter, per-interpreter thread slots / stop-the-world). This stays +/// `false` until the Python-facing `_interpreters` module is wired up; flip it +/// in the commit that lands `_interpreters`. +pub const SUPPORTS_ISOLATED_INTERPRETERS: bool = false; + +/// Id of the main interpreter (PEP 734 `get_main()`), or `None` before any +/// interpreter has been created. +/// +/// This is distinct from [`PyGlobalState::is_main`]: every top-level (non-sub) +/// interpreter carries `is_main` for its own signal / main-thread bookkeeping, +/// but only the first one registered becomes *the* main. +#[must_use] +pub fn main_interpreter_id() -> Option { + match registry().main_id.load(Ordering::Acquire) { + NO_MAIN_INTERPRETER => None, + id => Some(id), + } +} + +/// Allocate a unique interpreter id. +/// +/// Ids are strictly monotonic and never reused for the lifetime of the +/// registry, so concurrent `Interpreter` construction (parallel unit tests, +/// multi-threaded embedding) never shares an id. Without `threading` the +/// registry — like `Context::genesis()` and the GC state — is per OS thread, so +/// ids are unique within a thread rather than across the process. +pub(crate) fn alloc_interpreter_id() -> i64 { + registry().next_id.fetch_add(1, Ordering::Relaxed) +} + +/// Gate between registering an interpreter and a collection's stop-the-world. +/// +/// A collection snapshots the registry, stops every interpreter in the +/// snapshot, and then reads tracked objects with those threads parked. An +/// interpreter that registered after the snapshot was taken would not be in it, +/// so nothing would stop it, and its bootstrap — which runs Python and mutates +/// the shared generation lists — would run underneath that scan. Registration +/// therefore waits for an in-flight stop to end; the next collection's snapshot +/// then contains the new interpreter. +fn admission() -> &'static Mutex<()> { + static ADMISSION: std::sync::OnceLock> = std::sync::OnceLock::new(); + ADMISSION.get_or_init(|| Mutex::new(())) +} + +/// Take the admission gate for the duration of a stop-the-world. +#[cfg(feature = "threading")] +pub(crate) fn lock_admission_for_stop() -> parking_lot::MutexGuard<'static, ()> { + admission().lock() +} + +/// Add the registry entry, behind the admission gate. +/// +/// Only ever called with this thread detached, because the gate is held across +/// a stop-the-world: an attached thread waiting here, or re-attaching while +/// holding the gate, would leave that stop no safepoint to complete at. Nothing +/// under the gate blocks or allocates a tracked object, so this cannot re-enter +/// the collection it waits for. +fn insert_registry_entry(state: &PyRc) { + let _admission = admission().lock(); + let mut entries = registry().entries.lock(); + // Entries are weak and an interpreter's lifetime is decided by its last + // `PyRc` — which outlives the `Interpreter` handle whenever + // `new_thread()` workers are still running — so nothing removes them at a + // fixed point. Reap the dead ones here to bound the table instead. + entries.retain(|_, entry| entry.state.strong_count() > 0); + entries.insert( + state.interpreter_id, + RegistryEntry { + whence: state.whence, + state: PyRc::downgrade(state), + }, + ); +} + +/// Register an interpreter state in the registry. +pub(crate) fn register_interpreter(state: &PyRc) { + let id = state.interpreter_id; + if state.is_main { + // First `is_main` interpreter defines the main for `get_main()`. + // Additional top-level Interpreters (embedding) keep their own `is_main` + // flag but do not displace the recorded main. + let _ = registry().main_id.compare_exchange( + NO_MAIN_INTERPRETER, + id, + Ordering::AcqRel, + Ordering::Relaxed, + ); + } + // A subinterpreter is registered by a thread that is running its parent, so + // detach for the whole insert rather than only for the wait. + let detached = crate::vm::thread::try_with_current_vm(|vm| { + vm.allow_threads(|| insert_registry_entry(state)) + }); + if detached.is_none() { + insert_registry_entry(state); + } +} + +/// Look up a live interpreter state by id. +#[must_use] +pub fn lookup_interpreter(id: i64) -> Option> { + let entries = registry().entries.lock(); + entries.get(&id).and_then(|e| e.state.upgrade()) +} + +/// List all currently registered (still-alive) interpreters. +#[must_use] +pub fn list_interpreters() -> Vec { + let entries = registry().entries.lock(); + let mut out: Vec = entries + .iter() + .filter_map(|(&id, entry)| { + // Drop dead weak refs from the listing. + if entry.state.strong_count() == 0 { + return None; + } + Some(InterpreterInfo { + id, + whence: entry.whence, + }) + }) + .collect(); + out.sort_by_key(|info| info.id); + out +} + +/// Number of registered interpreters that are still alive. +#[must_use] +pub fn interpreter_count() -> usize { + list_interpreters().len() +} + +/// Reset the registry's locks after `fork()`. +/// +/// The tables are reachable from every thread, so a thread that died in the +/// fork may have left one locked; the child would then deadlock the first time +/// it enumerates interpreters (which the collector now does on every stop). +/// +/// # Safety +/// Must only be called after `fork()` in the child process, when no other +/// threads exist and the calling thread holds none of these locks. +#[cfg(all(unix, feature = "threading"))] +pub unsafe fn reinit_after_fork() { + unsafe { + crate::common::lock::reinit_mutex_after_fork(®istry().entries); + crate::common::lock::reinit_mutex_after_fork(owned_interpreters()); + crate::common::lock::reinit_mutex_after_fork(admission()); + } +} + +/// All live interpreter states, ordered by id. +/// +/// Used by the cyclic collector, which must stop every interpreter's threads +/// (not just the collecting one) because GC-tracked objects from all +/// interpreters share one object graph. Ordering is deterministic so that +/// multiple stop-the-world requesters always take exclusions in the same order. +#[must_use] +pub fn live_interpreter_states() -> Vec> { + let entries = registry().entries.lock(); + let mut states: Vec<(i64, PyRc)> = entries + .iter() + .filter_map(|(&id, entry)| entry.state.upgrade().map(|state| (id, state))) + .collect(); + drop(entries); + states.sort_by_key(|(id, _)| *id); + states.into_iter().map(|(_, state)| state).collect() +} + +/// Runtime-owned interpreters (the ownership anchor for the Python +/// `_interpreters` API). +/// +/// A Rust [`crate::Interpreter`] handle is normally owned by its Rust caller. +/// For PEP 734, `_interpreters.create()` returns only an id and the runtime +/// must keep the interpreter alive until `_interpreters.destroy(id)`. These +/// functions hold that ownership, keyed by interpreter id, while the weak +/// [`registry`] above still drives enumeration and lookup. +/// +/// Only available with the `threading` feature: a runtime-owned interpreter is +/// reachable from other OS threads, which requires `Interpreter: Send` (true +/// only when `PyObjectRef` is `Arc`-backed). +#[cfg(feature = "threading")] +fn owned_interpreters() -> &'static Mutex> { + use std::sync::OnceLock; + static OWNED: OnceLock>> = OnceLock::new(); + OWNED.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Transfer ownership of `interp` to the runtime, returning its id. +#[cfg(feature = "threading")] +pub fn store_owned_interpreter(interp: crate::Interpreter) -> i64 { + let id = interp.id(); + // Ids are strictly monotonic, so this never displaces (and drops) an + // existing entry under the lock. + owned_interpreters().lock().insert(id, interp); + id +} + +/// Reclaim a runtime-owned interpreter, removing it from the owner table. +/// +/// The returned handle is dropped by the caller *outside* the owner lock; its +/// `Drop` unregisters the interpreter from the weak [`registry`]. +#[cfg(feature = "threading")] +#[must_use] +pub fn take_owned_interpreter(id: i64) -> Option { + owned_interpreters().lock().remove(&id) +} + +/// Whether `id` refers to a runtime-owned interpreter. +#[cfg(feature = "threading")] +#[must_use] +pub fn is_owned_interpreter(id: i64) -> bool { + owned_interpreters().lock().contains_key(&id) +} + +/// Number of runtime-owned interpreters currently alive. +#[cfg(feature = "threading")] +#[must_use] +pub fn owned_interpreter_count() -> usize { + owned_interpreters().lock().len() +} diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 7298c95ab08..3c42ca0b6fc 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -25,6 +25,7 @@ pub struct Paths { /// Combined configuration: user settings + computed paths /// CPython directly exposes every fields under both of them. /// We separate them to maintain better ownership discipline. +#[derive(Clone)] pub struct PyConfig { pub settings: Settings, pub paths: Paths, @@ -39,6 +40,7 @@ impl PyConfig { /// User-configurable settings for the python vm. #[non_exhaustive] +#[derive(Clone)] pub struct Settings { /// -I pub isolated: bool, diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index 33fcec5e43e..3f83d88fe70 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -1,48 +1,70 @@ -#[cfg(feature = "threading")] +#[cfg(all(not(unix), feature = "threading"))] use super::FramePtr; #[cfg(feature = "threading")] use crate::builtins::PyBaseExceptionRef; #[cfg(feature = "threading")] use alloc::sync::Arc; -use crate::frame::Frame; +use crate::frame::InterpreterFrame; +#[cfg(feature = "threading")] +use crate::vm::PyGlobalState; use crate::{AsObject, PyObject, VirtualMachine}; +#[cfg(all(unix, feature = "threading"))] +use crate::{Py, frame::FrameObject}; +#[cfg(all(unix, feature = "threading"))] +use core::sync::atomic::AtomicPtr; use core::{ cell::{Cell, RefCell}, ptr::NonNull, - sync::atomic::{AtomicPtr, Ordering}, + sync::atomic::{AtomicUsize, Ordering}, }; use itertools::Itertools; +#[cfg(feature = "threading")] +use std::collections::HashMap; use std::thread_local; // Thread states for stop-the-world support. // DETACHED: not executing Python bytecode (in native code, or idle) // ATTACHED: actively executing Python bytecode // SUSPENDED: parked by a stop-the-world request -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub const THREAD_DETACHED: i32 = 0; -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub const THREAD_ATTACHED: i32 = 1; -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub const THREAD_SUSPENDED: i32 = 2; /// Per-thread shared state for sys._current_frames() and sys._current_exceptions(). /// The exception field uses atomic operations for lock-free cross-thread reads. #[cfg(feature = "threading")] pub struct ThreadSlot { + /// Top of the owning thread's Python call stack, published for + /// cross-thread readers (`sys._current_frames`, cross-thread `f_back`). + /// The rest of the stack is reachable via each frame's `previous` pointer. + /// Written lock-free on the hot push/pop path with relaxed ordering; every + /// cross-thread read runs under stop-the-world, which parks the owning + /// thread at a safepoint and supplies the happens-before edge, so the + /// pointer and the frames it reaches are quiescent and alive at read time. + #[cfg(unix)] + pub top_frame: AtomicPtr>, + /// Raw InterpreterFrame pointer, published alongside top_frame so + /// cross-thread readers (sys._current_frames) can materialize + /// stack-allocated frames that have no FrameObject. + pub top_iframe: AtomicUsize, /// Raw frame pointers, valid while the owning thread's call stack is active. - /// Readers must hold the Mutex and convert to FrameRef inside the lock. + /// Readers must hold the Mutex and convert to FrameObjectRef inside the lock. + /// Used on non-unix threading builds, which have no stop-the-world. + #[cfg(not(unix))] pub frames: parking_lot::Mutex>, pub exception: crate::PyAtomicRef>, /// Thread state for stop-the-world: DETACHED / ATTACHED / SUSPENDED - #[cfg(unix)] pub state: core::sync::atomic::AtomicI32, /// Per-thread stop request bit (eval breaker equivalent). - #[cfg(unix)] pub stop_requested: core::sync::atomic::AtomicBool, /// Handle for waking this thread from park in stop-the-world paths. - #[cfg(unix)] pub thread: std::thread::Thread, + /// QSBR state for deferred memory reclamation. + pub(crate) qsbr: Arc, } #[cfg(feature = "threading")] @@ -67,17 +89,50 @@ thread_local! { pub(crate) static COROUTINE_ORIGIN_TRACKING_DEPTH: Cell = const { Cell::new(0) }; - /// Current thread's slot for sys._current_frames() and sys._current_exceptions() + /// Per-interpreter thread slots for this OS thread (PEP 734 multi-interpreter). + /// + /// CPython keeps a `PyThreadState` per (thread, interpreter) pair. RustPython + /// mirrors that: each interpreter's `PyGlobalState.thread_frames` gets its own + /// [`ThreadSlot`] for this OS thread. `CURRENT_THREAD_SLOT` always points at + /// the slot for the currently entered interpreter. + #[cfg(feature = "threading")] + static INTERP_THREAD_SLOTS: RefCell> = + RefCell::new(HashMap::new()); + + /// Current thread's slot for the currently entered interpreter. #[cfg(feature = "threading")] static CURRENT_THREAD_SLOT: RefCell> = const { RefCell::new(None) }; /// Current top frame for signal-safe traceback walking. - /// Mirrors `PyThreadState.current_frame`. Read by faulthandler's signal - /// handler to dump tracebacks without accessing RefCell or locks. - /// Uses AtomicPtr for async-signal-safety (signal handlers may read this - /// while the owning thread is writing). - pub(crate) static CURRENT_FRAME: AtomicPtr = - const { AtomicPtr::new(core::ptr::null_mut()) }; + /// Stores a `*const InterpreterFrame` as `usize`. + /// Read by faulthandler's signal handler to dump tracebacks without + /// accessing RefCell or locks. Uses AtomicUsize for async-signal-safety. + pub(crate) static CURRENT_FRAME: AtomicUsize = + const { AtomicUsize::new(0) }; + + /// Cached pointer to this thread's `ThreadSlot::top_frame`, so the hot + /// push/pop path can publish the top frame with a single relaxed store and + /// no `CURRENT_THREAD_SLOT` RefCell borrow. Null until the slot is + /// initialized; the `Arc` in `CURRENT_THREAD_SLOT` keeps the + /// pointee alive until `cleanup_current_thread_frames` clears this. + #[cfg(all(unix, feature = "threading"))] + static CURRENT_TOP_FRAME_SLOT: Cell<*const AtomicPtr>> = + const { Cell::new(core::ptr::null()) }; + + /// Cached pointer to this thread's `ThreadSlot::top_iframe` for the hot + /// light-frame push/pop path. The slot's Arc keeps the pointee alive. + #[cfg(feature = "threading")] + static CURRENT_TOP_IFRAME_SLOT: Cell<*const AtomicUsize> = + const { Cell::new(core::ptr::null()) }; + + /// Cached pointer to this thread's `ThreadSlot::stop_requested`, for the + /// safepoint the dispatch loop takes once per instruction. Reading it + /// through `CURRENT_THREAD_SLOT` costs a `RefCell` borrow — two stores to + /// thread-local memory — where this costs one relaxed load. The slot's Arc + /// keeps the pointee alive, as with the frame pointers above. + #[cfg(feature = "threading")] + static CURRENT_STOP_REQUESTED: Cell<*const core::sync::atomic::AtomicBool> = + const { Cell::new(core::ptr::null()) }; } @@ -100,38 +155,97 @@ pub fn with_current_vm(f: impl FnOnce(&VirtualMachine) -> R) -> R { } fn set_current_vm(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { + // Attach to this VM's interpreter, detaching the enclosing one if this is a + // switch between interpreters on the same OS thread. + #[cfg(feature = "threading")] + let switched = begin_interpreter_section(vm); + VM_STACK.with(|vms| { vms.borrow_mut().push(vm.into()); scopeguard::defer! { vms.borrow_mut().pop(); + #[cfg(feature = "threading")] + end_interpreter_section(switched); } f() }) } +/// Pointer to the GC state of the interpreter running on this thread. +/// +/// The pointee belongs to the `PyGlobalState` of the VM on top of `VM_STACK`, +/// which is borrowed for the whole `set_current_vm` scope — so the pointer stays +/// valid as long as the caller remains inside that scope. +pub(crate) fn current_gc_state() -> Option> { + // Reached from every tracked allocation, including ones a thread-local + // destructor makes while the VM stack is being torn down, so neither a + // destroyed key nor an outstanding borrow may panic here. + VM_STACK + .try_with(|vms| { + let vm = vms.try_borrow().ok()?.last().copied()?; + // SAFETY: entries in VM_STACK either borrow a VM for the dynamic + // scope of a set_current_vm()/enter_vm() call or point at GILSTATE_VM. + Some(NonNull::from(&unsafe { vm.as_ref() }.state.gc)) + }) + .ok() + .flatten() +} + +pub fn try_with_current_vm(f: impl FnOnce(&VirtualMachine) -> R) -> Option { + VM_STACK.with(|vms| { + let vm = vms.borrow().last().copied()?; + // SAFETY: entries in VM_STACK either borrow a VM for the dynamic + // scope of a set_current_vm()/enter_vm() call or point at GILSTATE_VM. + Some(f(unsafe { vm.as_ref() })) + }) +} + pub fn enter_vm(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { - // Outermost enter_vm: transition DETACHED → ATTACHED - #[cfg(all(unix, feature = "threading"))] - let was_outermost = !current_vm_is_set(); + // Attach/detach is handled by `set_current_vm`, which pairs it with the + // VM_STACK push so that switching interpreters mid-stack stays consistent. + set_current_vm(vm, f) +} - // Initialize thread slot for this thread if not already done +/// RAII counterpart to `enter_vm`, for code that runs Python bytecode across +/// several statements interspersed with `&mut VirtualMachine` calls +/// (`VirtualMachine::initialize`), where a single closure-based `enter_vm` +/// scope cannot be expressed because the borrow checker won't let a closure +/// hold `&mut VirtualMachine` at the same time `enter_vm` reborrows it as +/// `&VirtualMachine`. Construction only needs a transient `&VirtualMachine` +/// borrow, so it can be dropped before subsequent `&mut` use. +/// +/// Without this, code that runs Python bytecode before any `enter_vm` scope +/// exists would leave the thread not ATTACHED, making lock-free type cache +/// reads unsound. +#[must_use] +pub(crate) struct VmBootstrapGuard { #[cfg(feature = "threading")] - init_thread_slot_if_needed(vm); + switched: bool, +} - #[cfg(all(unix, feature = "threading"))] - if was_outermost { - attach_thread(vm); - } +impl VmBootstrapGuard { + pub(crate) fn new(vm: &VirtualMachine) -> Self { + #[cfg(feature = "threading")] + let switched = begin_interpreter_section(vm); - scopeguard::defer! { - // Outermost exit: transition ATTACHED → DETACHED - #[cfg(all(unix, feature = "threading"))] - if was_outermost { - detach_thread(); + VM_STACK.with(|vms| vms.borrow_mut().push(vm.into())); + + Self { + #[cfg(feature = "threading")] + switched, } } +} - set_current_vm(vm, f) +impl Drop for VmBootstrapGuard { + fn drop(&mut self) { + VM_STACK.with(|vms| { + vms.borrow_mut().pop(); + }); + + #[cfg(feature = "threading")] + end_interpreter_section(self.switched); + } } #[cfg(feature = "threading")] @@ -141,6 +255,67 @@ pub enum CurrentVmAttachState { Attached, } +/// State preserved while the current native thread is detached from its VM. +#[cfg(feature = "threading")] +pub struct SavedThreadState { + vm_stack: Vec>, + gilstate_vm: Option>, +} + +/// Detach the current native thread and preserve its VM context for restoration. +#[cfg(feature = "threading")] +#[must_use = "the saved thread state must be restored"] +pub fn save_current_thread() -> SavedThreadState { + let vm_stack = VM_STACK.with(|vms| core::mem::take(&mut *vms.borrow_mut())); + assert!( + !vm_stack.is_empty(), + "save_current_thread() called without an attached VM" + ); + let gilstate_vm = GILSTATE_VM.with(|gilstate_vm| gilstate_vm.borrow_mut().take()); + detach_thread(); + SavedThreadState { + vm_stack, + gilstate_vm, + } +} + +/// Restore a VM context previously returned by [`save_current_thread`]. +#[cfg(feature = "threading")] +pub fn restore_current_thread(state: SavedThreadState) { + assert!( + !current_vm_is_set(), + "restore_current_thread() called with an attached VM" + ); + let SavedThreadState { + vm_stack, + gilstate_vm, + } = state; + let vm = vm_stack + .last() + .copied() + .expect("saved thread state has no VM"); + + GILSTATE_VM.with(|current| { + let mut current = current.borrow_mut(); + assert!( + current.is_none(), + "restore_current_thread() called with a GILState VM" + ); + *current = gilstate_vm; + }); + + // SAFETY: borrowed VMs remain alive for the dynamic save/restore scope, + // while an owned GILState VM was restored above before this dereference. + let vm = unsafe { vm.as_ref() }; + // Point CURRENT_THREAD_SLOT at the restored interpreter before attach. + // After subinterpreter bootstrap, CURRENT may still refer to the temporary + // subinterpreter slot (DETACHED); attaching that would leave the parent + // slot detached and later confuse outermost detach. + init_thread_slot_if_needed(vm); + attach_thread(vm); + VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack); +} + /// Attach the current native thread to a RustPython VM until /// `release_current_thread()` is called. #[cfg(feature = "threading")] @@ -161,7 +336,6 @@ pub fn attach_current_thread( init_thread_slot_if_needed(vm); - #[cfg(unix)] attach_thread(vm); VM_STACK.with(|vms| { @@ -188,46 +362,136 @@ pub fn release_current_thread(state: CurrentVmAttachState) { .expect("release_current_thread() called without an attached VM"); }); - #[cfg(unix)] detach_thread(); } -/// Initialize thread slot for current thread if not already initialized. -/// Called automatically by enter_vm(). +/// Ensure this OS thread has a [`ThreadSlot`] registered with `vm`'s interpreter +/// and make it the current slot. +/// +/// Called automatically by `enter_vm()` / `VmBootstrapGuard` whenever a VM +/// becomes current. Switching between interpreters on the same OS thread swaps +/// `CURRENT_THREAD_SLOT` to that interpreter's slot (creating one if needed). #[cfg(feature = "threading")] fn init_thread_slot_if_needed(vm: &VirtualMachine) { - CURRENT_THREAD_SLOT.with(|slot| { - if slot.borrow().is_none() { - let thread_id = crate::stdlib::_thread::get_ident(); - let mut registry = vm.state.thread_frames.lock(); - let new_slot = Arc::new(ThreadSlot { - frames: parking_lot::Mutex::new(Vec::new()), - exception: crate::PyAtomicRef::from(None::), - #[cfg(unix)] - state: core::sync::atomic::AtomicI32::new( - if vm.state.stop_the_world.requested.load(Ordering::Acquire) { - // Match init_threadstate(): new thread-state starts - // suspended while stop-the-world is active. - THREAD_SUSPENDED - } else { - THREAD_DETACHED - }, - ), - #[cfg(unix)] - stop_requested: core::sync::atomic::AtomicBool::new(false), - #[cfg(unix)] - thread: std::thread::current(), - }); - registry.insert(thread_id, new_slot.clone()); - drop(registry); - *slot.borrow_mut() = Some(new_slot); + let slot = ensure_thread_slot(vm); + set_current_thread_slot(slot); +} + +/// Look up (creating if needed) this thread's [`ThreadSlot`] for `vm`'s +/// interpreter, without making it the current slot. +#[cfg(feature = "threading")] +fn ensure_thread_slot(vm: &VirtualMachine) -> CurrentFrameSlot { + let interp_id = vm.state.interpreter_id; + INTERP_THREAD_SLOTS.with(|slots| { + let mut slots = slots.borrow_mut(); + if let Some(existing) = slots.get(&interp_id) { + return existing.clone(); } + + let thread_id = crate::stdlib::_thread::get_ident(); + let mut registry = vm.state.thread_frames.lock(); + let new_slot = Arc::new(ThreadSlot { + #[cfg(unix)] + top_frame: AtomicPtr::new(core::ptr::null_mut()), + top_iframe: AtomicUsize::new(0), + #[cfg(not(unix))] + frames: parking_lot::Mutex::new(Vec::new()), + exception: crate::PyAtomicRef::from(None::), + state: core::sync::atomic::AtomicI32::new( + if vm.state.stop_the_world.requested.load(Ordering::Acquire) { + // Match init_threadstate(): new thread-state starts + // suspended while stop-the-world is active. + THREAD_SUSPENDED + } else { + THREAD_DETACHED + }, + ), + stop_requested: core::sync::atomic::AtomicBool::new(false), + thread: std::thread::current(), + qsbr: crate::object::qsbr::QSBR.register(), + }); + registry.insert(thread_id, new_slot.clone()); + drop(registry); + slots.insert(interp_id, new_slot.clone()); + new_slot + }) +} + +/// Make `slot` the current thread slot (and the cached top-frame pointer). +#[cfg(feature = "threading")] +fn set_current_thread_slot(slot: CurrentFrameSlot) { + #[cfg(unix)] + CURRENT_TOP_FRAME_SLOT.with(|c| c.set(&slot.top_frame)); + CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(&slot.top_iframe)); + CURRENT_STOP_REQUESTED.with(|c| c.set(&slot.stop_requested)); + CURRENT_THREAD_SLOT.with(|current| { + *current.borrow_mut() = Some(slot); }); } +/// Whether the current thread slot is ATTACHED. +#[cfg(feature = "threading")] +fn current_slot_is_attached() -> bool { + CURRENT_THREAD_SLOT.with(|slot| { + slot.borrow() + .as_ref() + .is_some_and(|s| s.state.load(Ordering::Acquire) == THREAD_ATTACHED) + }) +} + +/// Attach this thread to `vm`'s interpreter for the duration of a section, +/// detaching whichever interpreter it was attached to (≈ `_PyThreadState_Swap`). +/// +/// A thread must never be ATTACHED to two interpreters at once: stop-the-world +/// treats an ATTACHED slot as "running this interpreter's bytecode" and a +/// DETACHED slot as parkable without cooperation, so running interpreter B's +/// code while B's slot is DETACHED would let a collector conclude B is stopped +/// while this thread keeps mutating the (process-global) object graph. +/// +/// Returns whether the attachment changed, i.e. whether the matching +/// [`end_interpreter_section`] must undo it. +#[cfg(feature = "threading")] +fn begin_interpreter_section(vm: &VirtualMachine) -> bool { + let target = ensure_thread_slot(vm); + let already_current = CURRENT_THREAD_SLOT.with(|slot| { + slot.borrow() + .as_ref() + .is_some_and(|s| Arc::ptr_eq(s, &target)) + }); + if already_current && current_slot_is_attached() { + // Nested section in the same interpreter: already attached. + return false; + } + if !already_current && current_slot_is_attached() { + detach_thread(); + } + set_current_thread_slot(target); + attach_thread(vm); + true +} + +/// Undo [`begin_interpreter_section`]: detach this interpreter and re-attach the +/// enclosing one, if any. Call after the VM has been popped from `VM_STACK`. +#[cfg(feature = "threading")] +fn end_interpreter_section(switched: bool) { + if !switched { + return; + } + if current_slot_is_attached() { + detach_thread(); + } + // The enclosing section, if any, is the VM now on top of the stack. + if let Some(vm_ptr) = VM_STACK.with(|vms| vms.borrow().last().copied()) { + // SAFETY: entries on VM_STACK are valid for their enter/set_current_vm scope. + let vm = unsafe { vm_ptr.as_ref() }; + set_current_thread_slot(ensure_thread_slot(vm)); + attach_thread(vm); + } +} + /// Transition DETACHED → ATTACHED. Blocks if the thread was SUSPENDED by /// a stop-the-world request (like `_PyThreadState_Attach` + `tstate_wait_attach`). -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] fn wait_while_suspended(slot: &ThreadSlot) -> u64 { let mut wait_yields = 0u64; while slot.state.load(Ordering::Acquire) == THREAD_SUSPENDED { @@ -237,7 +501,7 @@ fn wait_while_suspended(slot: &ThreadSlot) -> u64 { wait_yields } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] fn attach_thread(vm: &VirtualMachine) { CURRENT_THREAD_SLOT.with(|slot| { if let Some(s) = slot.borrow().as_ref() { @@ -250,6 +514,7 @@ fn attach_thread(vm: &VirtualMachine) { Ordering::Relaxed, ) { Ok(_) => { + crate::object::qsbr::QSBR.online(&s.qsbr); super::stw_trace(format_args!("attach DETACHED->ATTACHED")); break; } @@ -268,10 +533,19 @@ fn attach_thread(vm: &VirtualMachine) { } } }); + // A stop-the-world may have been requested while this thread was detached. + // Honoring it here (rather than only at the next bytecode safepoint) keeps + // a thread doing rapid allow_threads calls from re-attaching and running + // past the requester forever, which would stall stop-the-world. Done + // outside the CURRENT_THREAD_SLOT borrow above because suspend re-borrows + // it. Safe against a concurrent start_the_world: suspend_if_needed decides + // whether to park under the registry lock, so it never parks after the + // request has been withdrawn. + suspend_if_needed(&vm.state); } /// Transition ATTACHED → DETACHED (like `_PyThreadState_Detach`). -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] fn detach_thread() { CURRENT_THREAD_SLOT.with(|slot| { if let Some(s) = slot.borrow().as_ref() { @@ -281,7 +555,9 @@ fn detach_thread() { Ordering::AcqRel, Ordering::Acquire, ) { - Ok(_) => {} + Ok(_) => { + crate::object::qsbr::QSBR.offline(&s.qsbr); + } Err(THREAD_DETACHED) => { debug_assert!(false, "detach called while already DETACHED"); return; @@ -301,7 +577,7 @@ fn detach_thread() { /// to park this thread during blocking operations. /// /// `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS` equivalent. -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] pub fn allow_threads(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { // Preserve save/restore semantics: // only detach if this call observed ATTACHED at entry, and always restore @@ -322,8 +598,8 @@ pub fn allow_threads(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { result } -/// No-op on non-unix or non-threading builds. -#[cfg(not(all(unix, feature = "threading")))] +/// No-op on non-threading builds. +#[cfg(not(feature = "threading"))] pub fn allow_threads(_vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { f() } @@ -331,120 +607,177 @@ pub fn allow_threads(_vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { /// Called from check_signals when stop-the-world is requested. /// Transitions ATTACHED → SUSPENDED and waits until released /// (like `_PyThreadState_Suspend` + `_PyThreadState_Attach`). -#[cfg(all(unix, feature = "threading"))] -pub fn suspend_if_needed(stw: &super::StopTheWorldState) { +#[cfg(feature = "threading")] +pub fn suspend_if_needed(state: &PyGlobalState) { let should_suspend = CURRENT_THREAD_SLOT.with(|slot| { slot.borrow() .as_ref() .is_some_and(|s| s.stop_requested.load(Ordering::Relaxed)) }); - if !should_suspend { - return; + if should_suspend { + do_suspend(state); } +} + +#[cfg(feature = "threading")] +#[cold] +fn do_suspend(state: &PyGlobalState) { + let stw = &state.stop_the_world; + CURRENT_THREAD_SLOT.with(|slot| { + let borrowed = slot.borrow(); + let Some(s) = borrowed.as_ref() else { + return; + }; - if !stw.requested.load(Ordering::Acquire) { - CURRENT_THREAD_SLOT.with(|slot| { - if let Some(s) = slot.borrow().as_ref() { + // Decide whether to park while holding the thread registry. Both edges + // of `requested` are written under that lock: `init_thread_countdown` + // sets it, and `start_the_world` clears it and then releases every + // SUSPENDED thread without letting go. Publishing SUSPENDED here is + // therefore either seen by that release pass or never reached, which + // leaves the requester the only writer that takes a thread out of + // SUSPENDED. A completion check that observed this thread parked cannot + // then be invalidated by the thread resuming on its own. + let park = { + let _registry = state.thread_frames.lock(); + if stw.requested.load(Ordering::Acquire) { + Some(s.state.compare_exchange( + THREAD_ATTACHED, + THREAD_SUSPENDED, + Ordering::AcqRel, + Ordering::Acquire, + )) + } else { + // The stop already ended; this thread's request bit is stale. s.stop_requested.store(false, Ordering::Release); + None } - }); - return; - } + }; - do_suspend(stw); -} + match park { + None => { + super::stw_trace(format_args!("suspend skip not-requested")); + return; + } + Some(Ok(_)) => { + // Consumed this thread's stop request bit. + s.stop_requested.store(false, Ordering::Release); + } + Some(Err(THREAD_DETACHED)) => { + // Leaving VM; caller will re-check on next entry. + super::stw_trace(format_args!("suspend skip DETACHED")); + return; + } + Some(Err(THREAD_SUSPENDED)) => { + // Already parked by another path. + s.stop_requested.store(false, Ordering::Release); + super::stw_trace(format_args!("suspend skip already-suspended")); + return; + } + Some(Err(state)) => { + debug_assert!(false, "unexpected thread state in suspend: {state}"); + return; + } + } + super::stw_trace(format_args!("suspend ATTACHED->SUSPENDED")); -#[cfg(all(unix, feature = "threading"))] -#[cold] -fn do_suspend(stw: &super::StopTheWorldState) { - CURRENT_THREAD_SLOT.with(|slot| { - if let Some(s) = slot.borrow().as_ref() { - // ATTACHED → SUSPENDED + // Notify the stop-the-world requester that we've parked. The registry + // is released first: the requester's wait loop takes the notify mutex + // and then the registry, so taking them the other way round here would + // invert the order. + stw.notify_suspended(); + super::stw_trace(format_args!("suspend notified-requester")); + + // Wait until start_the_world sets us back to DETACHED + let wait_yields = wait_while_suspended(s); + stw.add_suspend_wait_yields(wait_yields); + + // Re-attach (DETACHED → ATTACHED), tstate_wait_attach CAS loop. + loop { match s.state.compare_exchange( + THREAD_DETACHED, THREAD_ATTACHED, - THREAD_SUSPENDED, Ordering::AcqRel, Ordering::Acquire, ) { - Ok(_) => { - // Consumed this thread's stop request bit. - s.stop_requested.store(false, Ordering::Release); - } - Err(THREAD_DETACHED) => { - // Leaving VM; caller will re-check on next entry. - super::stw_trace(format_args!("suspend skip DETACHED")); - return; - } + Ok(_) => break, Err(THREAD_SUSPENDED) => { - // Already parked by another path. - s.stop_requested.store(false, Ordering::Release); - super::stw_trace(format_args!("suspend skip already-suspended")); - return; + let extra_wait = wait_while_suspended(s); + stw.add_suspend_wait_yields(extra_wait); } + Err(THREAD_ATTACHED) => break, Err(state) => { - debug_assert!(false, "unexpected thread state in suspend: {state}"); - return; + debug_assert!(false, "unexpected post-suspend state: {state}"); + break; } } - super::stw_trace(format_args!("suspend ATTACHED->SUSPENDED")); - - // Re-check: if start_the_world already ran (cleared `requested`), - // no one will set us back to DETACHED — we must self-recover. - if !stw.requested.load(Ordering::Acquire) { - s.state.store(THREAD_ATTACHED, Ordering::Release); - s.stop_requested.store(false, Ordering::Release); - super::stw_trace(format_args!("suspend abort requested-cleared")); - return; - } - - // Notify the stop-the-world requester that we've parked - stw.notify_suspended(); - super::stw_trace(format_args!("suspend notified-requester")); - - // Wait until start_the_world sets us back to DETACHED - let wait_yields = wait_while_suspended(s); - stw.add_suspend_wait_yields(wait_yields); - - // Re-attach (DETACHED → ATTACHED), tstate_wait_attach CAS loop. - loop { - match s.state.compare_exchange( - THREAD_DETACHED, - THREAD_ATTACHED, - Ordering::AcqRel, - Ordering::Acquire, - ) { - Ok(_) => break, - Err(THREAD_SUSPENDED) => { - let extra_wait = wait_while_suspended(s); - stw.add_suspend_wait_yields(extra_wait); - } - Err(THREAD_ATTACHED) => break, - Err(state) => { - debug_assert!(false, "unexpected post-suspend state: {state}"); - break; - } - } - } - s.stop_requested.store(false, Ordering::Release); - super::stw_trace(format_args!("suspend resume -> ATTACHED")); } + s.stop_requested.store(false, Ordering::Release); + super::stw_trace(format_args!("suspend resume -> ATTACHED")); }); } -#[cfg(all(unix, feature = "threading"))] +#[cfg(feature = "threading")] #[inline] #[must_use] pub fn stop_requested_for_current_thread() -> bool { + CURRENT_STOP_REQUESTED.with(|cached| { + let flag = cached.get(); + // SAFETY: the pointer is non-null only while `CURRENT_THREAD_SLOT` + // holds the `Arc` that owns the flag; both are cleared + // together in `cleanup_current_thread_frames`. + !flag.is_null() && unsafe { &*flag }.load(Ordering::Relaxed) + }) +} + +/// Whether the QSBR subsystem asked this thread to pass a checkpoint. +/// A missed or racing read of this flag is harmless: the pending +/// retirement is still processed at the next checkpoint or by the GC +/// backstop. +#[cfg(feature = "threading")] +pub(crate) fn qsbr_break_requested() -> bool { CURRENT_THREAD_SLOT.with(|slot| { slot.borrow() .as_ref() - .is_some_and(|s| s.stop_requested.load(Ordering::Relaxed)) + .is_some_and(|s| s.qsbr.requested.load(Ordering::Relaxed)) }) } +/// Pass a QSBR checkpoint: the calling thread holds no borrowed cache +/// pointers here (instruction boundary), so mark it quiescent and try to +/// free retired allocations. +#[cfg(feature = "threading")] +pub(crate) fn qsbr_checkpoint() { + use crate::object::qsbr::QSBR; + CURRENT_THREAD_SLOT.with(|slot| { + if let Some(s) = slot.borrow().as_ref() { + s.qsbr.requested.store(false, Ordering::Relaxed); + QSBR.quiescent_state(&s.qsbr); + } + }); + QSBR.process(); +} + +/// Debug check: lock-free type-cache reads are only sound on threads that +/// are registered with QSBR and currently ATTACHED. +#[cfg(all(feature = "threading", debug_assertions))] +pub(crate) fn debug_assert_current_thread_attached() { + CURRENT_THREAD_SLOT.with(|slot| { + if let Some(s) = slot.borrow().as_ref() { + debug_assert_eq!( + s.state.load(Ordering::Relaxed), + THREAD_ATTACHED, + "type cache read while thread not ATTACHED" + ); + } + }); +} + /// Push a frame pointer onto the current thread's shared frame stack. /// The pointed-to frame must remain alive until the matching pop. -#[cfg(feature = "threading")] +/// +/// Only used on non-unix threading builds; unix builds publish the top frame +/// through `set_current_frame` writing `ThreadSlot::top_frame`. +#[cfg(all(not(unix), feature = "threading"))] pub fn push_thread_frame(fp: FramePtr) { CURRENT_THREAD_SLOT.with(|slot| { if let Some(s) = slot.borrow().as_ref() { @@ -460,7 +793,7 @@ pub fn push_thread_frame(fp: FramePtr) { /// Pop a frame from the current thread's shared frame stack. /// Called when a frame is exited. -#[cfg(feature = "threading")] +#[cfg(all(not(unix), feature = "threading"))] pub fn pop_thread_frame() { CURRENT_THREAD_SLOT.with(|slot| { if let Some(s) = slot.borrow().as_ref() { @@ -474,17 +807,54 @@ pub fn pop_thread_frame() { }); } -/// Set the current thread's top frame pointer for signal-safe traceback walking. -/// Returns the previous frame pointer so it can be restored on pop. -pub fn set_current_frame(frame: *const Frame) -> *const Frame { - CURRENT_FRAME.with(|c| c.swap(frame as *mut Frame, Ordering::Relaxed) as *const Frame) +/// Set the current thread's top InterpreterFrame pointer. +/// Returns the previous pointer so it can be restored on pop. +#[must_use] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub fn set_current_frame(frame: *const InterpreterFrame) -> *const InterpreterFrame { + // Publish the top frame for cross-thread readers (faulthandler, + // sys._current_frames). + #[cfg(feature = "threading")] + { + CURRENT_TOP_IFRAME_SLOT.with(|slot| { + let slot = slot.get(); + if !slot.is_null() { + unsafe { &*slot }.store(frame as usize, Ordering::Relaxed); + } + }); + #[cfg(unix)] + CURRENT_TOP_FRAME_SLOT.with(|slot| { + let slot = slot.get(); + if !slot.is_null() { + let fo_ptr = if frame.is_null() { + core::ptr::null_mut() + } else { + let frame_obj = unsafe { (*frame).frame_obj() }; + frame_obj.map_or(core::ptr::null_mut(), |py| { + py as *const Py as *mut Py + }) + }; + unsafe { &*slot }.store(fo_ptr, Ordering::Relaxed); + } + }); + } + CURRENT_FRAME.with(|c| c.swap(frame as usize, Ordering::Relaxed)) as *const InterpreterFrame } -/// Get the current thread's top frame pointer. +/// Lightweight version that only writes to TLS CURRENT_FRAME, returning +/// the previous value. Does not update cross-thread top_frame (that's +/// updated by `set_current_frame` for FrameObject-based calls). +#[inline(always)] +#[must_use] +pub fn set_current_frame_nosave(frame: *const InterpreterFrame) -> *const InterpreterFrame { + CURRENT_FRAME.with(|c| c.swap(frame as usize, Ordering::Relaxed)) as *const InterpreterFrame +} + +/// Get the current thread's top InterpreterFrame pointer. /// Used by faulthandler's signal handler to start traceback walking. #[must_use] -pub fn get_current_frame() -> *const Frame { - CURRENT_FRAME.with(|c| c.load(Ordering::Relaxed) as *const Frame) +pub fn get_current_frame() -> *const InterpreterFrame { + CURRENT_FRAME.with(|c| c.load(Ordering::Relaxed)) as *const InterpreterFrame } /// Update the current thread's exception slot atomically (no locks). @@ -511,16 +881,21 @@ pub fn get_all_current_exceptions(vm: &VirtualMachine) -> Vec<(u64, Option registry.remove(&thread_id), @@ -541,7 +916,6 @@ pub fn cleanup_current_thread_frames(vm: &VirtualMachine) { None }; - #[cfg(all(unix, feature = "threading"))] if let Some(slot) = &_removed && vm.state.stop_the_world.requested.load(Ordering::Acquire) && thread_id != vm.state.stop_the_world.requester_ident() @@ -551,31 +925,87 @@ pub fn cleanup_current_thread_frames(vm: &VirtualMachine) { // Unblock requester countdown progress. vm.state.stop_the_world.notify_thread_gone(); } + + // If CURRENT pointed at the cleaned slot, clear it (and top-frame cache). CURRENT_THREAD_SLOT.with(|s| { - *s.borrow_mut() = None; + let clear = match (s.borrow().as_ref(), slot_to_clean.as_ref()) { + (Some(cur), Some(cleaned)) => Arc::ptr_eq(cur, cleaned), + (Some(_), None) => false, + (None, _) => false, + }; + if clear { + *s.borrow_mut() = None; + #[cfg(all(unix, feature = "threading"))] + CURRENT_TOP_FRAME_SLOT.with(|c| c.set(core::ptr::null())); + #[cfg(feature = "threading")] + CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(core::ptr::null())); + #[cfg(feature = "threading")] + CURRENT_STOP_REQUESTED.with(|c| c.set(core::ptr::null())); + } }); } /// Reinitialize thread slot after fork. Called in child process. /// Creates a fresh slot and registers it for the current thread, -/// preserving the current thread's frames from `vm.frames`. +/// preserving the current thread's frames from the signal-safe frame chain. /// /// Precondition: `reinit_locks_after_fork()` has already reset all /// VmState locks to unlocked. #[cfg(feature = "threading")] pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) { let current_ident = crate::stdlib::_thread::get_ident(); - let current_frames: Vec = vm.frames.borrow().clone(); + // On non-unix, rebuild the shared frame stack (bottom-to-top) from the + // current thread's frame chain, which walks top-to-bottom via `previous`. + #[cfg(not(unix))] + let current_frames: Vec = { + let mut current_frames = Vec::new(); + let mut cur = get_current_frame(); + while !cur.is_null() { + // SAFETY: the forking thread's chain frames are alive. + let iframe = unsafe { &*cur }; + if let Some(fo) = iframe.frame_obj() { + current_frames.push(FramePtr(unsafe { + NonNull::new_unchecked(fo as *const _ as *mut _) + })); + } + cur = iframe.previous.load(Ordering::Relaxed) as *const InterpreterFrame; + } + current_frames.reverse(); + current_frames + }; + #[cfg(unix)] + let top_fo_ptr = { + let top_iframe = get_current_frame(); + if top_iframe.is_null() { + core::ptr::null_mut() + } else { + match unsafe { (*top_iframe).frame_obj() } { + Some(fo) => fo as *const Py as *mut Py, + None => core::ptr::null_mut(), + } + } + }; + let top_iframe_ptr = get_current_frame() as usize; let new_slot = Arc::new(ThreadSlot { + // The surviving child thread keeps executing its current frame chain. + // Only publish heavy frames for signal safety. + #[cfg(unix)] + top_frame: AtomicPtr::new(top_fo_ptr), + top_iframe: AtomicUsize::new(top_iframe_ptr), + #[cfg(not(unix))] frames: parking_lot::Mutex::new(current_frames), exception: crate::PyAtomicRef::from(vm.topmost_exception()), - #[cfg(unix)] state: core::sync::atomic::AtomicI32::new(THREAD_ATTACHED), - #[cfg(unix)] stop_requested: core::sync::atomic::AtomicBool::new(false), - #[cfg(unix)] thread: std::thread::current(), + qsbr: crate::object::qsbr::QSBR.register(), }); + #[cfg(all(unix, feature = "threading"))] + CURRENT_TOP_FRAME_SLOT.with(|c| c.set(&new_slot.top_frame)); + #[cfg(feature = "threading")] + CURRENT_TOP_IFRAME_SLOT.with(|c| c.set(&new_slot.top_iframe)); + #[cfg(feature = "threading")] + CURRENT_STOP_REQUESTED.with(|c| c.set(&new_slot.stop_requested)); // Lock is safe: reinit_locks_after_fork() already reset it to unlocked. let mut registry = vm.state.thread_frames.lock(); @@ -584,7 +1014,23 @@ pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) { drop(registry); CURRENT_THREAD_SLOT.with(|s| { - *s.borrow_mut() = Some(new_slot); + *s.borrow_mut() = Some(new_slot.clone()); + }); + INTERP_THREAD_SLOTS.with(|slots| { + slots.borrow_mut().insert(vm.state.interpreter_id, new_slot); + }); +} + +/// Drop this thread's cached slots for every interpreter except `keep_id`. +/// +/// After `fork()` only the calling thread survives, and the other +/// interpreters' registries are cleared; a cached slot would otherwise stay +/// current for an interpreter that no longer lists it, hiding the thread from +/// that interpreter's stop-the-world. The next enter builds a fresh slot. +#[cfg(feature = "threading")] +pub fn purge_other_interpreter_slots_after_fork(keep_id: i64) { + INTERP_THREAD_SLOTS.with(|slots| { + slots.borrow_mut().retain(|&id, _| id == keep_id); }); } @@ -671,8 +1117,7 @@ impl VirtualMachine { #[cfg(feature = "threading")] pub fn start_thread(&self, f: F) -> std::thread::JoinHandle where - F: FnOnce(&Self) -> R, - F: Send + 'static, + F: Send + 'static + FnOnce(&Self) -> R, R: Send + 'static, { let func = self.new_thread().make_spawn_func(f); @@ -711,7 +1156,6 @@ impl VirtualMachine { builtins: self.builtins.clone(), sys_module: self.sys_module.clone(), ctx: self.ctx.clone(), - frames: RefCell::new(vec![]), datastack: core::cell::UnsafeCell::new(crate::datastack::DataStack::new()), wasm_id: self.wasm_id.clone(), exceptions: RefCell::default(), @@ -720,6 +1164,7 @@ impl VirtualMachine { profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())), trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())), use_tracing: Cell::new(use_tracing), + tracing_depth: Cell::new(0), recursion_limit: self.recursion_limit.clone(), signal_handlers: core::cell::OnceCell::new(), signal_rx: None, @@ -727,6 +1172,8 @@ impl VirtualMachine { state: self.state.clone(), initialized: self.initialized, recursion_depth: Cell::new(0), + #[cfg(any(miri, target_env = "musl"))] + native_recursion_depth: Cell::new(0), c_stack_soft_limit: Cell::new(Self::calculate_c_stack_soft_limit()), async_gen_firstiter: RefCell::new(None), async_gen_finalizer: RefCell::new(None), @@ -734,6 +1181,8 @@ impl VirtualMachine { asyncio_running_task: RefCell::new(None), callable_cache: self.callable_cache.clone(), audit_hooks: RefCell::new(vec![]), + pending_tailcall_frame: Cell::new(None), + pending_tailcall_owner: core::cell::UnsafeCell::new(None), }; ThreadedVirtualMachine { vm } } diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index 30b8e6b1af0..82f382ca6d7 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -10,19 +10,20 @@ use rustpython_compiler_core::SourceLocation; use rustpython_compiler::{CompileError, ParseError}; use crate::{ - AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, builtins::{ - PyBaseException, PyBaseExceptionRef, PyBytesRef, PyDictRef, PyModule, PyOSError, PyStrRef, - PyType, PyTypeRef, + PyBaseException, PyBaseExceptionRef, PyBytesRef, PyDictRef, PyModule, PyOSError, + PyStopIteration, PyStrRef, PySystemExit, PyType, PyTypeRef, builtin_func::PyNativeFunction, descriptor::PyMethodDescriptor, tuple::{IntoPyTuple, PyTupleRef}, }, convert::{ToPyException, ToPyObject}, exceptions::OSErrorBuilder, - function::{IntoPyNativeFn, PyMethodFlags}, + function::{FuncArgs, IntoPyNativeFn, PyMethodFlags}, scope::Scope, set_attrs, + types::{Constructor, Initializer}, vm::VirtualMachine, }; @@ -54,19 +55,10 @@ impl SyntaxErrorInfo { Self { msg, narrow_caret } } - fn with_msg(&mut self, msg: &str) { - self.msg = msg.into(); - } - - #[cfg(feature = "parser")] - const fn with_narrow_caret(&mut self, narrow_caret: bool) { - self.narrow_caret = narrow_caret; - } - #[cfg(feature = "parser")] #[must_use] - const fn handle_expected_token(expected: &TokenKind, found: &TokenKind) -> &'static str { - match (*expected, *found) { + const fn handle_expected_token(expected: TokenKind, found: TokenKind) -> &'static str { + match (expected, found) { (TokenKind::Colon, TokenKind::Newline) => "expected ':'", (TokenKind::Lpar, _) => "expected '('", @@ -110,11 +102,11 @@ impl SyntaxErrorInfo { ParseErrorType::UnexpectedExpressionToken => format!("invalid syntax: {}", self.msg), ParseErrorType::ExpectedToken { expected, found } => { - Self::handle_expected_token(expected, found).into() + Self::handle_expected_token(*expected, *found).into() } ParseErrorType::InvalidStarredExpressionUsage => { - self.with_narrow_caret(true); + self.narrow_caret = true; "invalid syntax".into() } @@ -134,7 +126,7 @@ impl SyntaxErrorInfo { ParseErrorType::EmptyTypeParams => "Type parameter list cannot be empty".into(), ParseErrorType::InvalidStarPatternUsage => { - self.with_narrow_caret(true); + self.narrow_caret = true; "cannot use starred expression here".into() } @@ -179,12 +171,22 @@ impl SyntaxErrorInfo { | ParseErrorType::SimpleAndCompoundStatementOnSameLine | ParseErrorType::ExpectedExpression => "invalid syntax".into(), + ParseErrorType::OtherError(s) if s.starts_with("Expected an identifier") => { + "invalid syntax".into() + } + ParseErrorType::OtherError(s) - if s.starts_with("Expected an identifier, but found a keyword") => + if s.eq_ignore_ascii_case( + "Expected a type parameter or the end of the type parameter list", + ) => { "invalid syntax".into() } + ParseErrorType::OtherError(s) if s.eq_ignore_ascii_case("Expected a statement") => { + "invalid syntax".into() + } + ParseErrorType::OtherError(s) if s.eq_ignore_ascii_case( "bytes literal cannot be mixed with non-bytes literals", @@ -262,7 +264,7 @@ impl SyntaxErrorInfo { _ => return, }; - self.with_msg(&msg); + self.msg = msg; } } @@ -347,11 +349,29 @@ impl VirtualMachine { exc_type.name() ); - PyRef::new_ref( - PyBaseException::new(args, self), - exc_type, - Some(self.ctx.new_dict()), - ) + PyBaseException::new(args, self) + .into_ref_with_type_lazy_dict(self, exc_type) + .expect("vm.new_exception() called with an invalid exception type") + } + + /// Construct a built-in exception type that carries a payload, directly + /// (`py_new` + `slot_init`), without routing through `PyType::call`. + /// Only valid for a built-in `T` whose exact type is known at compile time. + pub fn new_payload_exception(&self, cls: PyTypeRef, args: FuncArgs) -> PyResult> + where + T: Constructor + Initializer, + { + debug_assert_eq!( + cls.slots.basicsize, + size_of::(), + "vm.new_payload_exception::<{}>() called with mismatched type '{}'", + core::any::type_name::(), + cls.name() + ); + let payload = T::py_new(&cls, args.clone(), self)?; + let exc = payload.into_ref_with_type_lazy_dict(self, cls)?; + T::slot_init(exc.as_object().to_owned(), args, self)?; + Ok(exc) } pub fn new_os_error(&self, msg: impl ToPyObject) -> PyRef { @@ -497,7 +517,7 @@ impl VirtualMachine { self.new_os_subtype_error(exc_type.to_owned(), Some(errno), msg) } - pub fn new_unicode_decode_error_real( + pub fn new_unicode_decode_error( &self, encoding: PyStrRef, object: PyBytesRef, @@ -576,6 +596,16 @@ impl VirtualMachine { source: Option<&str>, allow_incomplete: bool, ) -> PyBaseExceptionRef { + if matches!( + error, + crate::compiler::CompileError::Codegen(crate::compiler::codegen::error::CodegenError { + error: crate::compiler::codegen::error::CodegenErrorType::RecursionError, + .. + }) + ) { + return self.new_recursion_error(error.to_string()); + } + let incomplete_or_syntax = |allow| -> &'static Py { if allow { self.ctx.exceptions.incomplete_input_error @@ -687,7 +717,9 @@ impl VirtualMachine { raw_location, .. }) => { - if s.starts_with("Expected an indented block after") { + if s.starts_with("Expected an indented block after") + || s.starts_with("expected an indented block after") + { if allow_incomplete { // Check that all chars in the error are whitespace, if so, the source is // incomplete. Otherwise, we've found code that might violates @@ -717,6 +749,12 @@ impl VirtualMachine { } else { self.ctx.exceptions.indentation_error } + } else if allow_incomplete + && source.is_some_and(|source| { + raw_location.end().to_usize() >= source.len() && !source.ends_with('\n') + }) + { + self.ctx.exceptions.incomplete_input_error } else { self.ctx.exceptions.syntax_error } @@ -735,10 +773,32 @@ impl VirtualMachine { Some(line + "\n") } - let statement = source.and_then(|src| get_statement(src, error.location())); + let mut statement = source.and_then(|src| get_statement(src, error.location())); let mut msg = error.to_string(); - if let Some(msg) = msg.get_mut(..1) { + if !msg.starts_with("Exceeds the limit ") + && !msg.starts_with("Did you mean ") + && !msg.starts_with("Invalid star expression") + && !msg.starts_with("Function parameters cannot be parenthesized") + && !msg.starts_with("Lambda expression parameters cannot be parenthesized") + && !msg.starts_with("Cannot have two type comments on def") + && !msg.starts_with("Variable annotation syntax is") + && !msg.starts_with("The '@' operator is") + && !msg.starts_with("Async functions are") + && !msg.starts_with("Async comprehensions are") + && !msg.starts_with("Async for loops are") + && !msg.starts_with("Async with statements are") + && !msg.starts_with("Exception groups are") + && !msg.starts_with("Positional-only parameters are") + && !msg.starts_with("Pattern matching is") + && !msg.starts_with("Type statement is") + && !msg.starts_with("Type parameter lists are") + && !msg.starts_with("Type parameter defaults are") + && !msg.starts_with("Assignment expressions are") + && !msg.starts_with("Await expressions are") + && !msg.starts_with("Underscores in numeric literals are") + && let Some(msg) = msg.get_mut(..1) + { msg.make_ascii_lowercase(); } @@ -753,17 +813,36 @@ impl VirtualMachine { }; if syntax_error_type.is(self.ctx.exceptions.tab_error) { - syntax_error_info.with_msg("inconsistent use of tabs and spaces in indentation"); + syntax_error_info.msg = + String::from("inconsistent use of tabs and spaces in indentation"); + } else if syntax_error_type.is(self.ctx.exceptions.incomplete_input_error) { + syntax_error_info.msg = String::from("incomplete input"); } let SyntaxErrorInfo { msg, narrow_caret } = syntax_error_info; + let unterminated_triple_quoted_string = + msg.starts_with("unterminated triple-quoted string literal"); + let unexpected_eof_error = msg == "unexpected EOF while parsing"; + if unterminated_triple_quoted_string + && let Some(statement) = statement.as_mut() + && statement.ends_with('\n') + { + // CPython omits the parser-added final newline from SyntaxError.text. + statement.pop(); + } + let check_version_suite_error = msg.starts_with("Async functions are") + || msg.starts_with("Async for loops are") + || msg.starts_with("Async with statements are") + || msg.starts_with("Exception groups are") + || msg.starts_with("except expressions without parentheses are") + || msg.starts_with("Pattern matching is"); + let line_end_binary_operator_error = msg.starts_with("The '@' operator is"); let syntax_error = self.new_exception_msg(syntax_error_type, msg.into()); - let (lineno, offset) = error.python_location(); - let lineno = self.ctx.new_int(lineno); - let offset = self.ctx.new_int(offset); - + let (lineno_raw, offset_raw) = error.python_location(); + let lineno = self.ctx.new_int(lineno_raw); + let offset = self.ctx.new_int(offset_raw); set_attrs!( syntax_error.as_object(), self, unwrap, "lineno" => lineno, @@ -772,15 +851,25 @@ impl VirtualMachine { // Set end_lineno and end_offset if available if let Some((end_lineno, end_offset)) = error.python_end_location() { - let (end_lineno, end_offset) = if narrow_caret { + // EOF errors have no source span in CPython. + let no_end_offset = unexpected_eof_error + || (check_version_suite_error + && statement + .as_deref() + .and_then(|line| line.chars().next()) + .is_some_and(|ch| ch.is_ascii_whitespace())); + let (end_lineno, end_offset) = if no_end_offset { + (end_lineno, -1) + } else if line_end_binary_operator_error && end_offset == offset_raw { + (end_lineno, (end_offset + 1) as isize) + } else if narrow_caret { let (l, o) = error.python_location(); - (l, o + 1) + (l, (o + 1) as isize) } else { - (end_lineno, end_offset) + (end_lineno, end_offset as isize) }; let end_lineno = self.ctx.new_int(end_lineno); let end_offset = self.ctx.new_int(end_offset); - set_attrs!( syntax_error.as_object(), self, unwrap, "end_lineno" => end_lineno, @@ -837,22 +926,20 @@ impl VirtualMachine { exc } - pub fn new_stop_iteration(&self, value: Option) -> PyBaseExceptionRef { - let dict = self.ctx.new_dict(); - let args = if let Some(value) = value { - // manually set `value` attribute like StopIteration.__init__ - dict.set_item("value", value.clone(), self) - .expect("dict.__setitem__ never fails"); - vec![value] - } else { - Vec::new() - }; + pub fn new_system_exit(&self, args: FuncArgs) -> PyBaseExceptionRef { + self.new_payload_exception::(self.ctx.exceptions.system_exit.to_owned(), args) + .expect("SystemExit construction from internal args is infallible") + .upcast() + } - PyRef::new_ref( - PyBaseException::new(args, self), + pub fn new_stop_iteration(&self, value: Option) -> PyBaseExceptionRef { + let args: FuncArgs = value.map(|v| vec![v]).unwrap_or_default().into(); + self.new_payload_exception::( self.ctx.exceptions.stop_iteration.to_owned(), - Some(dict), + args, ) + .expect("StopIteration construction from internal args is infallible") + .upcast() } fn new_downcast_error( @@ -909,12 +996,6 @@ impl VirtualMachine { define_exception_fn!(fn new_type_error, type_error, TypeError); define_exception_fn!(fn new_system_error, system_error, SystemError); - // TODO: remove & replace with new_unicode_decode_error_real - define_exception_fn!(fn new_unicode_decode_error, unicode_decode_error, UnicodeDecodeError); - - // TODO: remove & replace with new_unicode_encode_error_real - define_exception_fn!(fn new_unicode_encode_error, unicode_encode_error, UnicodeEncodeError); - define_exception_fn!(fn new_value_error, value_error, ValueError); define_exception_fn!(fn new_buffer_error, buffer_error, BufferError); diff --git a/crates/vm/src/vm/vm_object.rs b/crates/vm/src/vm/vm_object.rs index 8a7be140dc8..aa68f7f4dee 100644 --- a/crates/vm/src/vm/vm_object.rs +++ b/crates/vm/src/vm/vm_object.rs @@ -47,8 +47,7 @@ impl VirtualMachine { /// Returns true if the file object's `closed` attribute is truthy. fn file_is_closed(&self, file: &PyObject) -> bool { file.get_attr("closed", self) - .ok() - .is_some_and(|v| v.try_to_bool(self).unwrap_or(false)) + .is_ok_and(|v| v.try_to_bool(self).unwrap_or_default()) } pub(crate) fn flush_std(&self) -> i32 { diff --git a/crates/vm/src/vm/vm_ops.rs b/crates/vm/src/vm/vm_ops.rs index d25e7119df5..dc31e508218 100644 --- a/crates/vm/src/vm/vm_ops.rs +++ b/crates/vm/src/vm/vm_ops.rs @@ -168,6 +168,27 @@ impl VirtualMachine { } } + /// `vec![0; len]` for a length that came from Python, where a request too + /// large to satisfy is a `MemoryError` rather than an aborted process. + /// + /// The bytes are left for the allocator to zero, so a large request costs + /// no more than the pages that are actually written to. + pub fn new_zeroed_bytes(&self, len: usize) -> PyResult> { + if len == 0 { + return Ok(Vec::new()); + } + let layout = + core::alloc::Layout::array::(len).map_err(|_| self.new_memory_error(""))?; + // SAFETY: `len` is not zero, so neither is the layout's size. + let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; + if ptr.is_null() { + return Err(self.new_memory_error("")); + } + // SAFETY: `ptr` was just allocated by the global allocator for exactly + // this many bytes, and every one of them is initialized to zero. + Ok(unsafe { Vec::from_raw_parts(ptr, len, len) }) + } + /// Calling scheme used for binary operations: /// /// Order operations are tried until either a valid result or error: @@ -180,13 +201,13 @@ impl VirtualMachine { // Number slots are inherited, direct access is O(1) let slot_a = class_a.slots.as_number.left_binary_op(op_slot); - let slot_a_addr = slot_a.map(|x| x as usize); + let slot_a_addr = slot_a.map(|x| crate::types::fn_addr(x)); let mut slot_b = None; let left_b_addr = if class_a.is(class_b) { slot_a_addr } else { let slot_bb = class_b.slots.as_number.right_binary_op(op_slot); - if slot_bb.map(|x| x as usize) != slot_a_addr { + if slot_bb.map(|x| crate::types::fn_addr(x)) != slot_a_addr { slot_b = slot_bb; } @@ -194,7 +215,7 @@ impl VirtualMachine { .slots .as_number .left_binary_op(op_slot) - .map(|x| x as usize) + .map(|x| crate::types::fn_addr(x)) }; if let Some(slot_a) = slot_a { @@ -302,13 +323,13 @@ impl VirtualMachine { // Number slots are inherited, direct access is O(1) let slot_a = class_a.slots.as_number.left_ternary_op(op_slot); - let slot_a_addr = slot_a.map(|x| x as usize); + let slot_a_addr = slot_a.map(|x| crate::types::fn_addr(x)); let mut slot_b = None; let left_b_addr = if class_a.is(class_b) { slot_a_addr } else { let slot_bb = class_b.slots.as_number.right_ternary_op(op_slot); - if slot_bb.map(|x| x as usize) != slot_a_addr { + if slot_bb.map(|x| crate::types::fn_addr(x)) != slot_a_addr { slot_b = slot_bb; } @@ -316,7 +337,7 @@ impl VirtualMachine { .slots .as_number .left_ternary_op(op_slot) - .map(|x| x as usize) + .map(|x| crate::types::fn_addr(x)) }; if let Some(slot_a) = slot_a { @@ -568,7 +589,7 @@ impl VirtualMachine { formatted.downcast().map_err(|result| { self.new_type_error(format!( "__format__ must return a str, not {}", - &result.class().name() + result.class().name() )) }) } diff --git a/crates/vm/src/warn.rs b/crates/vm/src/warn.rs index 6500e8de0f6..d5729858dcd 100644 --- a/crates/vm/src/warn.rs +++ b/crates/vm/src/warn.rs @@ -513,7 +513,7 @@ fn show_warning( } /// Check if a frame's filename starts with any of the given prefixes. -fn is_filename_to_skip(frame: &crate::frame::Frame, prefixes: &PyTupleRef) -> bool { +fn is_filename_to_skip(frame: &crate::frame::FrameObject, prefixes: &PyTupleRef) -> bool { let filename = frame.f_code().co_filename(); let filename_bytes = filename.as_bytes(); prefixes.iter().any(|prefix| { @@ -523,15 +523,15 @@ fn is_filename_to_skip(frame: &crate::frame::Frame, prefixes: &PyTupleRef) -> bo }) } -/// Like Frame::next_external_frame but also skips frames matching prefixes. +/// Like FrameObject::next_external_frame but also skips frames matching prefixes. fn next_external_frame_with_skip( - frame: &crate::frame::FrameRef, + frame: &crate::frame::FrameObjectRef, skip_file_prefixes: Option<&PyTupleRef>, vm: &VirtualMachine, -) -> Option { +) -> Option { let mut f = frame.f_back(vm); loop { - let current: crate::frame::FrameRef = f.take()?; + let current: crate::frame::FrameObjectRef = f.take()?; if current.is_internal_frame() || skip_file_prefixes.is_some_and(|p| is_filename_to_skip(¤t, p)) { @@ -549,7 +549,9 @@ fn setup_context( skip_file_prefixes: Option<&PyTupleRef>, vm: &VirtualMachine, ) -> PyResult<(PyStrRef, usize, Option, PyObjectRef)> { - let mut f = vm.current_frame(); + // Materialize the topmost frame (including light frames) so stack + // level counting is correct across the full Python frame chain. + let mut f = crate::frame::current_thread_frame_materialize(vm); // Stack level comparisons to Python code is off by one as there is no // warnings-related stack level to avoid. @@ -576,10 +578,18 @@ fn setup_context( } let (globals, filename, lineno) = if let Some(f) = f { - (f.globals.clone(), f.code.source_path(), f.f_lineno()) + ( + f.iframe().globals().to_owned(), + f.iframe().code().source_path(), + f.f_lineno(), + ) } else if let Some(frame) = vm.current_frame() { // We have a frame but it wasn't found during stack walking - (frame.globals.clone(), vm.ctx.intern_str(""), 1) + ( + frame.iframe().globals().to_owned(), + vm.ctx.intern_str(""), + 1, + ) } else { // No frames on the stack - use sys.__dict__ (interp->sysdict) let globals = vm diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index 4150beaa81c..5a5e8d77fd6 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -20,6 +20,7 @@ no-start-func = [] rustpython-common = { workspace = true } rustpython-pylib = { workspace = true, optional = true } rustpython-stdlib = { workspace = true, default-features = false, optional = true } +ruff_text_size = { workspace = true } # make sure no threading! otherwise wasm build will fail rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde", "wasmbind"] } diff --git a/crates/wasm/Lib/asyncweb.py b/crates/wasm/Lib/asyncweb.py index 40bd843499b..f0e7983f775 100644 --- a/crates/wasm/Lib/asyncweb.py +++ b/crates/wasm/Lib/asyncweb.py @@ -64,8 +64,7 @@ async def _main_wrapper(coro): import traceback import sys - # TODO: sys.stderr on wasm - traceback.print_exc(file=sys.stdout) + traceback.print_exc(file=sys.stderr) def _resolve(prom): diff --git a/crates/wasm/README.md b/crates/wasm/README.md index 3a755009205..1878614be16 100644 --- a/crates/wasm/README.md +++ b/crates/wasm/README.md @@ -34,6 +34,9 @@ pyEval(code, options?); - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the native print function, and it will be `console.log` when giving `undefined` or "console", and it will be a dumb function when giving null. +- `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace + `sys.stderr`, and it will be `console.error` when giving `undefined` or + "console". ## License diff --git a/crates/wasm/src/convert.rs b/crates/wasm/src/convert.rs index 17ad5b62946..9349c727942 100644 --- a/crates/wasm/src/convert.rs +++ b/crates/wasm/src/convert.rs @@ -2,10 +2,13 @@ use crate::js_module; use crate::vm_class::{WASMVirtualMachine, stored_vm_from_wasm}; -use js_sys::{Array, ArrayBuffer, Object, Promise, Reflect, SyntaxError, Uint8Array}; +use js_sys::{ + Array, ArrayBuffer, JsString, Map, Object, Promise, Reflect, SyntaxError, Uint8Array, +}; +use rustpython_common::wtf8::{Wtf8, Wtf8Buf}; use rustpython_vm::{ AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromBorrowedObject, VirtualMachine, - builtins::{PyBaseException, PyBaseExceptionRef}, + builtins::{PyBaseException, PyBaseExceptionRef, PyDict, PyList, PyStr, PyTuple}, compiler::{CompileError, ParseError, parser::LexicalErrorType, parser::ParseErrorType}, exceptions, function::{ArgBytesLike, FuncArgs}, @@ -13,6 +16,26 @@ use rustpython_vm::{ }; use wasm_bindgen::{JsCast, closure::Closure, prelude::*}; +pub(crate) fn js_string_to_wtf8(value: &JsString) -> Wtf8Buf { + Wtf8Buf::from_wide(&value.iter().collect::>()) +} + +fn wtf8_to_js_string(value: &Wtf8) -> JsString { + const CHUNK_SIZE: usize = 8192; + + if let Ok(value) = value.as_str() { + return value.into(); + } + + value + .encode_wide() + .collect::>() + .chunks(CHUNK_SIZE) + .map(JsString::from_char_code) + .collect::() + .join("") +} + #[wasm_bindgen(inline_js = r" export class PyError extends Error { constructor(info) { @@ -121,7 +144,7 @@ pub fn py_to_js(vm: &VirtualMachine, py_obj: PyObjectRef) -> JsValue { let (key, val) = pair?; py_func_args .kwargs - .insert(js_sys::JsString::from(key).into(), js_to_py(vm, val)); + .insert(js_string_to_wtf8(&key.into()), js_to_py(vm, val)); } } let result = py_obj.call(py_func_args, vm); @@ -148,17 +171,44 @@ pub fn py_to_js(vm: &VirtualMachine, py_obj: PyObjectRef) -> JsValue { } if let Ok(bytes) = ArgBytesLike::try_from_borrowed_object(vm, &py_obj) { - bytes.with_ref(|bytes| unsafe { + return bytes.with_ref(|bytes| unsafe { // `Uint8Array::view` is an `unsafe fn` because it provides // a direct view into the WASM linear memory; if you were to allocate // something with Rust that view would probably become invalid. It's safe // because we then copy the array using `Uint8Array::slice`. let view = Uint8Array::view(bytes); view.slice(0, bytes.len() as u32).into() - }) + }); + } + py_serde_to_js(vm, &py_obj).unwrap_or(JsValue::UNDEFINED) +} + +fn py_serde_to_js( + vm: &VirtualMachine, + py_obj: &PyObjectRef, +) -> Result { + if let Some(value) = py_obj.downcast_ref::() { + Ok(wtf8_to_js_string(value.as_wtf8()).into()) + } else if let Some(value) = py_obj.downcast_ref::() { + let array = Array::new(); + for item in value.borrow_vec().iter() { + array.push(&py_serde_to_js(vm, item)?); + } + Ok(array.into()) + } else if let Some(value) = py_obj.downcast_ref::() { + let array = Array::new(); + for item in value { + array.push(&py_serde_to_js(vm, item)?); + } + Ok(array.into()) + } else if let Some(value) = py_obj.downcast_ref::() { + let map = Map::new(); + for (key, value) in value { + map.set(&py_serde_to_js(vm, &key)?, &py_serde_to_js(vm, &value)?); + } + Ok(map.into()) } else { - py_serde::serialize(vm, &py_obj, &serde_wasm_bindgen::Serializer::new()) - .unwrap_or(JsValue::UNDEFINED) + py_serde::serialize(vm, py_obj, &serde_wasm_bindgen::Serializer::new()) } } @@ -196,6 +246,15 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef { .map(|val| js_to_py(vm, val.expect("Iteration over array failed"))) .collect(); vm.ctx.new_list(elems).into() + } else if let Some(map) = js_val.dyn_ref::() { + let dict = vm.ctx.new_dict(); + for entry in map.entries() { + let entry = Array::from(&entry.expect("Iteration over map failed")); + let key = js_to_py(vm, entry.get(0)); + dict.set_item(&*key, js_to_py(vm, entry.get(1)), vm) + .unwrap(); + } + dict.into() } else if ArrayBuffer::is_view(&js_val) || js_val.is_instance_of::() { // unchecked_ref because if it's not an ArrayBuffer it could either be a TypedArray // or a DataView, but they all have a `buffer` property @@ -213,12 +272,8 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef { for pair in object_entries(&Object::from(js_val)) { let (key, val) = pair.expect("iteration over object to not fail"); let py_val = js_to_py(vm, val); - dict.set_item( - String::from(js_sys::JsString::from(key)).as_str(), - py_val, - vm, - ) - .unwrap(); + dict.set_item(&*js_string_to_wtf8(&key.into()), py_val, vm) + .unwrap(); } dict.into() } @@ -229,7 +284,7 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef { move |args: FuncArgs, vm: &VirtualMachine| -> PyResult { let this = Object::new(); for (k, v) in args.kwargs { - Reflect::set(&this, &k.into(), &py_to_js(vm, v)) + Reflect::set(&this, &wtf8_to_js_string(&k).into(), &py_to_js(vm, v)) .expect("property to be settable"); } let js_args = args @@ -248,6 +303,8 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef { } else if js_val.is_undefined() { // Because `JSON.stringify(undefined)` returns undefined vm.ctx.none() + } else if js_val.is_string() { + vm.ctx.new_str(js_string_to_wtf8(&js_val.into())).into() } else { py_serde::deserialize(vm, serde_wasm_bindgen::Deserializer::from(js_val)) .unwrap_or_else(|_| vm.ctx.none()) diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 99668df2855..041cb864ef2 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -66,11 +66,16 @@ pub mod eval { }; vm.set_stdout(Reflect::get(&options, &"stdout".into())?)?; + vm.set_stderr(Reflect::get(&options, &"stderr".into())?)?; if let Some(js_vars) = js_vars { vm.add_to_scope("js_vars".into(), js_vars.into())?; } - vm.run(source, mode, None) + if matches!(mode, Mode::Single) { + vm.exec_single(source, None) + } else { + vm.run(source, mode, None) + } } /// Evaluate Python code @@ -89,6 +94,8 @@ pub mod eval { /// - `stdout?`: `"console" | ((out: string) => void) | null`: A function to replace the /// native print native print function, and it will be `console.log` when giving /// `undefined` or "console", and it will be a dumb function when giving null. + /// - `stderr?`: `"console" | ((out: string) => void) | null`: A function to replace + /// `sys.stderr`, and it will be `console.error` when giving `undefined` or "console". #[wasm_bindgen(js_name = pyEval)] pub fn eval_py(source: &str, options: Option) -> Result { run_py(source, options, Mode::Eval) diff --git a/crates/wasm/src/vm_class.rs b/crates/wasm/src/vm_class.rs index 08cb49ecfca..80f3ece1358 100644 --- a/crates/wasm/src/vm_class.rs +++ b/crates/wasm/src/vm_class.rs @@ -6,9 +6,14 @@ use crate::{ use alloc::rc::{Rc, Weak}; use core::cell::RefCell; use js_sys::{Object, TypeError}; +use ruff_text_size::Ranged; use rustpython_vm::{ - Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine, builtins::PyWeak, - compiler::Mode, function::ArgMapping, scope::Scope, + Interpreter, PyObjectRef, PyRef, PyResult, Settings, VirtualMachine, + builtins::PyWeak, + compiler::{self, Mode}, + function::ArgMapping, + scope::Scope, + vm::VmCompileError, }; use std::collections::HashMap; use wasm_bindgen::prelude::*; @@ -21,6 +26,25 @@ pub(crate) struct StoredVirtualMachine { held_objects: RefCell>, } +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> { + let module = compiler::parser::parse_module(source).ok()?.into_syntax(); + module + .body + .iter() + .map(|stmt| { + let range = stmt.range(); + source.get(range.start().to_usize()..range.end().to_usize()) + }) + .collect() +} + #[pymodule] mod _window { use super::{js_module, wasm_builtins}; @@ -225,31 +249,59 @@ impl WASMVirtualMachine { #[wasm_bindgen(js_name = setStdout)] pub fn set_stdout(&self, stdout: JsValue) -> Result<(), JsValue> { + self.set_stdstream( + "stdout", + "JSStdout", + stdout, + wasm_builtins::sys_stdout_write_console, + ) + } + + #[wasm_bindgen(js_name = setStderr)] + pub fn set_stderr(&self, stderr: JsValue) -> Result<(), JsValue> { + self.set_stdstream( + "stderr", + "JSStderr", + stderr, + wasm_builtins::sys_stderr_write_console, + ) + } + + fn set_stdstream( + &self, + attr: &'static str, + class_name: &'static str, + stream: JsValue, + console_write: fn(&str, &VirtualMachine) -> PyResult<()>, + ) -> Result<(), JsValue> { self.with_vm(|vm, _| { - fn error() -> JsValue { - TypeError::new("Unknown stdout option, please pass a function or 'console'").into() + fn error(attr: &str) -> JsValue { + TypeError::new(&format!( + "Unknown {attr} option, please pass a function or 'console'" + )) + .into() } - use wasm_builtins::make_stdout_object; - let stdout: PyObjectRef = if let Some(s) = stdout.as_string() { + use wasm_builtins::make_stdstream_object; + let stream: PyObjectRef = if let Some(s) = stream.as_string() { match s.as_str() { - "console" => make_stdout_object(vm, wasm_builtins::sys_stdout_write_console), - _ => return Err(error()), + "console" => make_stdstream_object(vm, class_name, console_write), + _ => return Err(error(attr)), } - } else if stdout.is_function() { - let func = js_sys::Function::from(stdout); - make_stdout_object(vm, move |data, vm| { + } else if stream.is_function() { + let func = js_sys::Function::from(stream); + make_stdstream_object(vm, class_name, move |data, vm| { func.call1(&JsValue::UNDEFINED, &data.into()) .map_err(|err| convert::js_py_typeerror(vm, err))?; Ok(()) }) - } else if stdout.is_null() { - make_stdout_object(vm, |_, _| Ok(())) - } else if stdout.is_undefined() { - make_stdout_object(vm, wasm_builtins::sys_stdout_write_console) + } else if stream.is_null() { + make_stdstream_object(vm, class_name, |_, _| Ok(())) + } else if stream.is_undefined() { + make_stdstream_object(vm, class_name, console_write) } else { - return Err(error()); + return Err(error(attr)); }; - vm.sys_module.set_attr("stdout", stdout, vm).unwrap(); + vm.sys_module.set_attr(attr, stream, vm).unwrap(); Ok(()) })? } @@ -263,8 +315,8 @@ impl WASMVirtualMachine { ) -> Result<(), JsValue> { self.with_vm(|vm, _| { let code = vm - .compile(source, Mode::Exec, &name) - .map_err(convert::syntax_err)?; + .compile(source, Mode::Exec, name.as_str()) + .map_err(|err| compile_err_to_js(vm, err))?; let attrs = vm.ctx.new_dict(); attrs .set_item("__name__", vm.new_pyobj(name.as_str()), vm) @@ -273,9 +325,12 @@ impl WASMVirtualMachine { if let Some(imports) = imports { for entry in convert::object_entries(&imports) { let (key, value) = entry?; - let key: String = Object::from(key).to_string().into(); attrs - .set_item(key.as_str(), convert::js_to_py(vm, value), vm) + .set_item( + &*convert::js_string_to_wtf8(&key.into()), + convert::js_to_py(vm, value), + vm, + ) .into_js(vm)?; } } @@ -304,10 +359,10 @@ impl WASMVirtualMachine { let py_module = vm.new_module(&name, vm.ctx.new_dict(), None); for entry in convert::object_entries(&module) { let (key, value) = entry?; - let key = Object::from(key).to_string(); - extend_module!(vm, &py_module, { - String::from(key) => convert::js_to_py(vm, value), - }); + let key = vm.ctx.new_str(convert::js_string_to_wtf8(&key.into())); + py_module + .set_attr(&key, convert::js_to_py(vm, value), vm) + .into_js(vm)?; } let sys_modules = vm.sys_module.get_attr("modules", vm).into_js(vm)?; @@ -327,13 +382,46 @@ impl WASMVirtualMachine { ) -> Result { self.with_vm(|vm, StoredVirtualMachine { scope, .. }| { let source_path = source_path.unwrap_or_else(|| "".to_owned()); - let code = vm.compile(source, mode, &source_path); - let code = code.map_err(convert::syntax_err)?; + let code = vm.compile(source, mode, source_path.as_str()); + let code = code.map_err(|err| compile_err_to_js(vm, err))?; let result = vm.run_code_obj(code, scope.clone()); convert::pyresult_to_js_result(vm, result) })? } + pub(crate) fn run_single( + &self, + source: &str, + source_path: Option, + ) -> Result { + self.with_vm(|vm, StoredVirtualMachine { scope, .. }| { + let source_path = source_path.unwrap_or_else(|| "".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) -> Result { self.run(source, Mode::Exec, source_path) } @@ -348,6 +436,6 @@ impl WASMVirtualMachine { source: &str, source_path: Option, ) -> Result { - self.run(source, Mode::Single, source_path) + self.run_single(source, source_path) } } diff --git a/crates/wasm/src/wasm_builtins.rs b/crates/wasm/src/wasm_builtins.rs index efbc03c39ce..ae2ffa63dbe 100644 --- a/crates/wasm/src/wasm_builtins.rs +++ b/crates/wasm/src/wasm_builtins.rs @@ -16,19 +16,20 @@ pub fn sys_stdout_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<() Ok(()) } -pub fn make_stdout_object( +pub fn sys_stderr_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<()> { + console::error_1(&data.into()); + Ok(()) +} + +pub fn make_stdstream_object( vm: &VirtualMachine, + name: &'static str, write_f: impl Fn(&str, &VirtualMachine) -> PyResult<()> + 'static, ) -> PyObjectRef { let ctx = &vm.ctx; // there's not really any point to storing this class so that there's a consistent type object, // we just want a half-decent repr() output - let cls = PyRef::leak(py_class!( - ctx, - "JSStdout", - vm.ctx.types.object_type.to_owned(), - {} - )); + let cls = PyRef::leak(py_class!(ctx, name, vm.ctx.types.object_type.to_owned(), {})); let write_method = vm.new_method( "write", cls, diff --git a/crates/wtf8/Cargo.toml b/crates/wtf8/Cargo.toml index 110b54ad0ca..20bf824898a 100644 --- a/crates/wtf8/Cargo.toml +++ b/crates/wtf8/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true license.workspace = true [dependencies] -ascii = { workspace = true } -bstr = { workspace = true } +ascii = { workspace = true, features = ["alloc"] } +bstr = { workspace = true, features = ["alloc"] } itertools = { workspace = true } memchr = { workspace = true } diff --git a/crates/wtf8/src/lib.rs b/crates/wtf8/src/lib.rs index 772a2879944..2167b6b04c6 100644 --- a/crates/wtf8/src/lib.rs +++ b/crates/wtf8/src/lib.rs @@ -31,7 +31,7 @@ //! to match CPython's behavior. //! //! [WTF-8]: https://simonsapin.github.io/wtf-8 -//! [`OsStr`]: std::ffi::OsStr +//! [`OsStr`]: https://doc.rust-lang.org/std/ffi/struct.OsStr.html #![no_std] #![allow(clippy::precedence, clippy::match_overlapping_arm)] @@ -1347,6 +1347,13 @@ pub fn slice_error_fail(s: &Wtf8, begin: usize, end: usize) -> ! { panic!("index {begin} and/or {end} in `{s:?}` do not lie on character boundary"); } +/// True for the ASCII bytes Python treats as whitespace in numeric parsing +/// (`\t \n \x0b \x0c \r` and space). +#[must_use] +pub const fn is_py_ascii_whitespace(b: u8) -> bool { + matches!(b, b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r' | b' ') +} + /// Iterator for the code points of a WTF-8 string. /// /// Created with the method `.code_points()`. diff --git a/examples/hello_embed.rs b/examples/hello_embed.rs index 9e1cdb829d6..ae56ed21bf1 100644 --- a/examples/hello_embed.rs +++ b/examples/hello_embed.rs @@ -6,7 +6,7 @@ fn main() -> vm::PyResult<()> { let source = r#"print("Hello World!")"#; let code_obj = vm .compile(source, vm::compiler::Mode::Exec, "") - .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)?; diff --git a/examples/mini_repl.rs b/examples/mini_repl.rs index 40d111732ae..edbd6e1495c 100644 --- a/examples/mini_repl.rs +++ b/examples/mini_repl.rs @@ -66,7 +66,7 @@ def fib(n): // (note that this is only the case when compiler::Mode::Single is passed to vm.compile) match vm .compile(&input, vm::compiler::Mode::Single, "") - .map_err(|err| vm.new_syntax_error(&err, Some(&input))) + .map_err(|err| err.into_pyexception(vm, Some(&input))) .and_then(|code_obj| vm.run_code_obj(code_obj, scope.clone())) { Ok(output) => { diff --git a/examples/parse_folder.rs b/examples/parse_folder.rs index 440bcdb9b5f..7ece0c74065 100644 --- a/examples/parse_folder.rs +++ b/examples/parse_folder.rs @@ -131,4 +131,4 @@ struct ParsedFile { result: ParseResult, } -type ParseResult = Result, String>; +type ParseResult = Result; diff --git a/extra_tests/custom_text_test_runner.py b/extra_tests/custom_text_test_runner.py index 3457bdfd0e4..865265750f4 100644 --- a/extra_tests/custom_text_test_runner.py +++ b/extra_tests/custom_text_test_runner.py @@ -389,11 +389,9 @@ def startTest(self, test): } self.start_time = time.time() if self.test_types: - if "test_type" in getattr( - test, test._testMethodName - ).__func__.__dict__ and set([s.lower() for s in self.test_types]) == set( - [s.lower() for s in _get_method_dict(test)["test_type"]] - ): + if "test_type" in _get_method_dict(test) and set( + [s.lower() for s in self.test_types] + ) == set([s.lower() for s in _get_method_dict(test)["test_type"]]): pass else: _get_method_dict(test)["__unittest_skip_why__"] = ( diff --git a/extra_tests/snippets/builtin_bytes.py b/extra_tests/snippets/builtin_bytes.py index 2cb4c317f49..3cbed79c069 100644 --- a/extra_tests/snippets/builtin_bytes.py +++ b/extra_tests/snippets/builtin_bytes.py @@ -708,3 +708,61 @@ def __new__(cls, value): assert b.foo == "bar" skip_if_unsupported(3, 11, test__bytes__) + +assert " \f\n\r\t\v".encode("utf-8").isspace() +assert " \f\n\r\t\v".encode("latin-1").isspace() + +# bytes.istitle tests +s = b"Aa6A" +assert s.istitle(), f"{s}" +s = b"Aa6aA" +assert not s.istitle(), f"{s}" +s = b"Python Is Fun" +assert s.istitle(), f"{s}" +s = b"Python is fun" +assert not s.istitle(), f"{s}" +s = b"PYTHON IS FUN" +assert not s.istitle(), f"{s}" +s = b"Python 3.9 Is Awesome!" +assert s.istitle(), f"{s}" +s = b"" +assert not s.istitle(), f"{s}" +s = b"Hello Is Amazing" +assert s.istitle(), f"{s}" +s = b"Not--a Titlecase String" +assert not s.istitle(), f"{s}" +s = b"123A" +assert s.istitle(), f"{s}" +s = b"123a" +assert not s.istitle(), f"{s}" +s = b"123A\ta" +assert not s.istitle(), f"{s}" +SUBSTR = b"123456" +s = b"".join([b"A", b"a" * 64, SUBSTR]) +assert s.istitle(), f"{s}" +s += b"A" +assert s.istitle(), f"{s}" +s += b"aA" +assert not s.istitle(), f"{s}" +assert "123A".istitle(), f"{s}" +assert not "123a".istitle(), f"{s}" +assert not "123A\ta".istitle(), f"{s}" + + +def test_huge_size(): + # sizes that cannot be allocated are MemoryError, not an aborted process + for factory in (bytes, bytearray): + assert_raises(MemoryError, lambda factory=factory: factory(2**62)) + for meth in ("center", "ljust", "rjust", "zfill"): + assert_raises( + MemoryError, + lambda factory=factory, meth=meth: getattr(factory(b"a"), meth)( + 1 << 62 + ), + ) + assert_raises( + OverflowError, lambda factory=factory: factory(b"\ta").expandtabs(2**31) + ) + + +test_huge_size() diff --git a/extra_tests/snippets/builtin_compile.py b/extra_tests/snippets/builtin_compile.py index 15095c0eede..73247e50df1 100644 --- a/extra_tests/snippets/builtin_compile.py +++ b/extra_tests/snippets/builtin_compile.py @@ -1,3 +1,8 @@ +import __future__ + +import ast +import sys + from testutils import assert_raises # compile() basic mode acceptance @@ -43,4 +48,105 @@ def _check_flags_error(flags): _check_flags_error(99999) +_check_flags_error(0x100) +_check_flags_error(0x800) _check_flags_error(0x10000) + + +ns = {} +exec( + "from __future__ import annotations\n" + "inherited = compile('x: __debug__\\n', '', 'exec')\n" + "not_inherited = compile('x: __debug__\\n', '', 'exec', dont_inherit=True)\n", + ns, +) +assert ns["inherited"].co_flags & 0x1000000 +assert not (ns["not_inherited"].co_flags & 0x1000000) + +barry_flag = __future__.barry_as_FLUFL.compiler_flag +barry_code = compile("x = 1", "", "exec", flags=barry_flag) +compile("from __future__ import barry_as_FLUFL\nx = 1\n", "", "exec") +if sys.implementation.name == "rustpython": + assert not (barry_code.co_flags & barry_flag) + +n = ast.parse('x = "# type: int"\n', type_comments=True) +assert n.body[0].type_comment is None +n = ast.parse("x = '# type: int'\n", type_comments=True) +assert n.body[0].type_comment is None +n = ast.parse('x = "abc" # type: str\n', type_comments=True) +assert n.body[0].type_comment == "str" +n = ast.parse("x = 1 # type: ignore[excuse]\n", type_comments=True) +assert [(ti.lineno, ti.tag) for ti in n.type_ignores] == [(1, "[excuse]")] + + +compile("() -> int", "", "func_type", flags=ast.PyCF_ONLY_AST) +func_type_tree = compile( + '("a,b", str) -> int', "", "func_type", flags=ast.PyCF_ONLY_AST +) +assert len(func_type_tree.argtypes) == 2 +assert func_type_tree.argtypes[0].value == "a,b" +func_type_tree = compile( + "(int, *str, **Any) -> float", + "", + "func_type", + flags=ast.PyCF_ONLY_AST, +) +assert [arg.id for arg in func_type_tree.argtypes] == ["int", "str", "Any"] +assert_raises( + SyntaxError, + compile, + "int -> str", + "", + "func_type", + flags=ast.PyCF_ONLY_AST, +) +assert_raises( + SyntaxError, + compile, + "(x=1) -> str", + "", + "func_type", + flags=ast.PyCF_ONLY_AST, +) +assert_raises( + SyntaxError, + compile, + "(int,) -> str", + "", + "func_type", + flags=ast.PyCF_ONLY_AST, +) +PY_CF_DONT_IMPLY_DEDENT = 0x0200 +PY_CF_ALLOW_INCOMPLETE_INPUT = 0x4000 +compile(b"# coding: latin-1\nx = '\xe9'\n", "", "exec") +compile("if 1:\n pass", "", "single") +assert_raises( + SyntaxError, + compile, + "if 1:\n pass", + "", + "single", + flags=PY_CF_DONT_IMPLY_DEDENT, +) +compile( + "if 1:\n pass\n", + "", + "single", + flags=PY_CF_DONT_IMPLY_DEDENT | PY_CF_ALLOW_INCOMPLETE_INPUT, +) +try: + compile( + "if 1:\n pass", + "", + "single", + flags=PY_CF_DONT_IMPLY_DEDENT | PY_CF_ALLOW_INCOMPLETE_INPUT, + ) +except _IncompleteInputError as exc: + assert exc.args[0] == "incomplete input", repr(exc) +else: + raise AssertionError("expected _IncompleteInputError") + +# The source is encoded before it is parsed, so a lone surrogate has to be +# reported rather than assumed away. +with assert_raises(UnicodeEncodeError): + compile(chr(0xD800), "", "eval") diff --git a/extra_tests/snippets/builtin_complex.py b/extra_tests/snippets/builtin_complex.py index 136f26ef001..ae84cac53e6 100644 --- a/extra_tests/snippets/builtin_complex.py +++ b/extra_tests/snippets/builtin_complex.py @@ -168,6 +168,15 @@ def __eq__(self, other): assert_raises(TypeError, lambda: complex("5+2j", 1)) assert_raises(ValueError, lambda: complex("abc")) +# whitespace is allowed around the string and the optional parentheses, +# but not inside the numeric token +assert complex(" 1+2j ") == 1 + 2j +assert complex("(1+2j)") == 1 + 2j +assert complex(" ( 1+2j ) ") == 1 + 2j +assert_raises(ValueError, lambda: complex("1 +2j")) +assert_raises(ValueError, lambda: complex("1+ 2j")) +assert_raises(ValueError, lambda: complex("1 + 2j")) + assert complex("1+10j") == 1 + 10j assert complex(10) == 10 + 0j assert complex(10.0) == 10 + 0j @@ -268,3 +277,10 @@ class complex_subclass(complex): assert repr(float("-inf") + 1j) == "(-inf+1j)" assert repr(complex(1, float("nan"))) == "(1+nanj)" assert repr(complex(1, float("inf"))) == "(1+infj)" + +# Round-half-to-even ties: Rust's shortest formatter can land on the +# odd-digit neighbour where repr()'s tie-breaking picks the even one. +assert repr(161852602146008.12 + 1j) == "(161852602146008.12+1j)" +assert repr(-788830060729777.2 + 2j) == "(-788830060729777.2+2j)" +assert repr(complex(0.0, 1959276370239205.2)) == "1959276370239205.2j" +assert repr(complex(-1818262230632059.2, 0.0)) == "(-1818262230632059.2+0j)" diff --git a/extra_tests/snippets/builtin_eval.py b/extra_tests/snippets/builtin_eval.py index 2f2405c8d9e..1648a1a271d 100644 --- a/extra_tests/snippets/builtin_eval.py +++ b/extra_tests/snippets/builtin_eval.py @@ -1,3 +1,5 @@ +from testutils import assert_raises + assert 3 == eval("1+2") code = compile("5+3", "x.py", "eval") @@ -75,3 +77,8 @@ def make_closure(): assert False, "eval with code containing free variables should fail" except NameError as e: pass + +# The source is encoded before it is parsed, so a lone surrogate has to be +# reported rather than assumed away. +with assert_raises(UnicodeEncodeError): + eval(chr(0xD800)) diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index 8879e130bc2..080294a3c8a 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -1,4 +1,5 @@ import builtins +import itertools import pickle import platform import sys @@ -393,3 +394,19 @@ class SubError(MyError): assert err.exceptions[0].args == ("x",) else: assert False, "except* handler did not run" + +# The exceptions argument is a sequence, so an arbitrary iterable must be +# rejected rather than drained. +try: + ExceptionGroup("m", itertools.count()) +except TypeError: + pass +else: + assert False, "ExceptionGroup accepted an unbounded iterable" + +# ImportError.__reduce__ has to cope with the exception carrying no args. +assert pickle.loads(pickle.dumps(ImportError())).args == () +restored = pickle.loads(pickle.dumps(ImportError("m", name="n", path="p"))) +assert restored.args == ("m",) +assert restored.name == "n" +assert restored.path == "p" diff --git a/extra_tests/snippets/builtin_exec.py b/extra_tests/snippets/builtin_exec.py index 2eae90e91c5..cfb88c15dc1 100644 --- a/extra_tests/snippets/builtin_exec.py +++ b/extra_tests/snippets/builtin_exec.py @@ -1,3 +1,5 @@ +from testutils import assert_raises + exec("def square(x):\n return x * x\n") assert 16 == square(4) # noqa: F821 @@ -71,3 +73,8 @@ def f(): f() + +# The source is encoded before it is parsed, so a lone surrogate has to be +# reported rather than assumed away. +with assert_raises(UnicodeEncodeError): + exec(chr(0xD800)) diff --git a/extra_tests/snippets/builtin_float.py b/extra_tests/snippets/builtin_float.py index f0fcae5d103..c459c2d0da6 100644 --- a/extra_tests/snippets/builtin_float.py +++ b/extra_tests/snippets/builtin_float.py @@ -549,3 +549,24 @@ def _check_msg(call, exc_type, expected_msg): lambda: INF.__int__(), OverflowError, "cannot convert float infinity to integer" ) _check_msg(lambda: NAN.__floor__(), ValueError, "cannot convert float NaN to integer") + +# repr round-half-to-even ties: Rust's shortest formatter can land on the +# odd-digit neighbour where repr()'s tie-breaking picks the even one. +assert repr(161852602146008.12) == "161852602146008.12" +assert repr(-788830060729777.2) == "-788830060729777.2" +assert repr(1959276370239205.2) == "1959276370239205.2" +assert repr(-1818262230632059.2) == "-1818262230632059.2" +assert str(161852602146008.12) == "161852602146008.12" +# non-tie values are unaffected +assert repr(1.5) == "1.5" +assert repr(0.1) == "0.1" +assert repr(100.0) == "100.0" + + +# float() takes at most one positional argument; the exact-float fast path +# must not let extra ones through. +assert_raises(TypeError, float, 1.5, True) +assert_raises(TypeError, float, 1.5, 2, 3) +assert_raises(TypeError, float, "1.5", 2) +assert float(1.5) == 1.5 +assert float() == 0.0 diff --git a/extra_tests/snippets/builtin_format.py b/extra_tests/snippets/builtin_format.py index 250d8ad6cac..c2e2a897470 100644 --- a/extra_tests/snippets/builtin_format.py +++ b/extra_tests/snippets/builtin_format.py @@ -24,6 +24,14 @@ def test_zero_padding(): test_zero_padding() +try: + format("result", "=8s") +except ValueError as error: + if str(error) != "'=' alignment not allowed in string format specifier": + raise AssertionError(f"unexpected error message: {error}") from error +else: + raise AssertionError("expected ValueError for '=8s' string format specifier") + assert "{:,}".format(100) == "100" assert "{:,}".format(1024) == "1,024" assert "{:_}".format(65536) == "65_536" diff --git a/extra_tests/snippets/builtin_hash.py b/extra_tests/snippets/builtin_hash.py index 9b2c8388790..818ee523f30 100644 --- a/extra_tests/snippets/builtin_hash.py +++ b/extra_tests/snippets/builtin_hash.py @@ -1,3 +1,5 @@ +import sys + from testutils import assert_raises @@ -28,3 +30,20 @@ def __hash__(self): with assert_raises(TypeError): hash([]) + +# Hashing a deeply nested tuple must not run off the native stack: the hash +# slot dispatch is what recurses, so that is where the depth is checked. + +if sys.implementation.name == "rustpython": + # Deep enough to reach the native stack guard; CPython, which also runs + # this snippet, dies on the same value. + deep_tuple = () + for _ in range(100_000): + deep_tuple = (deep_tuple,) + with assert_raises(RecursionError): + hash(deep_tuple) + # a dict key and a set member are hashed on insertion, same dispatch + with assert_raises(RecursionError): + {deep_tuple: 1} + with assert_raises(RecursionError): + {deep_tuple} diff --git a/extra_tests/snippets/builtin_iter.py b/extra_tests/snippets/builtin_iter.py new file mode 100644 index 00000000000..02d469a47ee --- /dev/null +++ b/extra_tests/snippets/builtin_iter.py @@ -0,0 +1,71 @@ +import queue +import threading + + +def make_iterator(): + holder = {} + + class Evil: + def __getitem__(self, index): + if index == 0: + return 0 + raise IndexError + + def __len__(self): + return holder["it"].__length_hint__() + + obj = Evil() + holder["it"] = iter(obj) + return holder["it"] + + +it = make_iterator() +q = queue.Queue() + + +def run(): + try: + it.__length_hint__() + except Exception as exc: # noqa: BLE001 + q.put(exc) + else: + q.put(None) + + +t = threading.Thread(target=run, daemon=True) +t.start() +t.join(1) + +assert not t.is_alive(), "iterator.__length_hint__ deadlocked" +err = q.get_nowait() +assert isinstance(err, RecursionError) + + +class NoLen: + def __getitem__(self, index): + if index < 3: + return index + raise IndexError + + +no_len_it = iter(NoLen()) +assert no_len_it.__length_hint__() is NotImplemented +next(no_len_it) +assert no_len_it.__length_hint__() is NotImplemented + + +class Seq: + def __init__(self): + self.items = [1, 2, 3] + + def __getitem__(self, index): + return self.items[index] + + def __len__(self): + return len(self.items) + + +seq_it = iter(Seq()) +assert seq_it.__length_hint__() == 3 +next(seq_it) +assert seq_it.__length_hint__() == 2 diff --git a/extra_tests/snippets/builtin_list.py b/extra_tests/snippets/builtin_list.py index d4afbffa1cb..44492092bad 100644 --- a/extra_tests/snippets/builtin_list.py +++ b/extra_tests/snippets/builtin_list.py @@ -1,3 +1,5 @@ +import sys + from testutils import assert_raises x = [1, 2, 3] @@ -242,6 +244,33 @@ def __eq__(self, x): assert sorted([(1, 2, 3), (0, 3, 6)], key=lambda x: x[1]) == [(1, 2, 3), (0, 3, 6)] assert sorted([(1, 2), (), (5,)], key=len) == [(), (5,), (1, 2)] +assert sorted(["b", "a", "é", "z\U0001f600", "z"]) == [ + "a", + "b", + "z", + "z\U0001f600", + "é", +] +assert sorted([10**30, -(10**30), 5, 0]) == [-(10**30), 0, 5, 10**30] +assert sorted([True, False, True]) == [False, True, True] + + +class IntSub(int): + pass + + +assert sorted([IntSub(2), 3, IntSub(1)]) == [1, 2, 3] +assert sorted([2.5, 1, 3.0, 2]) == [1, 2, 2.5, 3.0] +assert_raises(TypeError, sorted, [1, "a"]) +nan = float("nan") +assert repr(sorted([nan, 1.0, 2.0])) == "[nan, 1.0, 2.0]" +assert sorted([b"b", b"a", b"c"]) == [b"a", b"b", b"c"] +assert sorted([(2, 9), (1, 5), (2, 1)]) == [(1, 5), (2, 1), (2, 9)] +assert sorted([(1, "b"), (1, "a")]) == [(1, "a"), (1, "b")] +assert sorted([(1,), (1, 2), ()]) == [(), (1,), (1, 2)] +assert sorted([((2,), "x"), ((1,), "y")]) == [((1,), "y"), ((2,), "x")] +assert sorted([(1, "a"), (2.5, "b"), (0, "c")]) == [(0, "c"), (1, "a"), (2.5, "b")] + lst = [3, 1, 5, 2, 4] @@ -896,3 +925,8 @@ def __eq__(self, other): list1 = rewrite_list_eq([poc()]) list1.remove(list1) assert list1 == [] + +# The repeat count is multiplied by the element size; a count that overflows +# that product must raise instead of wrapping into a short allocation. +with assert_raises(MemoryError): + [1] * sys.maxsize diff --git a/extra_tests/snippets/builtin_memoryview.py b/extra_tests/snippets/builtin_memoryview.py index f206056ebfd..34928041cd2 100644 --- a/extra_tests/snippets/builtin_memoryview.py +++ b/extra_tests/snippets/builtin_memoryview.py @@ -90,3 +90,588 @@ def test_delitem(): test_delitem() + + +def test_empty_view_offset(): + # An empty view keeps the offset slicing left it, which can sit outside the + # exporter, and reaches no byte through it. + ba = bytearray(range(17)) + assert bytes(memoryview(ba)[::-9][-30::-9]) == b"" + assert bytes(memoryview(ba)[-30::-1]) == b"" + v = memoryview(ba)[::-9][-30::-9] + assert v.shape == (0,) + assert v.strides == (81,) + assert v.suboffsets == () + b24 = bytearray(range(24)) + assert bytes(memoryview(b24).cast("B", [4, 6])[-30::-1]) == b"" + + +test_empty_view_offset() + + +def test_exported_suboffsets(): + mv = memoryview(bytearray(b"abcdef"))[::-1] + exported = mv.__buffer__(284) + assert exported.suboffsets == () + assert bytes(exported) == b"fedcba" + assert ( + bytes(memoryview(memoryview(bytearray(b"abcdefg"))[::2].__buffer__(284))) + == b"aceg" + ) + + +test_exported_suboffsets() + + +def test_setitem_slice_strided_source(): + src = bytearray(b"abcdef") + dst = bytearray(b"......") + memoryview(dst)[:] = memoryview(src)[::-1] + assert bytes(dst) == b"fedcba" + dst = bytearray(b"...") + memoryview(dst)[:] = memoryview(src)[::2] + assert bytes(dst) == b"ace" + + +test_setitem_slice_strided_source() + + +def test_zero_dim_position(): + z = memoryview(bytearray(range(8)))[4:5].cast("B", []) + assert z[()] == 4 + assert z.tolist() == 4 + w = bytearray(range(8)) + memoryview(w)[4:5].cast("B", [])[()] = 99 + assert w[4] == 99 + assert w[0] == 0 + + +test_zero_dim_position() + + +def test_cast_zero_dim_size(): + assert_raises(TypeError, lambda: memoryview(bytearray(range(8))).cast("B", [])) + assert memoryview(bytearray(b"a")).cast("B", []).nbytes == 1 + + +test_cast_zero_dim_size() + + +def test_hash_format(): + assert_raises(ValueError, lambda: hash(memoryview(b"abcd").cast("I"))) + hash(memoryview(b"abcd").cast("b")) + hash(memoryview(b"abcdef")[::2]) + hash(memoryview(b"a").cast("B", [])) + + +test_hash_format() + + +def test_cast_keeps_exports(): + ba = bytearray(b"abc") + mv = memoryview(ba) + cast = mv.cast("B") + mv.release() + assert_raises(BufferError, lambda: ba.clear()) + cast.release() + ba.clear() + assert bytes(ba) == b"" + + +test_cast_keeps_exports() + + +def test_setitem_converts_before_writing(): + ba = bytearray(b"abc") + mv = memoryview(ba) + + class Idx: + def __index__(self): + return len(bytes(ba)) + + mv[0] = Idx() + assert bytes(ba) == b"\x03bc" + + +test_setitem_converts_before_writing() + + +def test_pep688_exporter_aliasing(): + def exporter(view_factory): + class C: + def __buffer__(self, flags): + return view_factory() + + def __release_buffer__(self, view): + pass + + return C() + + ba = bytearray(b"abc") + memoryview(ba)[:] = exporter(lambda: memoryview(ba)) + assert bytes(ba) == b"abc" + + ba = bytearray(b"abcdef") + memoryview(ba)[0:3] = exporter(lambda: memoryview(ba)[3:6]) + assert bytes(ba) == b"defdef" + + ba = bytearray(b"abcdef") + memoryview(ba)[3:6] = exporter(lambda: memoryview(ba)[0:3]) + assert bytes(ba) == b"abcabc" + + ba = bytearray(b"abcdef") + memoryview(ba)[:] = exporter(lambda: memoryview(ba)[::-1]) + assert bytes(ba) == b"fedcba" + + ba = bytearray(b"abcdef") + memoryview(ba)[::2] = exporter(lambda: memoryview(ba)[0:3]) + assert bytes(ba) == b"abbdcf" + + ba = bytearray(b"abcdef") + mv = memoryview(exporter(lambda: memoryview(ba))) + mv[:] = exporter(lambda: memoryview(ba)) + assert bytes(ba) == b"abcdef" + mv[:] = ba + assert bytes(ba) == b"abcdef" + + +test_pep688_exporter_aliasing() + + +def test_release_buffer_waits_for_last_view(): + class C(bytearray): + calls = 0 + + def __release_buffer__(self, view): + type(self).calls += 1 + super().__release_buffer__(view) + + c = C(b"abcdef") + a = memoryview(c) + b = memoryview(a) + a.release() + assert C.calls == 0 + assert b.tobytes() == b"abcdef" + b.release() + assert C.calls == 1 + + class D: + n = 0 + + def __init__(self): + self.b = bytearray(b"abcdef") + + def __buffer__(self, flags): + return memoryview(self.b) + + def __release_buffer__(self, view): + type(self).n += 1 + + d = D() + m = memoryview(d) + m2 = memoryview(m) + m3 = m.cast("B") + m.release() + m2.release() + assert D.n == 0 + m3.release() + assert D.n == 1 + + # Two acquisitions are two exports, each released on its own. + D.n = 0 + d = D() + a1 = memoryview(d) + a2 = memoryview(d) + a1.release() + assert D.n == 1 + a2.release() + assert D.n == 2 + + +test_release_buffer_waits_for_last_view() + + +def test_failed_request_does_not_release(): + import inspect + import mmap + + class M(mmap.mmap): + calls = 0 + + def __release_buffer__(self, view): + type(self).calls += 1 + super().__release_buffer__(view) + + m = M(-1, 10, access=mmap.ACCESS_READ) + assert_raises(BufferError, lambda: m.__buffer__(inspect.BufferFlags.WRITABLE)) + assert M.calls == 0 + + +test_failed_request_does_not_release() + + +def test_request_shapes_exported_descriptor(): + import array + + a = array.array("I", [1, 2, 3]) + assert a.__buffer__(0).format == "B" + assert a.__buffer__(28).format == "I" + + m = memoryview(a) + b = m.__buffer__(0) + assert (b.format, b.itemsize, b.ndim, b.shape, b.strides) == ("B", 4, 1, (3,), (4,)) + assert m.__buffer__(28).format == "I" + + b = a.__buffer__(0) + assert b[0] == 1 + assert b.tolist() == [1, 2, 3] + assert len(b.tobytes()) == 12 + b[0] = 9 + assert a[0] == 9 + + n = memoryview(bytearray(b"abcdef" * 4)).cast("I", (2, 3)) + assert n.__buffer__(0).ndim == 1 + assert n.__buffer__(0).shape == (6,) + assert n.__buffer__(8).ndim == 2 + assert n.__buffer__(8).format == "B" + + +test_request_shapes_exported_descriptor() + + +def test_release_during_index_conversion(): + # CHECK_RELEASED_AGAIN: the conversion that produces the value, and the one + # that produced the index, both run Python that can release the view. + ba = bytearray(b"abcdefgh") + mv = memoryview(ba) + + class Writer: + def __index__(self): + mv.release() + ba.clear() + return 7 + + try: + mv[7] = Writer() + raise AssertionError("write into a released view") + except ValueError as e: + assert "released memoryview" in str(e), e + + ba = bytearray(b"abcdefgh") + mv = memoryview(ba) + + class Reader: + def __index__(self): + mv.release() + ba.clear() + return 7 + + try: + mv[Reader()] + raise AssertionError("read from a released view") + except ValueError as e: + assert "released memoryview" in str(e), e + + # A release that does not resize still forbids the write. + ba = bytearray(b"abcd") + mv = memoryview(ba) + + class Quiet: + def __index__(self): + mv.release() + return 65 + + try: + mv[0] = Quiet() + raise AssertionError("write into a released view") + except ValueError as e: + assert "released memoryview" in str(e), e + assert bytes(ba) == b"abcd" + + +test_release_during_index_conversion() + + +def test_cast_rejects_non_native_format(): + # get_native_fmtchar + for fmt in ["", "ii", " 0; a 0 used to divide by zero while + # checking the product against SSIZE_MAX + for shape in ([0], [0, 4], [4, 0], [-1, 4], [0, 0]): + assert_raises( + ValueError, lambda shape=shape: memoryview(b"abcd").cast("B", shape) + ) + + class Index: + def __index__(self): + return 4 + + for shape in ([2.0, 2], [Index()], ["4"]): + assert_raises( + TypeError, lambda shape=shape: memoryview(b"abcd").cast("B", shape) + ) + + assert memoryview(b"abcd").cast("B", [True, 4]).tolist() == [[97, 98, 99, 100]] + + +test_cast_arguments() + + +def test_negative_stride(): + # A reversed view starts at its last byte, so walking it from there runs + # off the front of the exported slice. + assert memoryview(b"dcba") == memoryview(b"abcd")[::-1] + assert memoryview(b"abcd")[::-1] == memoryview(b"dcba") + assert not memoryview(b"abcd") == memoryview(b"abcd")[::-1] + + b = bytearray(b"____") + memoryview(b)[0:4] = memoryview(b"abcd")[::-1] + assert b == bytearray(b"dcba"), b + + a = array.array("i", [1, 2, 3]) + assert memoryview(array.array("i", [3, 2, 1])) == memoryview(a)[::-1] + assert memoryview(a)[::-1].tolist() == [3, 2, 1] + + +test_negative_stride() + + +def test_write_through_same_object(): + # Reading the source and writing the destination lock the same object + # when they overlap, and converting a value runs Python that can reach it. + b = bytearray(b"abcd") + memoryview(b)[0:4] = b + assert b == bytearray(b"abcd"), b + + b = bytearray(b"abcd") + memoryview(b)[0:4] = memoryview(b)[::-1] + assert b == bytearray(b"dcba"), b + + b = bytearray(b"abcd") + memoryview(b)[0:2] = memoryview(b)[2:4] + assert b == bytearray(b"cdcd"), b + + b = bytearray(b"abcd") + view = memoryview(b) + + class Index: + def __index__(self): + view[1] = 66 + return 65 + + view[0] = Index() + assert b == bytearray(b"ABcd"), b + + +test_write_through_same_object() + + +def test_cast_between_non_byte_formats(): + # A cast re-divides bytes into items; going from one item type straight to + # another would reinterpret what is already there. + view = memoryview(b"abcd").cast("i") + for fmt in ("h", "i", "f"): + try: + view.cast(fmt) + except TypeError as e: + assert "cannot cast between two non-byte formats" in str(e), e + else: + raise AssertionError(f"expected TypeError for cast to {fmt!r}") + + # Either side being bytes is allowed. + assert view.cast("B").tolist() == [97, 98, 99, 100] + assert view.cast("b").format == "b" + assert view.cast("c").tolist() == [b"a", b"b", b"c", b"d"] + assert memoryview(b"abcd").cast("c").cast("i").format == "i" + + +def test_cast_to_zero_dim(): + # A zero-dimensional view holds exactly one item, so the buffer has to be + # that one item and no more. + assert memoryview(b"abcd").cast("I", shape=()).tobytes() == b"abcd" + assert memoryview(b"a").cast("B", shape=()).tobytes() == b"a" + + for source, fmt in ((b"abcd", "B"), (b"abcdefgh", "I"), (b"ab", "b")): + try: + memoryview(source).cast(fmt, shape=()) + except TypeError as e: + assert "product(shape) * itemsize != buffer size" in str(e), e + else: + raise AssertionError(f"expected TypeError for {source!r} as {fmt!r}") + + +def test_hash_restricted_to_byte_formats(): + # The hash is over the bytes, so it agrees with the hash of those bytes + # only where an item is a byte. + data = b"abcdefgh" + assert hash(memoryview(data)) == hash(data) + assert hash(memoryview(data).cast("c")) == hash(data) + assert hash(memoryview(data).cast("b")) == hash(data) + + for fmt in ("I", "i", "h", "d"): + try: + hash(memoryview(data).cast(fmt)) + except ValueError as e: + assert "hashing is restricted to formats" in str(e), e + else: + raise AssertionError(f"expected ValueError for format {fmt!r}") + + +def test_tobytes_order(): + view = memoryview(b"abcdefgh") + for order in (None, "C", "F", "A"): + assert view.tobytes(order=order) == b"abcdefgh", order + + # A multidimensional view is laid out C-contiguously, so a Fortran-ordered + # copy walks it down the columns instead. + grid = memoryview(b"abcdefgh").cast("B", shape=(2, 4)) + assert grid.tolist() == [[97, 98, 99, 100], [101, 102, 103, 104]] + assert grid.tobytes() == b"abcdefgh" + assert grid.tobytes(order="C") == b"abcdefgh" + assert grid.tobytes(order="A") == b"abcdefgh" + assert grid.tobytes(order="F") == b"aebfcgdh" + + cube = memoryview(b"abcdefgh").cast("B", shape=(2, 2, 2)) + assert cube.tobytes(order="F") == b"aecgbfdh" + + for order in ("Z", "c", "f", ""): + try: + view.tobytes(order=order) + except ValueError as e: + assert str(e) == "order must be 'C', 'F' or 'A'", e + else: + raise AssertionError(f"expected ValueError for order {order!r}") + + +test_cast_between_non_byte_formats() +test_cast_to_zero_dim() +test_hash_restricted_to_byte_formats() +test_tobytes_order() diff --git a/extra_tests/snippets/builtin_property.py b/extra_tests/snippets/builtin_property.py index de64e526228..397d41fb075 100644 --- a/extra_tests/snippets/builtin_property.py +++ b/extra_tests/snippets/builtin_property.py @@ -85,3 +85,10 @@ def foo(self): p2 = property("a", doc="pdoc") # assert p2.__doc__ == 'pdoc' + + +# property() takes at most four arguments, and `name` is not one of them: +# the name slot is filled by __set_name__ and the __name__ setter instead. +assert_raises(TypeError, property, None, None, None, None, None) +assert_raises(TypeError, property, "a", "b", "c", "d", "e") +assert_raises(TypeError, property, name="x") diff --git a/extra_tests/snippets/builtin_round.py b/extra_tests/snippets/builtin_round.py index e94d9754204..83725420a40 100644 --- a/extra_tests/snippets/builtin_round.py +++ b/extra_tests/snippets/builtin_round.py @@ -93,3 +93,21 @@ def __round__(self, ndigits=None): assert round(1.0, 1000) == 1.0 assert round(1.0, -1000) == 0.0 assert round(1.7976931348623157e308, 0) == 1.7976931348623157e308 + + +# round() normalizes an int subclass to an exact int, like CPython's long_long(). +assert round(True) == 1 +assert type(round(True)) is int +assert type(round(True, 0)) is int +assert type(round(False)) is int + + +class MyInt(int): + pass + + +assert round(MyInt(5)) == 5 +assert type(round(MyInt(5))) is int +assert type(round(MyInt(5), 2)) is int +# A negative ndigits already produced a fresh exact int. +assert type(round(MyInt(15), -1)) is int diff --git a/extra_tests/snippets/builtin_str.py b/extra_tests/snippets/builtin_str.py index fde9deb8e0b..684bd66a1ff 100644 --- a/extra_tests/snippets/builtin_str.py +++ b/extra_tests/snippets/builtin_str.py @@ -170,6 +170,15 @@ assert "aaa".count("a", 2, 2) == 0 assert "aaa".count("a", 2, 1) == 0 +# An empty needle is counted in characters, not in encoded positions. +assert "".count("") == 1 +assert "abc".count("") == 4 +assert "가나다".count("") == 4 +assert "가나다".count("", 1) == 3 +assert "가나다".count("", 1, 2) == 2 +assert "가나다".count("", 4, 4) == 0 +assert "a\U0001f600b".count("") == 4 + assert "___a__".find("a") == 3 assert "___a__".find("a", -10) == 3 assert "___a__".find("a", -3) == 3 @@ -891,3 +900,18 @@ class MyString(str): assert id(b) != id(b * 1) assert id(b) != id(1 * b) assert id(b) != id(b * 2) + + +def test_huge_width(): + # A width that cannot be allocated is a MemoryError, not an aborted + # process, and a tabsize wider than a C int does not fit at all. + for meth in ("center", "ljust", "rjust", "zfill"): + assert_raises(MemoryError, lambda meth=meth: getattr("a", meth)(1 << 62)) + assert_raises(OverflowError, lambda: "\ta".expandtabs(1 << 62)) + assert_raises(OverflowError, lambda: "\ta".expandtabs(2**31)) + # The widest tabsize that still fits is accepted. With no tab to expand + # there is nothing to lay out, so the width is never allocated. + assert "a".expandtabs(2**31 - 1) == "a" + + +test_huge_width() diff --git a/extra_tests/snippets/builtin_str_unicode_slice.py b/extra_tests/snippets/builtin_str_unicode_slice.py index 252f84b1c72..1d35c6c483c 100644 --- a/extra_tests/snippets/builtin_str_unicode_slice.py +++ b/extra_tests/snippets/builtin_str_unicode_slice.py @@ -59,3 +59,32 @@ def expect_index_error(s, index): assert len(hebrew_text[30:10:-3]) == 7 assert hebrew_text[30:10:-1] == "א ,םיִהֹלֱא אָרָּב ," assert len(hebrew_text[30:10:-1]) == 20 + + +# A stepped slice whose span is an exact multiple of the step ends on the last +# character it collects rather than one past it, so the character count is the +# span divided by the step and not one more. The subject goes through a +# variable because a constant subscript is folded at compile time and would +# never reach the runtime slice at all. +def stepped(s, step): + return s[::step] + + +for subject, step, expected in [ + ("a\u00e9c", 3, "a"), + ("가나다라", 2, "가다"), + ("가나다라마바", 3, "가라"), + ("가나다라", -2, "라나"), + ("가나다라마바", -3, "바다"), + ("\U0001f600\U0001f601\U0001f602\U0001f603", 2, "\U0001f600\U0001f602"), +]: + sliced = stepped(subject, step) + assert sliced == expected, (subject, step, sliced) + assert len(sliced) == len(expected), (subject, step, len(sliced)) + # An overstated count makes the string claim characters its buffer does not + # hold, which reversed() then reads past. + assert list(reversed(sliced)) == list(expected)[::-1] + +assert len(stepped(hebrew_text, 2)) == 30 +assert len(stepped(hebrew_text, 4)) == 15 +assert len(stepped(hebrew_text, -2)) == 30 diff --git a/extra_tests/snippets/builtin_tuple.py b/extra_tests/snippets/builtin_tuple.py index fc2f8d5bb75..a679d2a99a8 100644 --- a/extra_tests/snippets/builtin_tuple.py +++ b/extra_tests/snippets/builtin_tuple.py @@ -1,3 +1,5 @@ +import sys + from testutils import assert_raises assert (1, 2) == (1, 2) @@ -93,3 +95,8 @@ def __eq__(self, x): assert (float("inf"), float("inf")) >= (float("inf"), float("inf")) assert not (float("inf"), float("inf")) < (float("inf"), float("inf")) assert not (float("inf"), float("inf")) > (float("inf"), float("inf")) + +# The repeat count is multiplied by the element size; a count that overflows +# that product must raise instead of wrapping into a short allocation. +with assert_raises(MemoryError): + (1,) * sys.maxsize diff --git a/extra_tests/snippets/builtin_type.py b/extra_tests/snippets/builtin_type.py index 8cb0a09a215..15a330aea19 100644 --- a/extra_tests/snippets/builtin_type.py +++ b/extra_tests/snippets/builtin_type.py @@ -687,3 +687,33 @@ def foo(): code = compile(stmts, "", "exec") assert code.co_names == ("blah", "foo") + + +# A slot descriptor carries the layout it was defined for. Reached from another +# class, it has to report that rather than read the slot at its own offset, +# whether the access is fresh or has been seen often enough to be specialized. + + +class WideSlots: + __slots__ = ("s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7") + + +class NarrowSlots: + __slots__ = ("only",) + + +class NoSlots: + __slots__ = () + + +NarrowSlots.borrowed = WideSlots.__dict__["s7"] +NoSlots.borrowed = WideSlots.__dict__["s7"] + +for owner in (NarrowSlots(), NoSlots()): + for _ in range(1000): + with assert_raises(TypeError): + owner.borrowed + with assert_raises(TypeError): + owner.borrowed = 1 + with assert_raises(TypeError): + del owner.borrowed diff --git a/extra_tests/snippets/builtin_type_bases.py b/extra_tests/snippets/builtin_type_bases.py new file mode 100644 index 00000000000..1d413e48e5c --- /dev/null +++ b/extra_tests/snippets/builtin_type_bases.py @@ -0,0 +1,293 @@ +from testutils import assert_raises + +# Reassigning __bases__ must rebuild slot dispatchers for the type and all its +# descendants: a slot whose method left the new MRO must be reset, not left stale. + + +# --- zelf itself loses __add__ (nb_add) --- +class OldAdd: + def __add__(self, other): + return "OLD" + + +class Bare: + pass + + +class C(OldAdd): + pass + + +c = C() +assert c + 1 == "OLD" +C.__bases__ = (Bare,) +with assert_raises(TypeError): + c + 1 + + +# --- 3-level descendant loses __iter__ (tp_iter) --- +class Itr: + def __iter__(self): + return iter([1, 2, 3]) + + +class New: + pass + + +class C2(Itr): + pass + + +class D2(C2): + pass + + +class E2(D2): + pass + + +e = E2() +assert list(e) == [1, 2, 3] +C2.__bases__ = (New,) +with assert_raises(TypeError): + list(e) + + +# --- descendant loses __len__ (sq_length), sibling slot untouched --- +class Sized: + def __len__(self): + return 7 + + +class C3(Sized): + pass + + +class D3(C3): + pass + + +d3 = D3() +assert len(d3) == 7 +C3.__bases__ = (Bare,) +with assert_raises(TypeError): + len(d3) + + +# --- descendant loses __getitem__ (mp_subscript) --- +class Subscriptable: + def __getitem__(self, key): + return key * 2 + + +class C4(Subscriptable): + pass + + +class D4(C4): + pass + + +d4 = D4() +assert d4[3] == 6 +C4.__bases__ = (Bare,) +with assert_raises(TypeError): + d4[3] + + +# --- descendant loses __call__ (tp_call) --- +class Callable: + def __call__(self): + return "called" + + +class C5(Callable): + pass + + +class D5(C5): + pass + + +d5 = D5() +assert d5() == "called" +C5.__bases__ = (Bare,) +with assert_raises(TypeError): + d5() + + +# --- guard: stale-wrong-target, name present in both bases must switch --- +class OldTarget: + def __add__(self, other): + return "OLD.__add__" + + +class NewTarget: + def __add__(self, other): + return "NEW.__add__" + + +class C6(OldTarget): + pass + + +class D6(C6): + pass + + +d6 = D6() +assert d6 + 1 == "OLD.__add__" +C6.__bases__ = (NewTarget,) +assert d6 + 1 == "NEW.__add__" + + +# --- guard: __getattr__ resolves at call time, stays correct --- +class OldGetattr: + def __getattr__(self, name): + return "OLD:" + name + + +class C7(OldGetattr): + pass + + +class D7(C7): + pass + + +d7 = D7() +assert d7.missing == "OLD:missing" +C7.__bases__ = (Bare,) +with assert_raises(AttributeError): + d7.missing + + +# --- mirror: new base ADDS a dunder the old chain lacked --- +class Adder: + def __add__(self, other): + return "ADDED" + + +class C8(Bare): + pass + + +class D8(C8): + pass + + +d8 = D8() +with assert_raises(TypeError): + d8 + 1 +C8.__bases__ = (Adder,) +assert d8 + 1 == "ADDED" + + +# --- round trip: swap away then back restores the slot --- +class C9(OldAdd): + pass + + +class D9(C9): + pass + + +d9 = D9() +assert d9 + 1 == "OLD" +C9.__bases__ = (Bare,) +with assert_raises(TypeError): + d9 + 1 +C9.__bases__ = (OldAdd,) +assert d9 + 1 == "OLD" + + +# --- left-only __add__ defined on the type itself survives a base swap --- +# __add__ and __radd__ share one accessor but occupy distinct fields; resolving +# the absent __radd__ must not overwrite the __add__ dispatcher. +class Mixin: + pass + + +class Other: + pass + + +class C10(Mixin): + def __add__(self, o): + return "C10" + + +c10 = C10() +assert c10 + 1 == "C10" +C10.__bases__ = (Other,) +assert c10 + 1 == "C10" + + +# --- right-only __radd__ survives a base swap --- +class C11(Mixin): + def __radd__(self, o): + return "C11" + + +c11 = C11() +assert 1 + c11 == "C11" +C11.__bases__ = (Other,) +assert 1 + c11 == "C11" + + +# --- subclass/grandchild shadowing __add__ keeps it when an ancestor swaps bases --- +class AddBase: + def __add__(self, o): + return "AddBase" + + +class Ancestor(AddBase): + pass + + +class Shadow(Ancestor): + def __add__(self, o): + return "Shadow" + + +class GrandShadow(Shadow): + pass + + +sh = Shadow() +gsh = GrandShadow() +assert sh + 1 == "Shadow" +assert gsh + 1 == "Shadow" +Ancestor.__bases__ = (Mixin,) +assert sh + 1 == "Shadow" +assert gsh + 1 == "Shadow" + + +# --- another Nb* pair: left-only __sub__ survives a base swap --- +class C12(Mixin): + def __sub__(self, o): + return "C12" + + +c12 = C12() +assert c12 - 1 == "C12" +C12.__bases__ = (Other,) +assert c12 - 1 == "C12" + + +# --- setattr/delattr-driven right-op updates keep the left op intact --- +class C13: + def __add__(self, o): + return "C13.add" + + +c13 = C13() +assert c13 + 1 == "C13.add" +C13.__radd__ = lambda self, o: "C13.radd" +assert c13 + 1 == "C13.add" +assert 1 + c13 == "C13.radd" +del C13.__radd__ +assert c13 + 1 == "C13.add" +with assert_raises(TypeError): + 1 + c13 diff --git a/extra_tests/snippets/forbidden_instantiation.py b/extra_tests/snippets/forbidden_instantiation.py index 50b6f58f07f..50a0e2cf635 100644 --- a/extra_tests/snippets/forbidden_instantiation.py +++ b/extra_tests/snippets/forbidden_instantiation.py @@ -1,3 +1,4 @@ +import re from types import ( AsyncGeneratorType, BuiltinFunctionType, @@ -62,3 +63,9 @@ def check_forbidden_instantiation(typ, reverse=False): for typ in internal_types: with assert_raises(TypeError): typ() + +# a match object carries state that only the matcher can fill in +with assert_raises(TypeError): + re.Match() +with assert_raises(TypeError): + re.Match.__new__(re.Match) diff --git a/extra_tests/snippets/operator_comparison.py b/extra_tests/snippets/operator_comparison.py index 71231f033dc..35a2083e94d 100644 --- a/extra_tests/snippets/operator_comparison.py +++ b/extra_tests/snippets/operator_comparison.py @@ -87,3 +87,50 @@ def test_type_error(x, y): assert not math.nan < 123 assert not math.nan >= 123 assert not math.nan <= 123 + + +# str and bytes comparisons, through a function so that the operands are not +# constants the compiler can fold, and in a loop so the specialized comparison +# is reached. +def cmp_all(a, b): + return (a == b, a != b, a < b, a <= b, a > b, a >= b) + + +def check(a, b, expected): + for _ in range(200): + assert cmp_all(a, b) == expected, (a, b, cmp_all(a, b), expected) + + +EQ = (True, False, False, True, False, True) +LT = (False, True, True, True, False, False) +GT = (False, True, False, False, True, True) + +same = "abc" * 3 +check(same, same, EQ) # the very same object +check(same, "abcabcabc", EQ) # equal, distinct objects +check("abc", "abd", LT) # same length, differing content +check("abc", "abcd", LT) # a prefix is less than what extends it +check("abcd", "abc", GT) +check("", "a", LT) +check("", "", EQ) +check("\ud800", "\ud800", EQ) # lone surrogates are compared as themselves +check("\ud800", "\udfff", LT) +check("a\U0001f600", "a\U0001f600", EQ) +check("가나다", "가나다", EQ) +check("가나", "가나다", LT) + +# Comparing with a non-string is never an error for == and !=. +assert not "abc" == 3 +assert "abc" != 3 + +bsame = b"abc" * 3 +check(bsame, bsame, EQ) +check(bsame, b"abcabcabc", EQ) +check(b"abc", b"abd", LT) +check(b"abc", b"abcd", LT) +check(b"abcd", b"abc", GT) +check(bytearray(b"abc"), bytearray(b"abcd"), LT) +check(bytearray(b"abc"), b"abc", EQ) # bytearray and bytes compare by content +check(b"abc", bytearray(b"abd"), LT) +assert not b"abc" == "abc" +assert b"abc" != "abc" diff --git a/extra_tests/snippets/recursion.py b/extra_tests/snippets/recursion.py index 2d3b2205d68..4b61a74b438 100644 --- a/extra_tests/snippets/recursion.py +++ b/extra_tests/snippets/recursion.py @@ -11,3 +11,36 @@ class Foo(object): # Since the default __str__ implementation calls __repr__ and __repr__ is # actually __str__, str(foo) should raise a RecursionError. assert_raises(RecursionError, str, foo) + + +# A __call__ that is the object being called dispatches through the call slot +# again, and none of that pushes a Python frame. + + +class Caller: + pass + + +caller = Caller() +Caller.__call__ = caller +assert_raises(RecursionError, caller) + + +# The same shape through the descriptor protocol: resolving the attribute +# fetches __get__, which is the descriptor itself. + + +class Descr: + pass + + +descr = Descr() +Descr.__get__ = descr +Descr.x = descr +try: + descr.x +except (RecursionError, TypeError): + # RecursionError here, TypeError from the call of a non-callable elsewhere + pass +else: + raise AssertionError("descr.x should not resolve") diff --git a/extra_tests/snippets/stdlib_array.py b/extra_tests/snippets/stdlib_array.py index ed2a8f22369..c2de6ac1ec8 100644 --- a/extra_tests/snippets/stdlib_array.py +++ b/extra_tests/snippets/stdlib_array.py @@ -143,3 +143,36 @@ def write(self, chunk): arr = array("b", range(128)) arr.tofile(_ReenteringWriter(arr)) assert len(arr) == 129 + + +def test_setitem_reentrant(): + # Converting the value runs Python, which can reach the array, so the + # array is not locked while it happens. + a = array("i", [1, 2, 3]) + + class Index: + def __index__(self): + a[1] = 9 + return 7 + + a[0] = Index() + assert a == array("i", [7, 9, 3]), a + + +test_setitem_reentrant() + + +def test_frombytes_of_itself(): + # Resizing is refused while a buffer is exported, before any lock is taken. + # The typecode is "b" so the view's items are bytes and the resize is what + # the call is refused for. + a = array("b", [1, 2, 3]) + m = memoryview(a) + with assert_raises(BufferError): + a.frombytes(m) + del m + + # A view of wider items is not a source of bytes at all. + wide = array("i", [1, 2, 3]) + with assert_raises(TypeError): + wide.frombytes(memoryview(wide)) diff --git a/extra_tests/snippets/stdlib_ast.py b/extra_tests/snippets/stdlib_ast.py index 7b5c69df49e..2dd2276723c 100644 --- a/extra_tests/snippets/stdlib_ast.py +++ b/extra_tests/snippets/stdlib_ast.py @@ -1,4 +1,5 @@ import ast +import copy print(ast) @@ -39,6 +40,25 @@ def foo(): assert i.names[0].asname is None +# Regression: parsed AST identifier fields are interned, matching CPython. +name_literal = "x" +name = ast.parse("x").body[0].value +assert name.id is name_literal + +name.extra = object() +replacement = copy.replace(name) +assert replacement.id is name.id +assert replacement.ctx is name.ctx +assert not hasattr(replacement, "extra") + +function_name = "f" +function = ast.parse("def f(): pass").body[0] +assert function.name is function_name + +async_function = ast.parse("async def f(): pass").body[0] +assert async_function.name is function_name + + # Regression test for issue #4862: # A cyclic AST fed to compile() used to overflow the Rust stack and SIGSEGV. # After the fix, the recursion guard in ast_from_object raises RecursionError, diff --git a/extra_tests/snippets/stdlib_asyncio.py b/extra_tests/snippets/stdlib_asyncio.py new file mode 100644 index 00000000000..a6a55509036 --- /dev/null +++ b/extra_tests/snippets/stdlib_asyncio.py @@ -0,0 +1,102 @@ +"""The private _asyncio accessors, reached directly instead of through a loop. + +CPython's _asyncio rejects every call below with "loop ... is not the running +loop" before it gets anywhere, and does not expose _current_tasks at all, so +these only run where they are reachable. +""" + +import sys + +from testutils import assert_raises + +if sys.implementation.name != "rustpython": + sys.exit(0) + +import _asyncio + + +def _task(): + pass + + +# The "already entered" message formats both tasks; a plain function used to be +# formatted as the wrong type there. +_asyncio._enter_task(0, _task) +with assert_raises(RuntimeError) as cm: + _asyncio._enter_task(0, _task) +assert "Cannot enter into task" in str(cm.exception), cm.exception +assert " str: @@ -364,8 +395,6 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead. # https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid - import ctypes # noqa: PLC0415 - csidl_const = { "CSIDL_APPDATA": 26, "CSIDL_COMMON_APPDATA": 35, @@ -398,4 +427,25 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: # print(get_win_folder_via_ctypes("CSIDL_DOWNLOADS")) +# A value wider than the C type is masked down to it instead of failing an +# unchecked conversion. +assert ctypes.c_char_p(2**64).value is None +assert ctypes.c_int(2**64 + 7).value == 7 +buf = (ctypes.c_int * 1)() +int_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_int)) +int_ptr[0] = 2**64 + 5 +assert int_ptr[0] == 5 + +# A slice assignment is length-checked against the slice, so the right-hand +# side must not be drained first. +array3 = (ctypes.c_int * 3)() +try: + array3[0:3] = itertools.count() +except ValueError: + pass +else: + assert False, "slice assignment accepted an unbounded iterable" +array3[0:3] = [7, 8, 9] +assert list(array3) == [7, 8, 9] + print("done") diff --git a/extra_tests/snippets/stdlib_ctypes_byvalue.py b/extra_tests/snippets/stdlib_ctypes_byvalue.py new file mode 100644 index 00000000000..73d4b334506 --- /dev/null +++ b/extra_tests/snippets/stdlib_ctypes_byvalue.py @@ -0,0 +1,106 @@ +# ctypes by-value aggregate arguments and returns over the live FFI path. +# +# Exercises passing structs/unions BY VALUE to foreign functions and returning +# structs BY VALUE through the unified host_env `call` entry point: +# - div(7, 3) / div(-7, 3): return div_t{quot, rem} by value (8-byte int +# struct, register-returned on SysV/AArch64), +# - imaxdiv(7, 3): return imaxdiv_t{quot, rem} by value (16-byte two-long +# struct, two-register return on SysV), +# - inet_ntoa(struct in_addr): take a 4-byte struct by value, with argtypes, +# without argtypes (direct-instance paramfunc path), and via a Union. +# +# Runs on little-endian linux/macOS; skipped on Windows (see below). Prints +# "OK"; a failed assertion aborts with a non-zero status. + +import ctypes +import sys +from ctypes import ( + CDLL, + Structure, + Union, + c_char, + c_char_p, + c_int, + c_int64, + c_uint32, + sizeof, +) + +if sys.platform == "win32": + # The C library is not reachable as CDLL(None) on Windows; by-value + # aggregate calls are covered there by test_ctypes. Keep output identical. + print("OK") + sys.exit(0) + + +libc = CDLL(None) + + +# 1. struct RETURN by value: div(7, 3) -> div_t{quot=2, rem=1} +class div_t(Structure): + _fields_ = [("quot", c_int), ("rem", c_int)] + + +assert sizeof(div_t) == 8, sizeof(div_t) +libc.div.argtypes = [c_int, c_int] +libc.div.restype = div_t + +r = libc.div(7, 3) +assert isinstance(r, div_t) +assert (r.quot, r.rem) == (2, 1), (r.quot, r.rem) + +# C division truncates toward zero. +r = libc.div(-7, 3) +assert (r.quot, r.rem) == (-2, -1), (r.quot, r.rem) + +# struct RETURN by value with NO argtypes on the arguments (ints via ConvParam) +libc.div.argtypes = None +r = libc.div(17, 5) +assert (r.quot, r.rem) == (3, 2), (r.quot, r.rem) + + +# 2. larger struct RETURN by value: imaxdiv(7, 3) -> imaxdiv_t{quot=2, rem=1} +class imaxdiv_t(Structure): + _fields_ = [("quot", c_int64), ("rem", c_int64)] + + +assert sizeof(imaxdiv_t) == 16, sizeof(imaxdiv_t) +libc.imaxdiv.argtypes = [c_int64, c_int64] +libc.imaxdiv.restype = imaxdiv_t + +r = libc.imaxdiv(7, 3) +assert (r.quot, r.rem) == (2, 1), (r.quot, r.rem) +r = libc.imaxdiv(-9, 4) +assert (r.quot, r.rem) == (-2, -1), (r.quot, r.rem) + + +# 3. struct ARGUMENT by value: inet_ntoa(struct in_addr) -> b"1.2.3.4" +class in_addr(Structure): + _fields_ = [("s_addr", c_uint32)] + + +assert sizeof(in_addr) == 4, sizeof(in_addr) +# `s_addr` holds the four address bytes in memory (network) order; a host-endian +# int whose bytes are [1, 2, 3, 4] yields the dotted string "1.2.3.4". +addr_value = int.from_bytes(bytes([1, 2, 3, 4]), sys.byteorder) + +libc.inet_ntoa.argtypes = [in_addr] +libc.inet_ntoa.restype = c_char_p +assert libc.inet_ntoa(in_addr(addr_value)) == b"1.2.3.4" + +# struct ARGUMENT by value with NO argtypes (direct-instance paramfunc path) +libc.inet_ntoa.argtypes = None +assert libc.inet_ntoa(in_addr(addr_value)) == b"1.2.3.4" + + +# 4. union ARGUMENT by value: a union laid out like in_addr, passed by value. +class in_addr_u(Union): + _fields_ = [("s_addr", c_uint32), ("bytes", c_char * 4)] + + +assert sizeof(in_addr_u) == 4, sizeof(in_addr_u) +libc.inet_ntoa.argtypes = [in_addr_u] +libc.inet_ntoa.restype = c_char_p +assert libc.inet_ntoa(in_addr_u(addr_value)) == b"1.2.3.4" + +print("OK") diff --git a/extra_tests/snippets/stdlib_ctypes_calls.py b/extra_tests/snippets/stdlib_ctypes_calls.py new file mode 100644 index 00000000000..cc4e8020511 --- /dev/null +++ b/extra_tests/snippets/stdlib_ctypes_calls.py @@ -0,0 +1,78 @@ +# Exercises the migrated _ctypes foreign-call path (routed through the unified +# host_env `call` entry point): scalar int/double arguments and returns, +# pointer (c_char_p / c_void_p) returns, a use_errno round-trip, and the +# argument conversion an untyped call performs. +# +# Prints "OK" and exits 0; any failed assertion aborts. Output is identical +# under CPython and RustPython on the same platform. +import ctypes +import errno +import sys +from ctypes import ( + CDLL, + c_char_p, + c_double, + c_int, + c_long, + c_size_t, + c_void_p, + get_errno, + set_errno, +) + +if sys.platform == "win32": + # The C library is not reachable as CDLL(None) on Windows; the migrated + # path is covered there by test_ctypes. Keep output identical regardless. + print("OK") + sys.exit(0) + +libc = CDLL(None, use_errno=True) + +# 1. scalar int argument + int return: abs(-5) == 5 +libc.abs.argtypes = [c_int] +libc.abs.restype = c_int +assert libc.abs(-5) == 5, libc.abs(-5) + +# 2. pointer argument (bytes -> char*) + size_t return: strlen(b"hello") == 5 +libc.strlen.argtypes = [c_char_p] +libc.strlen.restype = c_size_t +assert libc.strlen(b"hello") == 5, libc.strlen(b"hello") + +# 3. double argument + double return: sqrt(2.0) +libc.sqrt.argtypes = [c_double] +libc.sqrt.restype = c_double +root = libc.sqrt(2.0) +assert abs(root - 2.0**0.5) < 1e-12, root + +# 4. c_char_p return: strchr(b"abcdef", 'c') -> b"cdef" +libc.strchr.argtypes = [c_char_p, c_int] +libc.strchr.restype = c_char_p +assert libc.strchr(b"abcdef", ord("c")) == b"cdef", libc.strchr(b"abcdef", ord("c")) + +# 5. c_void_p return: the same call yields a non-null integer address +libc.strchr.restype = c_void_p +addr = libc.strchr(b"abcdef", ord("c")) +assert isinstance(addr, int) and addr != 0, addr + +# 6. use_errno round-trip: strtol overflow sets errno == ERANGE, captured into +# the ctypes-private errno by the call's errno swap. +libc.strtol.argtypes = [c_char_p, c_void_p, c_int] +libc.strtol.restype = c_long +set_errno(0) +libc.strtol(b"9" * 40, None, 10) +assert get_errno() == errno.ERANGE, (get_errno(), errno.ERANGE) + +# 7. A float has no implicit conversion to an integer argument: converting it +# would pass a truncated value where the callee expects an int or a pointer. +libc.abs.argtypes = None +for bad in (1.5, 0.0, 1e300): + try: + libc.abs(bad) + except (TypeError, ctypes.ArgumentError): + pass + else: + assert False, f"{bad!r} was accepted as an integer argument" +assert libc.abs(-3) == 3 +assert libc.abs(True) == 1 + +print("OK") diff --git a/extra_tests/snippets/stdlib_gc.py b/extra_tests/snippets/stdlib_gc.py new file mode 100644 index 00000000000..6c3169beedb --- /dev/null +++ b/extra_tests/snippets/stdlib_gc.py @@ -0,0 +1,68 @@ +"""The cycle collector has to walk the internal fields of containers and +iterators. + +Every type below is built into the cycle + + node -> node.__dict__ -> wrapper -> container -> node + +so the only path back to `node` runs through a field of the wrapper. A type +that reports nothing while being traversed, or reports the objects it iterates +instead of the iterator it holds, leaves its own reference unaccounted for: the +cycle is then classified as reachable and `node` is never freed. +""" + +import gc +import itertools +import weakref +from collections import defaultdict, deque + + +class Node: + pass + + +def collects(wrap): + """Report whether the collector breaks the cycle built around wrap().""" + + def build(): + container = [] + node = Node() + container.append(node) + node.held = wrap(container) + return weakref.ref(node) + + gc.collect() + ref = build() + gc.collect() + return ref() is None + + +# containers keeping their items in a field of their own +assert collects(deque) +assert collects(lambda c: defaultdict(int, {"k": c})) +assert collects(lambda c: classmethod(lambda cls: c)) + +# iterators: the wrapper holds an iterator, and that iterator holds the +# container +assert collects(iter) +assert collects(lambda c: map(str, c)) +assert collects(lambda c: filter(None, c)) +assert collects(lambda c: zip(c)) +assert collects(enumerate) +assert collects(reversed) +assert collects(itertools.chain) +assert collects(itertools.cycle) +assert collects(lambda c: itertools.islice(c, 5)) +assert collects(itertools.groupby) +assert collects(itertools.accumulate) +assert collects(lambda c: itertools.starmap(str, c)) +assert collects(lambda c: itertools.takewhile(bool, c)) +assert collects(lambda c: itertools.dropwhile(bool, c)) +assert collects(lambda c: itertools.filterfalse(None, c)) +assert collects(lambda c: itertools.compress(c, [1])) +assert collects(lambda c: itertools.product(c)) +assert collects(lambda c: itertools.combinations(c, 1)) +# tee holds its buffer through a second object, which has to be walked too +assert collects(lambda c: itertools.tee(c)[0]) + +print("ok") diff --git a/extra_tests/snippets/stdlib_hashlib.py b/extra_tests/snippets/stdlib_hashlib.py index c5feb709e17..f3400aed57d 100644 --- a/extra_tests/snippets/stdlib_hashlib.py +++ b/extra_tests/snippets/stdlib_hashlib.py @@ -1,5 +1,9 @@ +import _md5 +import _sha1 import hashlib +from testutils import assert_raises + # print(hashlib.md5) h = hashlib.md5() h.update(b"a") @@ -48,3 +52,15 @@ assert ( h.hexdigest() == "25738bfe4cc104131e1b45bece4dfd4e7e1d6f0dffda1211e996e9d5d3b66e81" ) + +# The single-algorithm modules set up their own types rather than relying on +# hashlib having done it. + +assert _md5.md5(b"").hexdigest() == "d41d8cd98f00b204e9800998ecf8427e" +assert _sha1.sha1(b"").hexdigest() == "da39a3ee5e6b4b0d3255bfef95601890afd80709" + +# a derived key wider than a C int does not fit, and never gets allocated. +# Which OverflowError comes out depends on the width of a C long: where it is +# narrower than the length asked for, converting the argument fails first. +with assert_raises(OverflowError): + hashlib.pbkdf2_hmac("sha256", b"password", b"salt", 1, 2**62) diff --git a/extra_tests/snippets/stdlib_imp.py b/extra_tests/snippets/stdlib_imp.py index 835b50d6171..9fd5f8a36fa 100644 --- a/extra_tests/snippets/stdlib_imp.py +++ b/extra_tests/snippets/stdlib_imp.py @@ -1,6 +1,8 @@ import _imp import time as import_time +from testutils import assert_raises + assert _imp.is_builtin("time") == True assert _imp.is_builtin("os") == False assert _imp.is_builtin("not existing module") == False @@ -29,3 +31,14 @@ def __init__(self, name): hello = _imp.init_frozen("__hello__") assert hello.initialized == True + +# withdata is keyword-only +with assert_raises(TypeError): + _imp.find_frozen("x", True) +assert _imp.find_frozen("_this_module_does_not_exist_") is None + +# and it hands back the marshalled code that get_frozen_object() takes +data, ispkg, origname = _imp.find_frozen("__hello__", withdata=True) +assert ispkg is False +assert origname == "__hello__" +assert _imp.get_frozen_object("__hello__", data).co_name == "" diff --git a/extra_tests/snippets/stdlib_io.py b/extra_tests/snippets/stdlib_io.py index 93c083a90c1..8346ddbb62d 100644 --- a/extra_tests/snippets/stdlib_io.py +++ b/extra_tests/snippets/stdlib_io.py @@ -84,3 +84,161 @@ def write(self, data): raw.textio = textio with assert_raises(AttributeError): textio.writelines(["x"]) + +textio = TextIOWrapper(BytesIO()) + +for invalid_chunk_size in (0, -1, 2**100): + try: + textio._CHUNK_SIZE = invalid_chunk_size + except ValueError: + pass + else: + raise AssertionError(f"expected ValueError for {invalid_chunk_size!r}") + +for invalid_chunk_size in (1.5, "4"): + try: + textio._CHUNK_SIZE = invalid_chunk_size + except TypeError: + pass + else: + raise AssertionError(f"expected TypeError for {invalid_chunk_size!r}") + + +class ChunkSize: + def __index__(self): + return 16 + + +textio._CHUNK_SIZE = ChunkSize() +assert textio._CHUNK_SIZE == 16 + + +class OversizedChunkSize: + def __index__(self): + return 2**100 + + +try: + textio._CHUNK_SIZE = OversizedChunkSize() +except ValueError as error: + expected = "cannot fit 'OversizedChunkSize' into an index-sized integer" + if str(error) != expected: + raise AssertionError(f"unexpected error message: {error}") from error +else: + raise AssertionError("expected ValueError for oversized indexable object") + + +def expect_value_error(expected, operation): + try: + operation() + except ValueError as error: + if str(error) != expected: + raise AssertionError(f"unexpected error message: {error}") from error + else: + raise AssertionError(f"expected ValueError: {expected}") + + +class UninitializedChunkSize: + def __init__(self): + self.called = False + + def __index__(self): + self.called = True + return 16 + + +uninitialized_textio = TextIOWrapper.__new__(TextIOWrapper) +uninitialized_chunk_size = UninitializedChunkSize() +expect_value_error( + "I/O operation on uninitialized object", + lambda: setattr(uninitialized_textio, "_CHUNK_SIZE", uninitialized_chunk_size), +) + +if uninitialized_chunk_size.called: + raise AssertionError( + "__index__ should not be called for uninitialized TextIOWrapper" + ) + + +detached_textio = TextIOWrapper(BytesIO()) +detached_textio.detach() +expect_value_error( + "underlying buffer has been detached", + lambda: setattr(detached_textio, "_CHUNK_SIZE", 16), +) +expect_value_error( + "underlying buffer has been detached", + lambda: delattr(detached_textio, "_CHUNK_SIZE"), +) + + +long_type_name = "X" * 250 +LongNamedChunkSize = type( + long_type_name, + (), + {"__index__": lambda self: 2**100}, +) +expect_value_error( + f"cannot fit '{long_type_name[:200]}' into an index-sized integer", + lambda: setattr(textio, "_CHUNK_SIZE", LongNamedChunkSize()), +) + + +non_ascii_type_name = "é" * 250 +NonAsciiNamedChunkSize = type( + non_ascii_type_name, + (), + {"__index__": lambda self: 2**100}, +) +truncated_non_ascii_type_name = non_ascii_type_name.encode("utf-8")[:200].decode( + "utf-8" +) +expect_value_error( + f"cannot fit '{truncated_non_ascii_type_name}' into an index-sized integer", + lambda: setattr(textio, "_CHUNK_SIZE", NonAsciiNamedChunkSize()), +) + + +# A buffer size or read size that cannot be allocated is a MemoryError, not an +# aborted process. +assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a"), buffer_size=2**62)) +assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a")).read(2**62)) +assert_raises(MemoryError, lambda: BufferedReader(BytesIO(b"a")).read1(2**62)) + + +def _text_cookie( + start_pos=0, + dec_flags=0, + bytes_to_feed=0, + chars_to_skip=0, + need_eof=0, + bytes_to_skip=0, +): + packed = ( + start_pos.to_bytes(8, "little", signed=True) + + dec_flags.to_bytes(4, "little", signed=True) + + bytes_to_feed.to_bytes(4, "little", signed=True) + + chars_to_skip.to_bytes(4, "little", signed=True) + + bytes([need_eof]) + + bytes_to_skip.to_bytes(4, "little", signed=True) + ) + return int.from_bytes(packed, "little") + + +# A cookie names a position both in characters and in bytes, and everything +# read back from it indexes what was decoded, so a position past the end is +# refused rather than stored. +for _bad in ( + _text_cookie(bytes_to_feed=10, chars_to_skip=1000, bytes_to_skip=0), + _text_cookie(bytes_to_feed=10, chars_to_skip=100000, bytes_to_skip=3), + _text_cookie(bytes_to_feed=10, chars_to_skip=1, bytes_to_skip=1000), +): + _textio = TextIOWrapper(BytesIO(b"hello world " * 20), encoding="utf-8") + _textio.read(1) + try: + _textio.seek(_bad) + except (OSError, OverflowError): + pass + else: + assert _textio.read(50) is not None + _textio.tell() diff --git a/extra_tests/snippets/stdlib_io_blocking_buffer.py b/extra_tests/snippets/stdlib_io_blocking_buffer.py new file mode 100644 index 00000000000..2119111dc2e --- /dev/null +++ b/extra_tests/snippets/stdlib_io_blocking_buffer.py @@ -0,0 +1,176 @@ +"""Transfers that wait for a peer must not hold the buffer they were given. + +A pipe or a socket answers when the other end does, which may be never. The +buffer is exported for the whole call, so it cannot be resized meanwhile, but +nothing else about it changes: another thread can still read it, write to it, +and the interpreter can still stop the world. An implementation that holds the +buffer's storage for the duration of the wait takes all of that away, and a +thread parked on that storage never reaches a safepoint, so a collection that +wants every thread stopped ends up waiting for the peer too. +""" + +import gc +import os +import socket +import threading +import time + +# The peer acts after DELAY; the checks below have to finish well inside it. +DELAY = 1.0 +SLACK = DELAY / 2 + + +def measure(buf, expected_len, writable): + """Time each operation on `buf` that does not need the peer, separately, so + a failure names the one that waited rather than the group.""" + elapsed = {} + + def timed(name, operation): + start = time.monotonic() + value = operation() + elapsed[name] = time.monotonic() - start + return value + + assert timed("len", lambda: len(buf)) == expected_len, len(buf) + assert isinstance(timed("bytes", lambda: bytes(buf)), bytes) + if writable: + timed("setitem", lambda: buf.__setitem__(0, buf[0])) + timed("gc.collect", gc.collect) + return elapsed + + +def run(buf, blocking_call, release_peer, writable): + started = threading.Event() + expected_len = len(buf) + result = [] + + def transfer(): + started.set() + result.append(blocking_call(buf)) + + def peer(): + time.sleep(DELAY) + release_peer() + + threads = [threading.Thread(target=transfer), threading.Thread(target=peer)] + for t in threads: + t.start() + started.wait() + time.sleep(0.2) # the transfer is now waiting on its peer + + elapsed = measure(buf, expected_len, writable) + waited = ["%s %.2fs" % item for item in elapsed.items() if item[1] >= SLACK] + assert not waited, "waited on the peer: " + ", ".join(waited) + + # The transfer is still in flight, so its export is still held and the + # buffer cannot be resized. An operating system that took the whole + # transfer without a peer leaves nothing here to observe. + assert not result, "the transfer finished without its peer" + try: + buf.append(0) + except BufferError: + pass + else: + raise AssertionError("append during an export should raise BufferError") + + for t in threads: + t.join() + return result[0] + + +# --- reading: the buffer is written into, so nothing else may touch it at all + + +read_fd, write_fd = os.pipe() +pipe = open(read_fd, "rb", buffering=0) +try: + target = bytearray(16) + n = run(target, pipe.readinto, lambda: os.write(write_fd, b"pipe"), writable=False) + assert n == 4, n + assert bytes(target[:4]) == b"pipe", bytes(target) +finally: + pipe.close() + os.close(write_fd) + +if hasattr(socket, "socketpair"): + left, right = socket.socketpair() + try: + target = bytearray(16) + n = run(target, left.recv_into, lambda: right.send(b"socket"), writable=False) + assert n == 6, n + assert bytes(target[:6]) == b"socket", bytes(target) + finally: + left.close() + right.close() + + +# --- writing: the buffer is only read, so it stays writable meanwhile + + +read_fd, write_fd = os.pipe() +sink = open(write_fd, "wb", buffering=0) +try: + # More than any pipe will hold, so the write cannot finish on its own. + source = bytearray(4 * 1024 * 1024) + drained = [] + + def drain(): + with open(read_fd, "rb", buffering=0) as f: + while True: + chunk = f.read(1 << 16) + if not chunk: + break + drained.append(len(chunk)) + + reader = threading.Thread(target=drain, daemon=True) + # One unbuffered write() reports what it transferred, which a signal can + # cut short, so the reader is measured against that rather than the source. + written = run(source, sink.write, reader.start, writable=True) + sink.close() + reader.join() + assert sum(drained) == written, (sum(drained), written) +finally: + if not sink.closed: + sink.close() + +if hasattr(socket, "socketpair"): + left, right = socket.socketpair() + try: + # How much a connection holds before it makes the sender wait is the + # operating system's to decide, and asking for a small send buffer does + # not settle it -- a socketpair is already connected, and on Windows it + # is a loopback pair whose receiver has a window of its own. So fill it + # until it refuses rather than guess a size that outruns it. + left.setblocking(False) + filled = 0 + while True: + try: + filled += left.send(bytes(1 << 16)) + except (BlockingIOError, InterruptedError): + break + left.setblocking(True) + + source = bytearray(1 << 16) + received = [] + + def receive(): + wanted = filled + len(source) + while sum(received) < wanted: + chunk = right.recv(1 << 16) + if not chunk: + break + received.append(len(chunk)) + + reader = threading.Thread(target=receive, daemon=True) + run(source, left.sendall, reader.start, writable=True) + reader.join() + assert sum(received) == filled + len(source), ( + sum(received), + filled, + len(source), + ) + finally: + left.close() + right.close() + +print("ok") diff --git a/extra_tests/snippets/stdlib_io_bytesio.py b/extra_tests/snippets/stdlib_io_bytesio.py index ba8ae20015e..9344c50d947 100644 --- a/extra_tests/snippets/stdlib_io_bytesio.py +++ b/extra_tests/snippets/stdlib_io_bytesio.py @@ -106,3 +106,11 @@ def test_07(): test_05() test_06() test_07() + + +# Reading into a buffer that views this same object locks it twice unless the +# read finishes first. +_bio = BytesIO(b"x" * 60) +assert _bio.readinto(_bio.getbuffer()) == 60 +_bio = BytesIO(b"x" * 60) +assert _bio.readinto(memoryview(_bio.getbuffer())) == 60 diff --git a/extra_tests/snippets/stdlib_io_stringio.py b/extra_tests/snippets/stdlib_io_stringio.py index 5419eef2bb2..0adf0edac0b 100644 --- a/extra_tests/snippets/stdlib_io_stringio.py +++ b/extra_tests/snippets/stdlib_io_stringio.py @@ -69,9 +69,25 @@ def test_05(): assert f.readline() == "" +def test_06(): + f = StringIO(newline=None) + f.write("\r") + f.__init__("x\n", newline=None) + assert f.newlines == "\n" + + f.close() + try: + f.newlines + except ValueError: + pass + else: + assert False + + if __name__ == "__main__": test_01() test_02() test_03() test_04() test_05() + test_06() diff --git a/extra_tests/snippets/stdlib_itertools.py b/extra_tests/snippets/stdlib_itertools.py index ce7a494713a..029d0d4229a 100644 --- a/extra_tests/snippets/stdlib_itertools.py +++ b/extra_tests/snippets/stdlib_itertools.py @@ -524,3 +524,19 @@ def __iter__(self): assert next(it) == (2, None) with assert_raises(StopIteration): next(it) + +# r is an arbitrary Python int: one too large for an index must raise +# OverflowError, and a representable one that cannot be allocated must raise +# MemoryError. +for factory in ( + itertools.combinations, + itertools.combinations_with_replacement, + itertools.permutations, +): + with assert_raises(OverflowError): + factory(range(5), 2**64) + +with assert_raises(MemoryError): + itertools.combinations(range(5), 2**44) +with assert_raises(MemoryError): + itertools.combinations_with_replacement(range(5), 2**44) diff --git a/extra_tests/snippets/stdlib_lzma.py b/extra_tests/snippets/stdlib_lzma.py new file mode 100644 index 00000000000..5ebce3c7fb1 --- /dev/null +++ b/extra_tests/snippets/stdlib_lzma.py @@ -0,0 +1,22 @@ +import itertools +import lzma + +from testutils import assert_raises + +# A raw-format compressor needs the filter chain's length before it can build +# it, so a filter argument that is not a sequence has to be rejected instead of +# being drained. +with assert_raises(TypeError): + lzma.LZMACompressor( + format=lzma.FORMAT_RAW, + filters=({"id": lzma.FILTER_LZMA2} for _ in itertools.count()), + ) + +compressor = lzma.LZMACompressor( + format=lzma.FORMAT_RAW, filters=[{"id": lzma.FILTER_LZMA2}] +) +compressed = compressor.compress(b"data") + compressor.flush() +decompressor = lzma.LZMADecompressor( + format=lzma.FORMAT_RAW, filters=[{"id": lzma.FILTER_LZMA2}] +) +assert decompressor.decompress(compressed) == b"data" diff --git a/extra_tests/snippets/stdlib_marshal.py b/extra_tests/snippets/stdlib_marshal.py index db843ff65d5..4e224fb313f 100644 --- a/extra_tests/snippets/stdlib_marshal.py +++ b/extra_tests/snippets/stdlib_marshal.py @@ -74,6 +74,85 @@ def test_roundtrip(self): assert eval(loaded) == eval(orig) + def test_roundtrip_non_constant_co_consts(self): + # `code.replace` accepts any marshalable object, including values the + # compiler constant representation cannot describe. + orig = compile("1 + 1", "", "eval").replace( + co_consts=([1, 2], {"a": 3}, {4, 5}, 6) + ) + + loaded = marshal.loads(marshal.dumps(orig)) + + self.assertEqual(loaded.co_consts, ([1, 2], {"a": 3}, {4, 5}, 6)) + + def test_roundtrip_shared_co_const(self): + # A constant shared with the enclosing object is written once and both + # readers resolve the same reference. + shared = ["shared"] + orig = compile("1 + 1", "", "eval").replace(co_consts=(shared,)) + + loaded_code, loaded_shared = marshal.loads(marshal.dumps((orig, shared))) + + self.assertIs(loaded_code.co_consts[0], loaded_shared) + + +class AllowCodeTests(unittest.TestCase): + """allow_code is answered where a code object is written or read, so a + graph that walks back on itself is not a second walk of its own.""" + + def test_recursive_value(self): + recursive = [] + recursive.append(recursive) + loaded = marshal.loads( + marshal.dumps(recursive, allow_code=False), allow_code=False + ) + self.assertIs(loaded[0], loaded) + + def test_too_deeply_nested(self): + nested = [] + for _ in range(100_000): + nested = [nested] + with self.assertRaises(ValueError): + marshal.dumps(nested, allow_code=False) + + def test_code_is_rejected(self): + code = compile("1", "", "exec") + for value in (code, [code], (code,), {0: code}): + with self.assertRaises(ValueError): + marshal.dumps(value, allow_code=False) + data = marshal.dumps(value) + with self.assertRaises(ValueError): + marshal.loads(data, allow_code=False) + + +class BadDataTests(unittest.TestCase): + def test_container_size_out_of_range(self): + import struct + + # a length is signed, so the top bit set is out of range rather than + # four billion items to reserve room for + for marker in b"([<>": + data = bytes([marker | 0x80]) + struct.pack("H", "'H' format requires 0 <= number <= 65535"), + (">i", "'i' format requires -2147483648 <= number <= 2147483647"), + ("N", "'N' format requires 0 <= number <= 18446744073709551615"), + ("P", "int too large to convert"), +): + try: + struct.pack(fmt, 10**30) + except struct.error as e: + assert str(e) == message, (fmt, str(e)) + else: + raise AssertionError(f"expected struct.error for {fmt!r}") + +try: + struct.pack("B", "x") +except struct.error as e: + assert str(e) == "required argument is not an integer", e +else: + raise AssertionError("expected struct.error") + + +# __init__ reads a new format into a Struct that already holds one. +s = struct.Struct(">h") +s.__init__(">hh") +assert s.format == ">hh" +assert s.size == 4 +assert s.pack(1, 2) == b"\x00\x01\x00\x02" +assert s.unpack(b"\x00\x01\x00\x02") == (1, 2) + +# A format that cannot be read leaves the Struct as it was. +for bad in ("\udc00", "$"): + with assert_raises((UnicodeEncodeError, struct.error)): + s.__init__(bad) + assert s.format == ">hh" + assert s.pack(1, 2) == b"\x00\x01\x00\x02" + + +# A subclass may do its own __init__ and pass the format up. +class BigShort(struct.Struct): + def __init__(self): + super().__init__(">h") + + +assert BigShort().pack(12345) == b"\x30\x39" + +# Until __init__ runs there is no format to answer with. +blank = struct.Struct.__new__(struct.Struct) +assert blank.size == -1 +for call in ( + lambda: blank.format, + lambda: blank.pack(1), + lambda: blank.unpack(b"aa"), + lambda: blank.unpack_from(b"aaaa"), + lambda: blank.pack_into(bytearray(4), 0, 1), + lambda: blank.iter_unpack(b"aa"), + lambda: repr(blank), +): + with assert_raises(RuntimeError): + call() diff --git a/extra_tests/snippets/stdlib_sys.py b/extra_tests/snippets/stdlib_sys.py index 155fc905a73..9dba301fb01 100644 --- a/extra_tests/snippets/stdlib_sys.py +++ b/extra_tests/snippets/stdlib_sys.py @@ -1,6 +1,7 @@ import os import subprocess import sys +import warnings from testutils import assert_raises @@ -158,3 +159,18 @@ def test_getframemodulename(): test_getframemodulename.__module__ = "awesome_module" assert test_getframemodulename() == "awesome_module" + +# An unimportable $PYTHONBREAKPOINT warns, and the hook has to survive that +# warning being turned into an exception. +saved_breakpoint_env = os.environ.get("PYTHONBREAKPOINT") +os.environ["PYTHONBREAKPOINT"] = "nonexistent_xyz.foo" +try: + with warnings.catch_warnings(): + warnings.simplefilter("error") + with assert_raises(RuntimeWarning): + sys.breakpointhook() +finally: + if saved_breakpoint_env is None: + del os.environ["PYTHONBREAKPOINT"] + else: + os.environ["PYTHONBREAKPOINT"] = saved_breakpoint_env diff --git a/extra_tests/snippets/stdlib_threading_contextvars.py b/extra_tests/snippets/stdlib_threading_contextvars.py new file mode 100644 index 00000000000..7947ef74fd4 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_contextvars.py @@ -0,0 +1,72 @@ +"""Stress contextvars from several threads at once. + +A Context holds the variable map, and both the map and the per-variable cache +are shared between every thread that touches the Context. Reading and writing +them has to be done under a lock rather than a cell borrow. + +Dropping a value that a set() or reset() displaced can run a __del__ that comes +straight back into the same Context, so the displaced value has to be released +after the lock is, not while it is held. +""" + +import contextvars +import threading + +ROUNDS = 2000 + +var = contextvars.ContextVar("v", default=0) +shared = contextvars.Context() +errors = [] + + +class Reentrant: + """__del__ runs while the variable that held this value is being replaced.""" + + def __del__(self): + try: + var.get() + except Exception: # a different context, or no value: not what is tested + pass + + +def churn(): + try: + for i in range(ROUNDS): + token = var.set(Reentrant()) + var.get() + var.reset(token) + var.set(i) + var.get() + contextvars.copy_context() + except Exception as exc: # noqa: BLE001 + errors.append(exc) + + +def run_in_shared(): + for i in range(ROUNDS): + try: + shared.run(var.set, i) + except RuntimeError: + # the Context is already entered by another thread + pass + + +threads = [threading.Thread(target=churn) for _ in range(4)] +threads += [threading.Thread(target=run_in_shared) for _ in range(4)] +for t in threads: + t.start() +for t in threads: + t.join() + +assert not errors, errors + +# the map itself still behaves +ctx = contextvars.copy_context() +ctx.run(var.set, 42) +assert ctx[var] == 42 +assert var in ctx +assert list(ctx) == [var] +assert ctx.get(var) == 42 +assert len(ctx) == 1 + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_current_frames.py b/extra_tests/snippets/stdlib_threading_current_frames.py new file mode 100644 index 00000000000..e93a222148e --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_current_frames.py @@ -0,0 +1,100 @@ +"""Take sys._current_frames() while other threads are running Python. + +The frame each thread is executing is published for cross-thread readers, and +_current_frames() takes a reference to it with the world stopped. A reader that +disagrees with the publisher about what the published pointer addresses reads +and reference-counts the wrong memory, which corrupts a neighbouring object +rather than failing at the read: the damage surfaces later, in the thread that +owns it, as a crash or a wedge. + +Workers therefore run ordinary Python calls (which publish a frame) in a tight +loop while the main thread hammers _current_frames(). +""" + +import sys +import threading +import time + +DURATION = 1.5 + + +def leaf(): + return sum(range(8)) + + +def nest(n): + if n: + return nest(n - 1) + return leaf() + + +def worker(stop): + while not stop.is_set(): + nest(16) + + +def frames_are_sane(frames): + # Every key is a thread id, every value a frame of this process. + for tid, frame in frames.items(): + assert isinstance(tid, int), tid + assert tid > 0, tid + assert type(frame).__name__ == "frame", frame + assert isinstance(frame.f_lineno, int), frame + assert isinstance(frame.f_code.co_name, str), frame + + +# The main thread sees itself where it stands. +me = sys._current_frames()[threading.get_ident()] +assert me is sys._getframe(), me + +stop = threading.Event() +threads = [threading.Thread(target=worker, args=(stop,)) for _ in range(4)] +for t in threads: + t.start() + +deadline = time.time() + DURATION +calls = 0 +while time.time() < deadline: + frames_are_sane(sys._current_frames()) + calls += 1 +stop.set() +for t in threads: + t.join() + +assert calls > 0, calls + + +# A thread parked in a call the main thread can name is reported inside it, +# with its callers reachable through f_back. +entered = threading.Event() +leave = threading.Event() +seen = [] + + +def g456(): + seen.append(threading.get_ident()) + entered.set() + leave.wait() + + +def f123(): + g456() + + +t = threading.Thread(target=f123) +t.start() +entered.wait() +try: + chain = [] + frame = sys._current_frames()[seen[0]] + while frame is not None: + chain.append(frame.f_code.co_name) + frame = frame.f_back + assert "g456" in chain, chain + assert "f123" in chain, chain + assert chain.index("g456") < chain.index("f123"), chain +finally: + leave.set() + t.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_gc_fork.py b/extra_tests/snippets/stdlib_threading_gc_fork.py new file mode 100644 index 00000000000..cd00cf00983 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_gc_fork.py @@ -0,0 +1,62 @@ +"""Fork while other threads drive concurrent GC stop-the-world. + +fork() and the cycle collector both stop the world through the same shared +state. Without a single exclusion around each stop->start span, an interleaving +of the fork requester and a GC requester clobbers that state (requester word, +suspension countdown) so the completion check never converges and a requester +waits on itself forever. + +Worker threads allocate cyclic garbage with GC enabled while the main thread +forks repeatedly; each child collects and exits. A regression shows up as a +hang in the parent (never finishing the fork loop). The allocation rate is kept +light so the collection stays cheap even in unoptimized builds. +""" + +import gc +import os +import threading +import time + +if not hasattr(os, "fork"): + print("skipped (no fork)") + raise SystemExit(0) + +gc.enable() +stop = threading.Event() + + +def churn(): + while not stop.is_set(): + a = {} + b = {"a": a} + a["b"] = b # cycle collectable only by the cycle collector + lst = [a, b] + lst.append(lst) + del a, b, lst + # Throttle so the collector keeps the heap small; the point is to + # interleave fork with concurrent collections, not to grow the heap. + time.sleep(0.001) + + +workers = [threading.Thread(target=churn) for _ in range(4)] +for w in workers: + w.start() + +# Let the workers get going before forking. +time.sleep(0.05) + +N = 25 +for _ in range(N): + pid = os.fork() + if pid == 0: + # Child: run its own stop-the-world collection, then exit. + gc.collect() + os._exit(0) + _, status = os.waitpid(pid, 0) + assert status == 0, status + +stop.set() +for w in workers: + w.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_gc_frame_race.py b/extra_tests/snippets/stdlib_threading_gc_frame_race.py new file mode 100644 index 00000000000..37cdbbf122c --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_gc_frame_race.py @@ -0,0 +1,101 @@ +"""Stress GC traversal against concurrently executing frames. + +The cycle collector reads each tracked object's interpreter state, including +the data stack and fast locals of frames that other threads are actively +executing. Those slots are written without synchronization by the running +thread, so the collector must only read them while the world is stopped. + +Workers churn frame state hard: deep recursion (many nested frames), heavy +local rebind / stack traffic, and generators repeatedly resumed. Meanwhile a +collector thread loops gc.collect() and an introspector walks live frame +objects via gc.get_objects(). A regression (torn read of a running frame) +shows up as a crash, a use-after-free, or a hang. +""" + +import gc +import sys +import threading +import time + +DURATION = 1.5 + + +def deep(n): + # Deep recursion + local rebind churns fast locals and the data stack. + a = n + b = [n, n + 1] + c = {"k": a} + if n <= 0: + return a + len(b) + len(c) + a = a - 1 + b.append(a) + return deep(n - 1) + a + + +def gen_worker(): + def counter(limit): + acc = 0 + i = 0 + while i < limit: + box = {"i": i} + box["self"] = box # a cycle held by the running generator frame + acc += i + yield acc + i += 1 + + g = counter(200) + total = 0 + for v in g: + total += v + return total + + +def make_frame_cycles(n): + for _ in range(n): + + def inner(): + fr = sys._getframe() + box = {"fr": fr} + box["self"] = box + return None + + inner() + + +def worker(stop): + # deep() nesting is kept modest so the recursion also fits the smaller + # worker-thread stack of unoptimized (debug) builds; the generators and + # frame cycles supply the rest of the frame churn. + while not stop.is_set(): + deep(12) + gen_worker() + make_frame_cycles(20) + + +def collector(stop): + while not stop.is_set(): + gc.collect() + + +def introspector(stop): + while not stop.is_set(): + for o in gc.get_objects(): + if type(o).__name__ == "frame": + try: + _ = o.f_lineno + _ = o.f_code.co_name + except Exception: + pass + + +stop = threading.Event() +threads = [threading.Thread(target=worker, args=(stop,)) for _ in range(4)] +threads.append(threading.Thread(target=collector, args=(stop,))) +threads.append(threading.Thread(target=introspector, args=(stop,))) +for t in threads: + t.start() +time.sleep(DURATION) +stop.set() +for t in threads: + t.join() +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_gc_import.py b/extra_tests/snippets/stdlib_threading_gc_import.py new file mode 100644 index 00000000000..340184093f3 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_gc_import.py @@ -0,0 +1,58 @@ +"""Concurrent imports plus GC stop-the-world must not deadlock (no fork). + +The global import lock is held across bytecode by the importlib bootstrap, so +its holder can be parked at a safepoint mid-hold. If another thread blocks on +that lock while attached, a GC stop-the-world requester waits forever for that +attached thread to suspend while the lock holder stays parked -- a three-party +deadlock. Acquiring the import lock must therefore detach so the wait honors a +stop-the-world request. + +Two threads repeatedly re-import modules (contending the import lock) while a +third storms the cycle collector and a fourth allocates cyclic garbage. A +regression shows up as a hang (the importer threads never finishing). +""" + +import gc +import importlib +import sys +import threading + +gc.enable() +stop = threading.Event() + +# Modules cheap to import and safe to drop/re-import repeatedly. +MODS = ("colorsys", "stringprep") +ITERS = 2000 + + +def importer(mod): + for _ in range(ITERS): + if stop.is_set(): + break + sys.modules.pop(mod, None) + importlib.import_module(mod) + + +def collector(): + while not stop.is_set(): + gc.collect() + + +def allocator(): + while not stop.is_set(): + y = [{"i": i} for i in range(50)] + y[0]["self"] = y # cycle collectable only by the cycle collector + + +importers = [threading.Thread(target=importer, args=(m,)) for m in MODS] +helpers = [threading.Thread(target=collector), threading.Thread(target=allocator)] + +for t in importers + helpers: + t.start() +for t in importers: + t.join() +stop.set() +for t in helpers: + t.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_generator.py b/extra_tests/snippets/stdlib_threading_generator.py new file mode 100644 index 00000000000..e606ef11cec --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_generator.py @@ -0,0 +1,95 @@ +"""Resume one generator from several threads at once. + +A generator is resumed by one thread at a time, and whether the sent value is +pushed onto the frame's value stack depends on whether the generator has +already started. Deciding that before the generator is claimed reads a frame +another thread can advance in the meantime, and resuming it then leaves the +stack short of what the code after the yield expects. + +Every yielded value still has to reach exactly one caller: threads that lose +the race get a ValueError instead of a value. +""" + +import threading + +WORKERS = 4 +ROUNDS = 400 + + +def counter(): + yield 1 + yield 2 + yield 3 + + +gens = [counter() for _ in range(ROUNDS)] +received = [[] for _ in range(ROUNDS)] +start = threading.Barrier(WORKERS) +errors = [] + + +def worker(): + try: + for index, gen in enumerate(gens): + start.wait() + for _ in range(3): + try: + received[index].append(next(gen)) + except StopIteration: + break + except ValueError: + # another thread is running this generator + pass + except Exception as exc: # noqa: BLE001 + errors.append(exc) + # the other workers are waiting at the barrier for this one + start.abort() + + +threads = [threading.Thread(target=worker) for _ in range(WORKERS)] +for t in threads: + t.start() +for t in threads: + t.join() + +assert not errors, errors +for got in received: + # no value handed out twice, and none skipped + assert sorted(got) == list(range(1, len(got) + 1)), got + + +# a generator that is closed while it is being resumed stays consistent +def loop(): + while True: + yield 1 + + +shared = loop() +closed = threading.Barrier(2) + + +def resumer(): + closed.wait() + for _ in range(ROUNDS): + try: + next(shared) + except (StopIteration, ValueError): + pass + + +def closer(): + closed.wait() + try: + shared.close() + except ValueError: + # the generator was running + pass + + +pair = [threading.Thread(target=resumer), threading.Thread(target=closer)] +for t in pair: + t.start() +for t in pair: + t.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_itertools_cycle.py b/extra_tests/snippets/stdlib_threading_itertools_cycle.py new file mode 100644 index 00000000000..b50a31b2443 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_itertools_cycle.py @@ -0,0 +1,26 @@ +"""Stress itertools.cycle from several threads at once. + +cycle() advances its index and wraps it back to zero when it reaches the end of +the saved items. Doing that in two separate steps lets another thread observe +the index past the end and read out of bounds, so the update has to be a single +atomic step. +""" + +import itertools +import threading + +shared_cycle = itertools.cycle([1, 2, 3]) + + +def spin(): + for _ in range(20000): + next(shared_cycle) + + +threads = [threading.Thread(target=spin) for _ in range(4)] +for t in threads: + t.start() +for t in threads: + t.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_itertools_tee.py b/extra_tests/snippets/stdlib_threading_itertools_tee.py new file mode 100644 index 00000000000..e13e20731f8 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_itertools_tee.py @@ -0,0 +1,57 @@ +"""Advance the iterators of one tee() from several threads at once. + +Every tee iterator reads its position, asks the shared buffer for that item and +then moves the position on. Reading and moving it on has to be one step, and +the buffer has to stay claimed until the value it fetched from the source is +cached: otherwise two callers work on the same index, a fetched value is +dropped, and the buffer is left to be filled out of order. + +A caller that loses the race gets a RuntimeError, never a value another caller +has already been handed. +""" + +import itertools +import threading + +ROUNDS = 200 +WORKERS = 4 + +errors = [] + + +def drain(iterator, out): + for _ in range(ROUNDS): + try: + out.append(next(iterator)) + except StopIteration: + break + except RuntimeError: + # another thread is advancing this tee + pass + except Exception as exc: # noqa: BLE001 + errors.append(exc) + break + + +for _ in range(10): + first, second = itertools.tee(iter(range(ROUNDS * WORKERS))) + taken = [[] for _ in range(WORKERS)] + threads = [ + threading.Thread(target=drain, args=(first if i % 2 else second, taken[i])) + for i in range(WORKERS) + ] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors, errors + for got in taken: + # one iterator hands out ascending values, each of them once + assert got == sorted(set(got)), got + for side in (taken[1], taken[3]), (taken[0], taken[2]): + # the two threads sharing an iterator split its values between them + shared = side[0] + side[1] + assert len(shared) == len(set(shared)), shared + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_set_repr.py b/extra_tests/snippets/stdlib_threading_set_repr.py new file mode 100644 index 00000000000..e2ce2d94357 --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_set_repr.py @@ -0,0 +1,44 @@ +"""Stress set repr against concurrent mutation. + +repr() checks that the set is non-empty and then reads its first element. The +two steps are separate, so another thread can empty the set in between; the +read has to cope with that rather than trusting the earlier check. + +Threads that observe a mutation mid-iteration raise RuntimeError, which is a +legitimate outcome here; a regression shows up as a crash instead. +""" + +import threading + +shared_set = {1, 2, 3, 4, 5} +stop = False + + +def mutate(): + while not stop: + try: + shared_set.clear() + shared_set.update({1, 2, 3}) + except RuntimeError: # changed size during iteration + pass + + +def read(): + for _ in range(20000): + try: + repr(shared_set) + except RuntimeError: # changed size during iteration + pass + + +mutators = [threading.Thread(target=mutate) for _ in range(2)] +readers = [threading.Thread(target=read) for _ in range(2)] +for t in mutators + readers: + t.start() +for t in readers: + t.join() +stop = True +for t in mutators: + t.join() + +print("ok") diff --git a/extra_tests/snippets/stdlib_threading_type_cache.py b/extra_tests/snippets/stdlib_threading_type_cache.py new file mode 100644 index 00000000000..92368e8b83d --- /dev/null +++ b/extra_tests/snippets/stdlib_threading_type_cache.py @@ -0,0 +1,69 @@ +"""Stress the lock-free type method cache against concurrent type mutation. + +Readers hammer method lookups while a mutator continuously replaces and +deletes the method, dropping the old function objects. Guards against +use-after-free in the cache read protocol (QSBR deferred reclamation). + +Also churns a freelist-eligible published value (a tuple class attribute): +tuples normally go back through the freelist on dealloc, but once one is +published to the type cache it must instead go through the QSBR-deferred +reclamation path, so this exercises that bypass. +""" + +import threading +import time + + +class C: + def m(self): + return -1 + + +DURATION = 1.5 + + +def reader(stop): + obj = C() + while not stop.is_set(): + for _ in range(1000): + try: + obj.m() + except AttributeError: + pass + try: + obj.shape + except AttributeError: + pass + + +def mutator(stop): + i = 0 + while not stop.is_set(): + + def m(self, _i=i): + return _i + + C.m = m + C.shape = (i, i + 1) + i += 1 + if i % 97 == 0: + try: + del C.m + except AttributeError: + pass + try: + del C.shape + except AttributeError: + pass + + +stop = threading.Event() +threads = [threading.Thread(target=reader, args=(stop,)) for _ in range(4)] +threads.append(threading.Thread(target=mutator, args=(stop,))) +for t in threads: + t.start() +time.sleep(DURATION) +stop.set() +for t in threads: + t.join() +print("ok") diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index 1629443a9e7..b74d5bbc638 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -1,3 +1,4 @@ +import sys import time x = time.gmtime(1000) @@ -11,41 +12,86 @@ # print(s) assert s == "1970-01-01-00-16-40" -x2 = time.strptime(s, "%Y-%m-%d-%H-%M-%S") -assert x2.tm_min == 16 +if sys.platform != "wasi": + # _strptime depends on time.tzname, which is not available on WASI yet. + x2 = time.strptime(s, "%Y-%m-%d-%H-%M-%S") + assert x2.tm_min == 16 + + # TODO: WASI currently does not raise OverflowError for some out-of-range + # struct_time values in asctime() and strftime(). + # Re-enable this regression on WASI once the non-Unix time conversion path is fixed. + + # Regression test for RustPython issue #4938: + # struct_time field overflow should raise OverflowError (matching CPython), + # not TypeError. Covers mktime, asctime, and strftime. + I32_MAX_PLUS_1 = 2147483648 + overflow_cases = [ + (I32_MAX_PLUS_1, 1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in year + (2024, I32_MAX_PLUS_1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in month + (2024, 1, I32_MAX_PLUS_1, 0, 0, 0, 0, 0, 0), # i32 overflow in mday + (2024, 1, 1, 0, 0, I32_MAX_PLUS_1, 0, 0, 0), # i32 overflow in sec + (88888888888,) * 9, # multi-field i32 overflow + ] + + for case in overflow_cases: + for func_name, call in [ + ("mktime", lambda c=case: time.mktime(c)), + ("asctime", lambda c=case: time.asctime(c)), + ("strftime", lambda c=case: time.strftime("%Y", c)), + ]: + try: + call() + except OverflowError: + pass # expected, matches CPython + except TypeError as e: + raise AssertionError( + f"{func_name}({case}) raised TypeError (should be OverflowError): {e}" + ) from e + else: + raise AssertionError( + f"{func_name}({case}) did not raise — expected OverflowError" + ) s = time.asctime(x) -# print(s) assert s == "Thu Jan 1 00:16:40 1970" +# Monotonic and performance clocks should advance with elapsed time. +monotonic_before = time.monotonic() +monotonic_ns = time.monotonic_ns() +monotonic_after = time.monotonic() + +assert isinstance(monotonic_before, float) +assert isinstance(monotonic_ns, int) +assert monotonic_before <= monotonic_ns / 1_000_000_000 <= monotonic_after + +perf_before = time.perf_counter() +perf_ns = time.perf_counter_ns() +perf_after = time.perf_counter() + +assert isinstance(perf_before, float) +assert isinstance(perf_ns, int) +assert perf_before <= perf_ns / 1_000_000_000 <= perf_after + +monotonic_start = time.monotonic() +perf_start = time.perf_counter() + +time.sleep(0.02) + +monotonic_elapsed = time.monotonic() - monotonic_start +perf_elapsed = time.perf_counter() - perf_start + +assert monotonic_elapsed >= 0.01 +assert perf_elapsed >= 0.01 -# Regression test for RustPython issue #4938: -# struct_time field overflow should raise OverflowError (matching CPython), -# not TypeError. Covers mktime, asctime, and strftime. -I32_MAX_PLUS_1 = 2147483648 -overflow_cases = [ - (I32_MAX_PLUS_1, 1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in year - (2024, I32_MAX_PLUS_1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in month - (2024, 1, I32_MAX_PLUS_1, 0, 0, 0, 0, 0, 0), # i32 overflow in mday - (2024, 1, 1, 0, 0, I32_MAX_PLUS_1, 0, 0, 0), # i32 overflow in sec - (88888888888,) * 9, # multi-field i32 overflow -] - -for case in overflow_cases: - for func_name, call in [ - ("mktime", lambda c=case: time.mktime(c)), - ("asctime", lambda c=case: time.asctime(c)), - ("strftime", lambda c=case: time.strftime("%Y", c)), - ]: - try: - call() - except OverflowError: - pass # expected, matches CPython - except TypeError as e: - raise AssertionError( - f"{func_name}({case}) raised TypeError (should be OverflowError): {e}" - ) from e - else: - raise AssertionError( - f"{func_name}({case}) did not raise — expected OverflowError" - ) +# The optional second argument fills the fields that are not part of the +# sequence. +fields = (2024, 1, 2, 3, 4, 5, 6, 7, 0) +assert time.struct_time(fields).tm_zone is None +assert time.struct_time(fields, {"tm_zone": "UTC"}).tm_zone == "UTC" +assert time.struct_time(fields, {"tm_gmtoff": 60}).tm_gmtoff == 60 +try: + time.struct_time(fields, ["tm_zone", "UTC"]) +except TypeError: + pass +else: + assert False, "struct_time accepted a non-dict second argument" diff --git a/extra_tests/snippets/stdlib_traceback.py b/extra_tests/snippets/stdlib_traceback.py index c2cc5773dbc..b1b11a75503 100644 --- a/extra_tests/snippets/stdlib_traceback.py +++ b/extra_tests/snippets/stdlib_traceback.py @@ -1,5 +1,9 @@ +import itertools import traceback +import _suggestions +from testutils import assert_raises + try: 1 / 0 except ZeroDivisionError as ex: @@ -25,3 +29,10 @@ except ZeroDivisionError as ex2: tb = traceback.extract_tb(ex2.__traceback__) assert len(tb) == 1 + +# The candidate list backing "Did you mean" suggestions is a list; an arbitrary +# iterable must be rejected rather than drained. + +with assert_raises(TypeError): + _suggestions._generate_suggestions(itertools.count(), "x") +assert _suggestions._generate_suggestions(["value"], "valu") == "value" diff --git a/extra_tests/snippets/stdlib_types.py b/extra_tests/snippets/stdlib_types.py index cdecf12dd2b..4bccd2985bf 100644 --- a/extra_tests/snippets/stdlib_types.py +++ b/extra_tests/snippets/stdlib_types.py @@ -1,5 +1,6 @@ import _ast import platform +import sys import types from testutils import assert_raises @@ -34,3 +35,26 @@ def _run_missing_type_params_regression(): _run_missing_type_params_regression() + +if sys.implementation.name == "rustpython": + # __parameters__ is computed when the alias is built, and the walk descends + # into every list and tuple argument, so a self-referential or deeply + # nested argument must be caught. CPython, which also runs this snippet, + # does not walk into plain lists at all. + self_referential = [] + self_referential.append(self_referential) + with assert_raises(RecursionError): + list[self_referential] + + nested = [0] + for _ in range(100_000): + nested = [nested] + with assert_raises(RecursionError): + list[nested] + + # hashing an alias walks the same shape + deep_alias = int + for _ in range(100_000): + deep_alias = list[deep_alias] + with assert_raises(RecursionError): + hash(deep_alias) diff --git a/extra_tests/snippets/stdlib_typing.py b/extra_tests/snippets/stdlib_typing.py index 07348945842..4082d683f8d 100644 --- a/extra_tests/snippets/stdlib_typing.py +++ b/extra_tests/snippets/stdlib_typing.py @@ -1,6 +1,9 @@ from collections.abc import Awaitable, Callable from typing import TypeVar +import _typing +from testutils import assert_raises + T = TypeVar("T") @@ -35,3 +38,28 @@ def __init__( def method(self, value: Union[int, float]) -> Union[str, bytes]: return str(value) + + +# _idfunc takes exactly one argument, checked before the argument is read. + +assert _typing._idfunc(1) == 1 +with assert_raises(TypeError): + _typing._idfunc() + + +# ParamSpecArgs shows a non-ParamSpec origin by its repr, which is where the +# recursion guard lives; nesting them deeply must not walk the native stack. + +from typing import ParamSpec, ParamSpecArgs + +spec = ParamSpec("spec") +assert repr(spec.args) == "spec.args" +assert repr(spec.kwargs) == "spec.kwargs" + +nested = object() +for _ in range(2000): + nested = ParamSpecArgs(nested) +try: + repr(nested) +except RecursionError: + pass diff --git a/extra_tests/snippets/stdlib_unicode_shared.py b/extra_tests/snippets/stdlib_unicode_shared.py new file mode 100644 index 00000000000..ff8bc0533e0 --- /dev/null +++ b/extra_tests/snippets/stdlib_unicode_shared.py @@ -0,0 +1,93 @@ +# Exercises the Unicode semantics routed through the shared rustpython-unicode +# crate: str predicates, casefold, identifier rules, unicodedata queries, +# normalization, \N{} escapes, and re character classes. + +import re +import unicodedata + +# --- str classification predicates --------------------------------------- + +# Numeric_Type chain: isdecimal ⊂ isdigit ⊂ isnumeric +assert "5".isdecimal() and "5".isdigit() and "5".isnumeric() +assert not "²".isdecimal() # SUPERSCRIPT TWO: digit but not decimal +assert "²".isdigit() and "²".isnumeric() +assert not "⅓".isdigit() # VULGAR FRACTION ONE THIRD: numeric only +assert "⅓".isnumeric() + +assert "abc".isalpha() +assert "abc123".isalnum() +assert not "abc123".isalpha() +assert "あ".isalpha() # HIRAGANA LETTER A + +assert " \t\n".isspace() +assert " ".isspace() # IDEOGRAPHIC SPACE +assert "hello world".isprintable() +assert not "\x00".isprintable() +assert " ".isprintable() # ASCII space is printable + +# identifier rules (XID_Start / XID_Continue, plus leading underscore) +assert "_var".isidentifier() +assert "유니코드".isidentifier() # Hangul identifier +assert not "1abc".isidentifier() +assert not "a b".isidentifier() + +# --- case mapping / casefold --------------------------------------------- + +assert "ABC".lower() == "abc" +assert "abc".upper() == "ABC" +# casefold uses full mappings, unlike lower() +assert "ß".casefold() == "ss" # LATIN SMALL LETTER SHARP S +assert "Σ".casefold() == "σ" # GREEK CAPITAL SIGMA -> small sigma +assert "Straße".casefold() == "strasse" + +# lone-surrogate safety: casefold must not panic on surrogates +surrogate = "\ud800" +assert surrogate.casefold() == surrogate + +# --- unicodedata ---------------------------------------------------------- + +assert unicodedata.category("A") == "Lu" +assert unicodedata.category("1") == "Nd" +assert unicodedata.bidirectional("A") == "L" +assert unicodedata.decimal("٥") == 5 # ARABIC-INDIC DIGIT FIVE +assert unicodedata.digit("²") == 2 +assert abs(unicodedata.numeric("⅓") - (1 / 3)) < 1e-6 +assert unicodedata.name("☃") == "SNOWMAN" +assert unicodedata.lookup("SNOWMAN") == "☃" +assert unicodedata.combining("́") == 230 # COMBINING ACUTE ACCENT +assert unicodedata.mirrored("(") == 1 +assert unicodedata.east_asian_width("あ") == "W" + +# ucd_3_2_0 legacy view (used by stringprep) +assert unicodedata.ucd_3_2_0.unidata_version == "3.2.0" + +# --- normalization -------------------------------------------------------- + +composed = "é" # é +decomposed = "é" +assert unicodedata.normalize("NFC", decomposed) == composed +assert unicodedata.normalize("NFD", composed) == decomposed +assert unicodedata.is_normalized("NFC", composed) +assert not unicodedata.is_normalized("NFD", composed) + +# --- \N{} escapes (compiler) --------------------------------------------- + +assert "\N{SNOWMAN}" == "☃" +assert "\N{GREEK SMALL LETTER ALPHA}" == "α" + +# --- re character classes ------------------------------------------------- + +assert re.fullmatch(r"\w+", "abc_123") is not None +assert re.fullmatch(r"\w+", "유니코드") is not None # \w is Unicode-aware +assert re.fullmatch(r"\d+", "123") is not None +# \d matches Unicode decimal digits (category Nd), not just ASCII +assert re.fullmatch(r"\d", "٥") is not None # ARABIC-INDIC DIGIT FIVE +assert re.fullmatch(r"\d", "५") is not None # DEVANAGARI DIGIT FIVE +assert re.fullmatch(r"\d", "²") is None # SUPERSCRIPT TWO (No), not decimal +assert re.fullmatch(r"\s+", " \t\n") is not None +# ASCII flag restricts \w to ASCII +assert re.fullmatch(r"\w+", "유", re.ASCII) is None +# case-insensitive matching routes through the shared case helpers +assert re.fullmatch(r"straße", "STRAßE", re.IGNORECASE) is not None + +print("stdlib_unicode_shared: OK") diff --git a/extra_tests/snippets/stdlib_xml.py b/extra_tests/snippets/stdlib_xml.py new file mode 100644 index 00000000000..34f1a726e4c --- /dev/null +++ b/extra_tests/snippets/stdlib_xml.py @@ -0,0 +1,68 @@ +import xml.sax +from xml.parsers import expat + +from testutils import assert_raises + +assert expat.XML_PARAM_ENTITY_PARSING_NEVER == 0 +assert expat.XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE == 1 +assert expat.XML_PARAM_ENTITY_PARSING_ALWAYS == 2 + +parser = expat.ParserCreate() +for value in (0, 1, 2, 3, -1, True): + assert parser.SetParamEntityParsing(value) == 1 + +for value in ("x", None): + with assert_raises(TypeError): + parser.SetParamEntityParsing(value) + +with assert_raises(OverflowError): + parser.SetParamEntityParsing(2**100) + +assert parser.GetBase() is None +assert parser.SetBase("example.xml") is None +assert parser.GetBase() == "example.xml" +for value in (b"example.xml", None, 123): + with assert_raises(TypeError): + parser.SetBase(value) + + +class Handler(xml.sax.handler.ContentHandler): + def __init__(self): + self.events = [] + + def startElement(self, name, attrs): + self.events.append(("start", name)) + + def endElement(self, name): + self.events.append(("end", name)) + + +handler = Handler() +xml.sax.parseString("
", handler) +assert handler.events == [ + ("start", "main"), + ("start", "child"), + ("end", "child"), + ("end", "main"), +] + +events = [] +parser = expat.ParserCreate() +parser.ProcessingInstructionHandler = lambda target, data: events.append( + ("processing-instruction", target, data) +) +parser.CommentHandler = lambda data: events.append(("comment", data)) +parser.StartCdataSectionHandler = lambda: events.append(("start-cdata",)) +parser.CharacterDataHandler = lambda data: events.append(("characters", data)) +parser.EndCdataSectionHandler = lambda: events.append(("end-cdata",)) +parser.Parse( + "", True +) +assert events == [ + ("processing-instruction", "target", "data"), + ("processing-instruction", "empty", ""), + ("comment", "comment"), + ("start-cdata",), + ("characters", "text"), + ("end-cdata",), +] diff --git a/extra_tests/snippets/syntax_annotations_locals.py b/extra_tests/snippets/syntax_annotations_locals.py new file mode 100644 index 00000000000..8669d94b2d3 --- /dev/null +++ b/extra_tests/snippets/syntax_annotations_locals.py @@ -0,0 +1,50 @@ +"""Module/class-scope locals() must not corrupt or leak __conditional_annotations__. + +CPython's _PyFrame_GetLocals never syncs cell variables into a module/class +scope's namespace dict, it just returns the dict directly (verified against +CPython 3.14.6). __conditional_annotations__ is a cell in both scopes, but +only module codegen also writes it into the dict (StoreName); class codegen +only ever uses the cell (StoreDeref). So it's visible via locals()/dir() at +module scope and absent at class scope. + +RustPython's fast-locals-to-mapping sync used to read every cellvar's value +straight from the cell regardless of scope. At module scope the cell is +always empty, so this overwrote the dict's real value with None -- deleting +it, and the next annotated statement raised NameError. At class scope it +leaked __conditional_annotations__ into locals()/dir(), which CPython never +does. +""" + +count: int = 1 +_ = locals() +maybe: int = None # used to raise NameError before the fix +assert maybe is None + +assert "__conditional_annotations__" in dir(), ( + "module-level annotation should expose __conditional_annotations__, matching CPython" +) + +exec("a: int = 1\nlocals()\nb: int = 2") + +if True: + x: int = 1 +vars() +if True: + y: int = 2 +assert (x, y) == (1, 2) + + +class C: + if True: + cx: int = 1 + locals() + if True: + cy: int = 2 + assert "__conditional_annotations__" not in dir(), ( + "class-level locals() should not leak __conditional_annotations__, matching CPython" + ) + + +assert (C.cx, C.cy) == (1, 2) + +print("ok") diff --git a/extra_tests/snippets/syntax_class.py b/extra_tests/snippets/syntax_class.py index 4e80e7edf8c..4d1a99edfb0 100644 --- a/extra_tests/snippets/syntax_class.py +++ b/extra_tests/snippets/syntax_class.py @@ -163,6 +163,29 @@ def t1(self): cm = classmethod(lambda cls: cls) assert cm.__func__(int) is int + +class Callback: + def __init__(self, error): + self.error = error + + def __call__(self, *args, **kwargs): + pass + + def __repr__(self): + raise self.error + + +callback = Callback(RuntimeError("callback is unavailable")) + +with assert_raises(RuntimeError) as caught: + repr(staticmethod(callback)) +assert caught.exception is callback.error + +with assert_raises(RuntimeError) as caught: + repr(classmethod(callback)) +assert caught.exception is callback.error + + assert str(super(int, 5)) == ", >" class T5(int): diff --git a/extra_tests/snippets/syntax_try.py b/extra_tests/snippets/syntax_try.py index 1f46caae3e7..5610cb23e6a 100644 --- a/extra_tests/snippets/syntax_try.py +++ b/extra_tests/snippets/syntax_try.py @@ -285,3 +285,85 @@ def y(): try: pass """) + + +# leaving the try block early emits an extra copy of the finally body, which +# must not consume the symbol tables of the nested scopes it contains +def return_from_try(): + log = [] + try: + return "returned" + finally: + log.append((lambda x: x * 2)(3)) + log.append({t for t in [1, 2]}) + log.append([t for t in [3]]) + log.append({k: k for k in [4]}) + + def nested(): + return 5 + + class Nested: + value = 6 + + assert log == [6, {1, 2}, [3], {4: 4}], log + assert nested() == 5 + assert Nested.value == 6 + + +assert return_from_try() == "returned" + + +def break_and_continue_from_try(): + seen = [] + for i in range(4): + try: + if i == 1: + continue + if i == 3: + break + seen.append(i) + finally: + seen.append({t for t in [i]}) + return seen + + +assert break_and_continue_from_try() == [0, {0}, {1}, 2, {2}, {3}] + + +def return_from_try_runs_finally_once(): + log = [] + + def inner(): + try: + return "value" + finally: + log.append(sorted({t for t in "ab"})) + + assert inner() == "value" + return log + + +assert return_from_try_runs_finally_once() == [["a", "b"]] + + +def generator_return_from_try(): + log = [] + + def gen(): + try: + return (yield "yielded") + finally: + log.append([t for t in "z"]) + + g = gen() + assert g.send(None) == "yielded" + try: + g.send("sent") + except StopIteration as stop: + assert stop.value == "sent", stop.value + else: + assert False, "generator did not stop" + return log + + +assert generator_return_from_try() == [["z"]] diff --git a/extra_tests/snippets/vm_specialization.py b/extra_tests/snippets/vm_specialization.py index 2c884cc2f6d..f2415b4b2e8 100644 --- a/extra_tests/snippets/vm_specialization.py +++ b/extra_tests/snippets/vm_specialization.py @@ -69,3 +69,145 @@ def check_latin1_subscr_singleton_after_warmup(): check_latin1_subscr_singleton_after_warmup() + + +## LOAD_ATTR_METHOD_WITH_VALUES: keys-version shadow check + + +class MethodHolder: + def m(self): + return "method" + + +def method_shadowed_after_specialization(): + obj = MethodHolder() + obj.pad = 1 + for _ in range(300): + assert obj.m() == "method" + # Shadowing after warmup must deopt the stamp-based shadow skip. + obj.m = lambda: "instance" + assert obj.m() == "instance" + del obj.m + assert obj.m() == "method" + obj.__dict__["m"] = lambda: "dict" + assert obj.m() == "dict" + del obj.__dict__["m"] + assert obj.m() == "method" + + +method_shadowed_after_specialization() + + +def method_with_value_only_updates(): + obj = MethodHolder() + obj.pad = 0 + for i in range(500): + obj.pad = i # value-only update keeps the keys-version stamp + assert obj.m() == "method" + + +method_with_value_only_updates() + + +## LOAD_ATTR_WITH_HINT / STORE_ATTR: entry-index hint invalidation + + +class Plain: + pass + + +def load_hint_survives_key_churn(): + obj = Plain() + obj.a = 1 + obj.b = 2 + obj.x = "first" + for _ in range(300): + assert obj.x == "first" + del obj.a + del obj.b + assert obj.x == "first" + del obj.x + try: + obj.x + except AttributeError: + pass + else: + raise AssertionError("expected AttributeError") + obj.x = "second" + assert obj.x == "second" + + +load_hint_survives_key_churn() + + +def store_hint_survives_dict_replacement(): + obj = Plain() + obj.v = 0 + for i in range(500): + obj.v = i + assert obj.v == i + obj.__dict__ = {"v": "fresh"} + for i in range(300): + obj.v = i + assert obj.v == i + obj.__dict__.clear() + obj.v = "back" + assert obj.v == "back" + + +store_hint_survives_dict_replacement() + + +## Shared shape stamps: same-layout instances share the shadow-check stamp + + +class ShapedCounter: + def __init__(self): + self.a = 1 + self.b = 2 + + def m(self): + return "method" + + +def shape_sharing_shadow_one_instance(): + objs = [ShapedCounter() for _ in range(30)] + for _ in range(300): + for o in objs: + assert o.m() == "method" + objs[13].m = lambda: "thirteen" + for i, o in enumerate(objs): + expected = "thirteen" if i == 13 else "method" + assert o.m() == expected + del objs[13].m + for o in objs: + assert o.m() == "method" + + +shape_sharing_shadow_one_instance() + + +def holey_dict_falls_back(): + class Holey: + def m(self): + return "g" + + def call_m(obj): + # single LOAD_ATTR cache site shared by both instances + return obj.m() + + g1, g2 = Holey(), Holey() + for o in (g1, g2): + o.x = 1 + o.y = 2 + o.z = 3 + del o.y # leaves a hole in the entries + for _ in range(300): + assert call_m(g1) == "g" + assert call_m(g2) == "g" + g2.m = lambda: "g2" + assert call_m(g1) == "g" + assert call_m(g2) == "g2" + + +holey_dict_falls_back() diff --git a/ruff.toml b/ruff.toml index 2ed67851f0a..7cfaafdb08a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -13,3 +13,6 @@ select = [ "F7", "F82", ] + +[lint.isort] +known-first-party = ["cpython", "opcodes", "utils"] diff --git a/scripts/check_redundant_patches.py b/scripts/check_redundant_patches.py index 4bc89a573d4..e9981e38d60 100644 --- a/scripts/check_redundant_patches.py +++ b/scripts/check_redundant_patches.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import ast import glob diff --git a/scripts/update_lib/deps.py b/scripts/update_lib/deps.py index 72374aa6be1..da49490c7ec 100644 --- a/scripts/update_lib/deps.py +++ b/scripts/update_lib/deps.py @@ -724,6 +724,12 @@ def clear_import_graph_caches() -> None: "curses_tests.py", ], }, + "signal": { + "test": [ + "signalinterproctester.py", + "test_signal.py", + ] + }, } diff --git a/src/interpreter.rs b/src/interpreter.rs index 230192d1e21..89e24c25b08 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -57,7 +57,23 @@ fn setup_dynamic_stdlib(vm: &mut crate::VirtualMachine) { use rustpython_vm::common::rc::PyRc; let state = PyRc::get_mut(&mut vm.state).unwrap(); - let paths = collect_stdlib_paths(); + let paths: Vec = collect_stdlib_paths() + .into_iter() + .map(|p| { + std::fs::canonicalize(&p) + .map(|canonical| { + let s = canonical.to_string_lossy(); + #[cfg(windows)] + { + if let Some(stripped) = s.strip_prefix(r"\\?\") { + return stripped.to_owned(); + } + } + s.into_owned() + }) + .unwrap_or(p) + }) + .collect(); // Set stdlib_dir to the first stdlib path if available if let Some(first_path) = paths.first() { diff --git a/src/lib.rs b/src/lib.rs index 9a5cede9bd4..dfede27fe23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -303,7 +303,7 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { RunMode::InstallPip(installer) => install_pip(installer, scope.clone(), vm), RunMode::Script(script_path) => { // pymain_run_file_obj - debug!("Running script {}", &script_path); + debug!("Running script {}", script_path); run_file(vm, scope.clone(), &script_path) } RunMode::Repl => Ok(()), diff --git a/src/shell.rs b/src/shell.rs index bc7ccec5c9d..7fb9336af4b 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -10,6 +10,7 @@ use rustpython_vm::{ compiler::{self}, readline::{Readline, ReadlineResult}, scope::Scope, + vm::VmCompileError, }; enum ShellExecResult { @@ -45,25 +46,25 @@ fn shell_exec( ShellExecResult::Ok } } - Err(CompileError::Parse(ParseError { + Err(VmCompileError::Compile(CompileError::Parse(ParseError { error: ParseErrorType::Lexical(LexicalErrorType::Eof), .. - })) => ShellExecResult::ContinueLine, - Err(CompileError::Parse(ParseError { + }))) => ShellExecResult::ContinueLine, + Err(VmCompileError::Compile(CompileError::Parse(ParseError { error: ParseErrorType::Lexical(LexicalErrorType::FStringError( InterpolatedStringErrorType::UnterminatedTripleQuotedString, )), .. - })) => ShellExecResult::ContinueLine, + }))) => ShellExecResult::ContinueLine, Err(err) => { // Check if the error is from an unclosed triple quoted string (which should always // continue) - if let CompileError::Parse(ParseError { + if let VmCompileError::Compile(CompileError::Parse(ParseError { error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError), raw_location, .. - }) = err + })) = &err { let loc = raw_location.start().to_usize(); let mut iter = source.chars(); @@ -80,8 +81,8 @@ fn shell_exec( // since indentations errors on columns other than 0 should be ignored. // if its an unrecognized token for dedent, set to false - let bad_error = match err { - CompileError::Parse(ref p) => { + let bad_error = match &err { + VmCompileError::Compile(CompileError::Parse(p)) => { match &p.error { ParseErrorType::Lexical(LexicalErrorType::IndentationError) => { continuing_block @@ -97,7 +98,7 @@ fn shell_exec( // If we are handling an error on an empty line or an error worthy of throwing if empty_line_given || bad_error { - ShellExecResult::PyErr(vm.new_syntax_error(&err, Some(source))) + ShellExecResult::PyErr(err.into_pyexception(vm, Some(source))) } else { ShellExecResult::ContinueBlock } diff --git a/tools/opcode_metadata/generate_rs_opcode_metadata.py b/tools/opcode_metadata/generate_rs_opcode_metadata.py index df2476c5e08..97482f337a1 100644 --- a/tools/opcode_metadata/generate_rs_opcode_metadata.py +++ b/tools/opcode_metadata/generate_rs_opcode_metadata.py @@ -11,6 +11,7 @@ import typing import tomllib + from cpython import Analysis, get_analysis, get_stack_effect from opcodes import OpcodeInfo from utils import DEFAULT_INPUT, ROOT, get_conf, to_pascal_case @@ -27,6 +28,7 @@ def fn_as_info_size(self) -> str: return f""" /// Returns [`Self`] as [`{self.size}`]. #[must_use] + #[inline] pub const fn as_{self.size}(self) -> {self.size} {{ self.as_numeric() }} @@ -112,6 +114,7 @@ def fn_to_base(self) -> str: return f""" #[must_use] + #[inline] pub const fn to_base(self) -> Option {{ {inner} }} @@ -145,25 +148,30 @@ def fn_to_instrumented(self) -> str: @property def fn_deopt(self) -> str: - arms = "" - for target, specialized in self.info.deopts.items(): - ops = "|".join(f"Self::{op}" for op in specialized) - arms += f"{ops} => Self::{target},\n" - - arms = arms.strip() + specialized_to_base = self.specialized_to_base - if not arms: + if not specialized_to_base: inner = "None" else: + table_type = f"super::{self.info.enum_name}" + entries = ",\n".join( + f"Some({table_type}::{specialized_to_base[name]})" + if name in specialized_to_base + else "None" + for name in self.rust_names_by_id + ) + inner = f""" - Some(match self {{ - {arms} - _ => return None, - }}) + const DEOPT: [Option<{table_type}>; {self.table_size}] = [ + {entries} + ]; + + DEOPT[self.as_numeric() as usize] """ return f""" #[must_use] + #[inline] pub const fn deopt(self) -> Option {{ {inner} }} @@ -171,7 +179,7 @@ def fn_deopt(self) -> str: @property def fn_cache_entries(self) -> str: - arms = "" + entries_by_base: dict[str, int] = {} for opcode in self: name = opcode.rust_name if opcode.is_instrumented: @@ -185,21 +193,25 @@ def fn_cache_entries(self) -> str: continue if size > 1: - arms += f"Self::{name} => {size - 1},\n" + entries_by_base[name] = size - 1 - arms = arms.strip() - if not arms: + if not entries_by_base: inner = "0" else: + entries = ", ".join( + str(entries_by_base.get(self.resolve_deoptimized(name), 0)) + for name in self.rust_names_by_id + ) + inner = f""" - match self.deoptimize() {{ - {arms} - _ => 0, - }} + const CACHE_ENTRIES: [u8; {self.table_size}] = [{entries}]; + + CACHE_ENTRIES[self.as_numeric() as usize] as usize """ return f""" #[must_use] + #[inline] pub const fn cache_entries(self) -> usize {{ {inner} }} @@ -322,6 +334,42 @@ def instrumented_mapping(self) -> dict[str, str]: return res + @property + def specialized_to_base(self) -> dict[str, str]: + """Maps a specialized opcode's name to its family's base opcode name.""" + res = {} + for target, specialized in self.info.deopts.items(): + for name in specialized: + res[name] = target + + return res + + @property + def instrumented_to_base(self) -> dict[str, str]: + """Maps an instrumented opcode's name to its base opcode name.""" + return {iname: name for name, iname in self.instrumented_mapping.items()} + + def resolve_deoptimized(self, name: str) -> str: + """ + Mirrors `deoptimize`: resolves a specialized opcode to its family's + base, an instrumented opcode to its base, or returns the name + unchanged. + """ + if name in self.specialized_to_base: + return self.specialized_to_base[name] + + return self.instrumented_to_base.get(name, name) + + @property + def table_size(self) -> int: + return {"u8": 256, "u16": 65536}[self.size] + + @property + def rust_names_by_id(self) -> list[str | None]: + """The opcode name at each numeric id, `None` where no opcode is assigned.""" + names_by_id = {opcode.id: opcode.rust_name for opcode in self} + return [names_by_id.get(i) for i in range(self.table_size)] + @property def size(self) -> str: return self.info.size diff --git a/wasm/demo/package-lock.json b/wasm/demo/package-lock.json index 21385a868a0..b114d92ebb8 100644 --- a/wasm/demo/package-lock.json +++ b/wasm/demo/package-lock.json @@ -24,7 +24,7 @@ "serve": "^14.2.6", "webpack": "^5.105.0", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.4" + "webpack-dev-server": "^5.2.6" } }, "node_modules/@codemirror/autocomplete": { @@ -2559,9 +2559,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { @@ -3026,9 +3026,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3704,9 +3704,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -4069,9 +4069,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -4089,7 +4089,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4849,9 +4849,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "dev": true, "license": "MIT", "engines": { @@ -5691,9 +5691,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", - "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz", + "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==", "dev": true, "license": "MIT", "dependencies": { @@ -5715,7 +5715,7 @@ "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", + "launch-editor": "^2.14.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", @@ -5787,9 +5787,9 @@ } }, "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5911,9 +5911,9 @@ } }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { diff --git a/wasm/demo/package.json b/wasm/demo/package.json index 0b22c24ea50..2aa9e5867ae 100644 --- a/wasm/demo/package.json +++ b/wasm/demo/package.json @@ -19,7 +19,7 @@ "serve": "^14.2.6", "webpack": "^5.105.0", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.4" + "webpack-dev-server": "^5.2.6" }, "scripts": { "dev": "webpack serve", diff --git a/wasm/demo/src/index.js b/wasm/demo/src/index.js index 0b568fa1d9e..aeab8e716e9 100644 --- a/wasm/demo/src/index.js +++ b/wasm/demo/src/index.js @@ -159,6 +159,7 @@ function onReady() { terminalVM = rp.vmStore.init('term_vm'); terminalVM.setStdout((data) => readline.print(data)); + terminalVM.setStderr((data) => readline.print(data)); readPrompts().catch((err) => console.error(err)); // so that the test knows that we're ready diff --git a/wasm/tests/test_exec_mode.py b/wasm/tests/test_exec_mode.py index a2a55846f48..28a0cea7ca8 100644 --- a/wasm/tests/test_exec_mode.py +++ b/wasm/tests/test_exec_mode.py @@ -19,3 +19,17 @@ def test_exec_single_mode(wdriver): """ ) assert stdout == "2\n4\n" + + +def test_exec_stderr_option(wdriver): + stderr = wdriver.execute_script( + """ + let output = ""; + save_output = function(text) { + output += text + }; + window.rp.pyExec('import sys; print("err", file=sys.stderr)', {stderr: save_output}); + return output; + """ + ) + assert stderr == "err\n"